Add sound options and semi-separate front-end from engine

This commit is contained in:
Nguyễn Gia Phong 2018-02-19 16:55:55 +07:00
parent 8852a9f678
commit bc47fb3f30
8 changed files with 107 additions and 67 deletions

View File

@ -24,7 +24,6 @@ from random import choice, randrange, shuffle
from sys import modules from sys import modules
import pygame import pygame
from pygame.mixer import Sound
from pygame.time import get_ticks from pygame.time import get_ticks
from .constants import * from .constants import *
@ -50,7 +49,7 @@ class Hero:
spin_speed (float): speed of spinning (in frames per slash) spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning spin_queue (float): frames left to finish spinning
wound (float): amount of wound wound (float): amount of wound
sfx_heart (Sound): heart beat sound effect sfx_heart (pygame.mixer.Sound): heart beat sound effect
""" """
def __init__(self, surface, fps): def __init__(self, surface, fps):
self.surface = surface self.surface = surface
@ -64,7 +63,7 @@ class Hero:
self.spin_speed = fps / HERO_HP self.spin_speed = fps / HERO_HP
self.spin_queue = self.wound = 0.0 self.spin_queue = self.wound = 0.0
self.sfx_heart = Sound(SFX_HEART) self.sfx_heart = SFX_HEART
def update(self, fps): def update(self, fps):
"""Update the hero.""" """Update the hero."""
@ -92,6 +91,9 @@ class Hero:
x, y = pygame.mouse.get_pos() x, y = pygame.mouse.get_pos()
self.angle = atan2(y - self.y, x - self.x) self.angle = atan2(y - self.y, x - self.x)
self.spin_queue = 0.0 self.spin_queue = 0.0
def draw(self):
"""Draw the hero."""
trigon = regpoly(3, self.R, self.angle, self.x, self.y) trigon = regpoly(3, self.R, self.angle, self.x, self.y)
fill_aapolygon(self.surface, trigon, self.color[int(self.wound)]) fill_aapolygon(self.surface, trigon, self.color[int(self.wound)])
@ -117,7 +119,7 @@ class Enemy:
spin_speed (float): speed of spinning (in frames per slash) spin_speed (float): speed of spinning (in frames per slash)
spin_queue (float): frames left to finish spinning spin_queue (float): frames left to finish spinning
wound (float): amount of wound wound (float): amount of wound
sfx_slash (Sound): sound effect indicating close-range attack damage sfx_slash (pygame.mixer.Sound): sound effect of slashed hero
""" """
def __init__(self, maze, x, y, color): def __init__(self, maze, x, y, color):
self.maze = maze self.maze = maze
@ -132,7 +134,7 @@ class Enemy:
self.spin_speed = self.maze.fps / ENEMY_HP self.spin_speed = self.maze.fps / ENEMY_HP
self.spin_queue = self.wound = 0.0 self.spin_queue = self.wound = 0.0
self.sfx_slash = Sound(SFX_SLASH_HERO) self.sfx_slash = SFX_SLASH_HERO
def get_pos(self): def get_pos(self):
"""Return coordinate of the center of the enemy.""" """Return coordinate of the center of the enemy."""
@ -238,6 +240,7 @@ class Enemy:
def draw(self): def draw(self):
"""Draw the enemy.""" """Draw the enemy."""
if get_ticks() < self.maze.next_move and not self.awake: return
radious = self.maze.distance/SQRT2 - self.awake*2 radious = self.maze.distance/SQRT2 - self.awake*2
square = regpoly(4, radious, self.angle, *self.get_pos()) square = regpoly(4, radious, self.angle, *self.get_pos())
color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR color = TANGO[self.color][int(self.wound)] if self.awake else FG_COLOR
@ -257,7 +260,6 @@ class Enemy:
self.spin_queue -= sign(self.spin_queue) self.spin_queue -= sign(self.spin_queue)
else: else:
self.angle, self.spin_queue = pi / 4, 0.0 self.angle, self.spin_queue = pi / 4, 0.0
if self.awake or get_ticks() >= self.maze.next_move: self.draw()
def hit(self, wound): def hit(self, wound):
"""Handle the enemy when it's attacked.""" """Handle the enemy when it's attacked."""
@ -290,8 +292,7 @@ class Chameleon(Enemy):
def draw(self): def draw(self):
"""Draw the Chameleon.""" """Draw the Chameleon."""
if (not self.awake or self.spin_queue if not self.awake or get_ticks() < self.visible or self.spin_queue:
or get_ticks() < max(self.visible, self.maze.next_move)):
Enemy.draw(self) Enemy.draw(self)
def hit(self, wound): def hit(self, wound):

View File

@ -19,21 +19,25 @@
__doc__ = 'brutalmaze module for shared constants' __doc__ = 'brutalmaze module for shared constants'
from pkg_resources import resource_filename from pkg_resources import resource_filename as pkg_file
from pygame import image import pygame
from pygame.mixer import Sound from pygame.mixer import Sound
SETTINGS = resource_filename('brutalmaze', 'settings.ini') SETTINGS = pkg_file('brutalmaze', 'settings.ini')
ICON = image.load(resource_filename('brutalmaze', 'icon.png')) ICON = pygame.image.load(pkg_file('brutalmaze', 'icon.png'))
MUSIC = resource_filename('brutalmaze', 'soundfx/music.ogg') MUSIC = pkg_file('brutalmaze', 'soundfx/music.ogg')
SFX_SPAWN = resource_filename('brutalmaze', 'soundfx/spawn.ogg')
SFX_SLASH_ENEMY = resource_filename('brutalmaze', 'soundfx/slash-enemy.ogg') mixer = pygame.mixer.get_init()
SFX_SLASH_HERO = resource_filename('brutalmaze', 'soundfx/slash-hero.ogg') if mixer is None: pygame.mixer.init(frequency=44100)
SFX_SHOT_ENEMY = resource_filename('brutalmaze', 'soundfx/shot-enemy.ogg') SFX_SPAWN = Sound(pkg_file('brutalmaze', 'soundfx/spawn.ogg'))
SFX_SHOT_HERO = resource_filename('brutalmaze', 'soundfx/shot-hero.ogg') SFX_SLASH_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/slash-enemy.ogg'))
SFX_MISSED = resource_filename('brutalmaze', 'soundfx/missed.ogg') SFX_SLASH_HERO = Sound(pkg_file('brutalmaze', 'soundfx/slash-hero.ogg'))
SFX_HEART = resource_filename('brutalmaze', 'soundfx/heart.ogg') SFX_SHOT_ENEMY = Sound(pkg_file('brutalmaze', 'soundfx/shot-enemy.ogg'))
SFX_LOSE = resource_filename('brutalmaze', 'soundfx/lose.ogg') SFX_SHOT_HERO = Sound(pkg_file('brutalmaze', 'soundfx/shot-hero.ogg'))
SFX_MISSED = Sound(pkg_file('brutalmaze', 'soundfx/missed.ogg'))
SFX_HEART = Sound(pkg_file('brutalmaze', 'soundfx/heart.ogg'))
SFX_LOSE = Sound(pkg_file('brutalmaze', 'soundfx/lose.ogg'))
if mixer is None: pygame.mixer.quit()
SQRT2 = 2 ** 0.5 SQRT2 = 2 ** 0.5
INIT_SCORE = 5**0.5/2 + 0.5 # golden mean INIT_SCORE = 5**0.5/2 + 0.5 # golden mean

View File

@ -35,7 +35,7 @@ from pygame import DOUBLEBUF, KEYDOWN, OPENGL, QUIT, RESIZABLE, VIDEORESIZE
from pygame.time import Clock, get_ticks from pygame.time import Clock, get_ticks
from appdirs import AppDirs from appdirs import AppDirs
from .constants import * from .constants import SETTINGS, ICON, MUSIC, HERO_SPEED
from .maze import Maze from .maze import Maze
from .misc import sign from .misc import sign
@ -44,7 +44,8 @@ class ConfigReader:
"""Object reading and processing INI configuration file for """Object reading and processing INI configuration file for
Brutal Maze. Brutal Maze.
""" """
CONTROL_ALIASES = (('New game', 'new'), ('Pause', 'pause'), CONTROL_ALIASES = (('New game', 'new'), ('Toggle pause', 'pause'),
('Toggle mute', 'mute'),
('Move left', 'left'), ('Move right', 'right'), ('Move left', 'left'), ('Move right', 'right'),
('Move up', 'up'), ('Move down', 'down'), ('Move up', 'up'), ('Move down', 'down'),
('Long-range attack', 'shot'), ('Long-range attack', 'shot'),
@ -57,12 +58,14 @@ class ConfigReader:
self.config.read(SETTINGS) # default configuration self.config.read(SETTINGS) # default configuration
self.config.read(filenames) self.config.read(filenames)
def parse_graphics(self): def parse_output(self):
"""Parse graphics configurations.""" """Parse graphics and sound configurations."""
self.size = (self.config.getint('Graphics', 'Screen width'), self.size = (self.config.getint('Graphics', 'Screen width'),
self.config.getint('Graphics', 'Screen height')) self.config.getint('Graphics', 'Screen height'))
self.opengl = self.config.getboolean('Graphics', 'OpenGL') self.opengl = self.config.getboolean('Graphics', 'OpenGL')
self.max_fps = self.config.getint('Graphics', 'Maximum FPS') self.max_fps = self.config.getint('Graphics', 'Maximum FPS')
self.muted = self.config.getboolean('Sound', 'Muted')
self.musicvol = self.config.getfloat('Sound', 'Music volume')
def parse_control(self): def parse_control(self):
"""Parse control configurations.""" """Parse control configurations."""
@ -84,27 +87,31 @@ class ConfigReader:
def read_args(self, arguments): def read_args(self, arguments):
"""Read and parse a ArgumentParser.Namespace.""" """Read and parse a ArgumentParser.Namespace."""
if arguments.size is not None: self.size = arguments.size for option in 'size', 'opengl', 'max_fps', 'muted', 'musicvol':
if arguments.opengl is not None: self.opengl = arguments.opengl value = getattr(arguments, option)
if arguments.max_fps is not None: self.max_fps = arguments.max_fps if value is not None: setattr(self, option, value)
class Game: class Game:
"""Object handling main loop and IO.""" """Object handling main loop and IO."""
def __init__(self, size, scrtype, max_fps, key, mouse): def __init__(self, size, scrtype, max_fps, muted, musicvol, key, mouse):
pygame.mixer.pre_init(frequency=44100) pygame.mixer.pre_init(frequency=44100)
pygame.init() pygame.init()
pygame.mixer.music.load(MUSIC) if muted:
pygame.mixer.music.play(-1) pygame.mixer.quit()
else:
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(musicvol)
pygame.mixer.music.play(-1)
pygame.display.set_icon(ICON) pygame.display.set_icon(ICON)
pygame.fastevent.init() pygame.fastevent.init()
self.clock = Clock()
# self.fps is a float to make sure floordiv won't be used in Python 2 # self.fps is a float to make sure floordiv won't be used in Python 2
self.max_fps, self.fps = max_fps, float(max_fps) self.max_fps, self.fps = max_fps, float(max_fps)
self.musicvol = musicvol
self.key, self.mouse = key, mouse self.key, self.mouse = key, mouse
self.maze = Maze(max_fps, size, scrtype) self.maze = Maze(max_fps, size, scrtype)
self.hero = self.maze.hero self.hero = self.maze.hero
self.paused = False self.clock, self.paused = Clock(), False
def __enter__(self): return self def __enter__(self): return self
@ -145,6 +152,14 @@ class Game:
self.maze.__init__(self.fps) self.maze.__init__(self.fps)
elif event.key == self.key['pause'] and not self.hero.dead: elif event.key == self.key['pause'] and not self.hero.dead:
self.paused ^= True self.paused ^= True
elif event.key == self.key['mute']:
if pygame.mixer.get_init() is None:
pygame.mixer.init(frequency=44100)
pygame.mixer.music.load(MUSIC)
pygame.mixer.music.set_volume(self.musicvol)
pygame.mixer.music.play(-1)
else:
pygame.mixer.quit()
if not self.hero.dead: if not self.hero.dead:
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
@ -181,7 +196,7 @@ def main():
parents.append(dirs.user_config_dir) parents.append(dirs.user_config_dir)
filenames = [join(parent, 'settings.ini') for parent in parents] filenames = [join(parent, 'settings.ini') for parent in parents]
config = ConfigReader(filenames) config = ConfigReader(filenames)
config.parse_graphics() config.parse_output()
# Parse command-line arguments # Parse command-line arguments
parser = ArgumentParser(formatter_class=RawTextHelpFormatter) parser = ArgumentParser(formatter_class=RawTextHelpFormatter)
@ -206,6 +221,14 @@ def main():
parser.add_argument( parser.add_argument(
'-f', '--max-fps', type=int, metavar='FPS', '-f', '--max-fps', type=int, metavar='FPS',
help='the desired maximum FPS (fallback: {})'.format(config.max_fps)) help='the desired maximum FPS (fallback: {})'.format(config.max_fps))
parser.add_argument(
'--mute', '-m', action='store_true', default=None,
help='mute all sounds (fallback: {})'.format(config.muted))
parser.add_argument('--unmute', action='store_false', dest='muted',
help='unmute sound')
parser.add_argument(
'--music-volume', type=float, metavar='VOL', dest='musicvol',
help='between 0.0 and 1.0 (fallback: {})'.format(config.musicvol))
args = parser.parse_args() args = parser.parse_args()
if args.defaultcfg is not None: if args.defaultcfg is not None:
with open(SETTINGS) as settings: args.defaultcfg.write(settings.read()) with open(SETTINGS) as settings: args.defaultcfg.write(settings.read())
@ -215,10 +238,11 @@ def main():
# Manipulate config # Manipulate config
if args.config: config.config.read(args.config) if args.config: config.config.read(args.config)
config.read_args(args) config.read_args(args)
config.parse_graphics() config.parse_output()
config.parse_control() config.parse_control()
# Main loop # Main loop
scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE scrtype = (config.opengl and DOUBLEBUF|OPENGL) | RESIZABLE
with Game(config.size, scrtype, config.max_fps, config.key, config.mouse) as game: with Game(config.size, scrtype, config.max_fps, config.muted,
config.musicvol, config.key, config.mouse) as game:
while game.loop(): pass while game.loop(): pass

View File

@ -25,7 +25,6 @@ from random import choice, getrandbits, uniform
import pygame import pygame
from pygame import RESIZABLE from pygame import RESIZABLE
from pygame.mixer import Sound
from pygame.time import get_ticks from pygame.time import get_ticks
from .characters import Hero, new_enemy from .characters import Hero, new_enemy
@ -76,9 +75,8 @@ class Maze:
next_move (int): the tick that the hero gets mobilized next_move (int): the tick that the hero gets mobilized
next_slashfx (int): the tick to play next slash effect of the hero next_slashfx (int): the tick to play next slash effect of the hero
slashd (float): minimum distance for slashes to be effective slashd (float): minimum distance for slashes to be effective
sfx_slash (Sound): sound effect indicating an enemy get slashed sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy
sfx_shot (Sound): sound effect indicating an enemy get shot sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose
sfx_lose (Sound): sound effect to be played when you lose
""" """
def __init__(self, fps, size=None, scrtype=None): def __init__(self, fps, size=None, scrtype=None):
self.fps = fps self.fps = fps
@ -109,10 +107,9 @@ class Maze:
self.next_move = self.next_slashfx = 0 self.next_move = self.next_slashfx = 0
self.slashd = self.hero.R + self.distance/SQRT2 self.slashd = self.hero.R + self.distance/SQRT2
self.sfx_spawn = Sound(SFX_SPAWN) self.sfx_spawn = SFX_SPAWN
self.sfx_slash = Sound(SFX_SLASH_ENEMY) self.sfx_slash = SFX_SLASH_ENEMY
self.sfx_shot = Sound(SFX_SHOT_ENEMY) self.sfx_lose = SFX_LOSE
self.sfx_lose = Sound(SFX_LOSE)
def add_enemy(self): def add_enemy(self):
"""Add enough enemies.""" """Add enough enemies."""
@ -140,13 +137,21 @@ class Maze:
def draw(self): def draw(self):
"""Draw the maze.""" """Draw the maze."""
self.surface.fill(BG_COLOR) self.surface.fill(BG_COLOR)
if get_ticks() < self.next_move: return if get_ticks() >= self.next_move:
for i in self.rangex: for i in self.rangex:
for j in self.rangey: for j in self.rangey:
if self.map[i][j] != WALL: continue if self.map[i][j] != WALL: continue
x, y = self.get_pos(i, j) x, y = self.get_pos(i, j)
square = regpoly(4, self.distance / SQRT2, pi / 4, x, y) square = regpoly(4, self.distance / SQRT2, pi / 4, x, y)
fill_aapolygon(self.surface, square, FG_COLOR) fill_aapolygon(self.surface, square, FG_COLOR)
for enemy in self.enemies: enemy.draw()
self.hero.draw()
bullet_radius = self.distance / 4
for bullet in self.bullets: bullet.draw(bullet_radius)
pygame.display.flip()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
def rotate(self): def rotate(self):
"""Rotate the maze if needed.""" """Rotate the maze if needed."""
@ -266,7 +271,7 @@ class Maze:
self.score += enemy.wound self.score += enemy.wound
enemy.die() enemy.die()
self.enemies.pop(j) self.enemies.pop(j)
play(self.sfx_shot, wound, bullet.angle) play(bullet.sfx_hit, wound, bullet.angle)
fallen.append(i) fallen.append(i)
break break
elif bullet.get_distance(self.x, self.y) < self.distance: elif bullet.get_distance(self.x, self.y) < self.distance:
@ -310,15 +315,12 @@ class Maze:
for enemy in self.enemies: enemy.wake() for enemy in self.enemies: enemy.wake()
for bullet in self.bullets: bullet.place(dx, dy) for bullet in self.bullets: bullet.place(dx, dy)
self.draw()
for enemy in self.enemies: enemy.update() for enemy in self.enemies: enemy.update()
if not self.hero.dead: if not self.hero.dead:
self.hero.update(fps) self.hero.update(fps)
self.slash() self.slash()
self.track_bullets() self.track_bullets()
pygame.display.flip() self.draw()
pygame.display.set_caption('Brutal Maze - Score: {}'.format(
int(self.score - INIT_SCORE)))
def resize(self, size): def resize(self, size):
"""Resize the maze.""" """Resize the maze."""

View File

@ -76,6 +76,7 @@ def choices(d):
def play(sound, volume=1.0, angle=None): def play(sound, volume=1.0, angle=None):
"""Play a pygame.mixer.Sound at the given volume.""" """Play a pygame.mixer.Sound at the given volume."""
if pygame.mixer.get_init() is None: return
if pygame.mixer.find_channel() is None: if pygame.mixer.find_channel() is None:
pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1) pygame.mixer.set_num_channels(pygame.mixer.get_num_channels() + 1)

View File

@ -6,6 +6,11 @@ OpenGL: no
# FPS should not be greater than refresh rate. # FPS should not be greater than refresh rate.
Maximum FPS: 60 Maximum FPS: 60
[Sound]
Muted: no
# Volume must be between 0.0 and 1.0
Music volume: 1.0
[Control] [Control]
# Input values should be either from Mouse1 to Mouse3 or a keyboard key # Input values should be either from Mouse1 to Mouse3 or a keyboard key
# and they are case-insensitively read. # and they are case-insensitively read.
@ -13,7 +18,8 @@ Maximum FPS: 60
# http://www.pygame.org/docs/ref/key.html # http://www.pygame.org/docs/ref/key.html
# Key combinations are not supported. # Key combinations are not supported.
New game: F2 New game: F2
Pause: p Toggle pause: p
Toggle mute: m
Move left: Left Move left: Left
Move right: Right Move right: Right
Move up: Up Move up: Up

View File

@ -22,7 +22,6 @@ __doc__ = 'brutalmaze module for weapon classes'
from math import cos, sin from math import cos, sin
from pygame.time import get_ticks from pygame.time import get_ticks
from pygame.mixer import Sound
from .constants import * from .constants import *
from .misc import regpoly, fill_aapolygon from .misc import regpoly, fill_aapolygon
@ -37,25 +36,28 @@ class Bullet:
angle (float): angle of the direction the bullet pointing (in radians) angle (float): angle of the direction the bullet pointing (in radians)
color (str): bullet's color name color (str): bullet's color name
fall_time (int): the tick that the bullet will fall down fall_time (int): the tick that the bullet will fall down
sfx_hit (Sound): sound effect indicating the bullet hits the target sfx_hit (pygame.mixer.Sound): sound effect indicating target was hit
sfx_missed (Sound): sound effect indicating the bullet hits the target sfx_missed (pygame.mixer.Sound): sound effect indicating a miss shot
""" """
def __init__(self, surface, x, y, angle, color): def __init__(self, surface, x, y, angle, color):
self.surface = surface self.surface = surface
self.x, self.y, self.angle, self.color = x, y, angle, color self.x, self.y, self.angle, self.color = x, y, angle, color
self.fall_time = get_ticks() + BULLET_LIFETIME self.fall_time = get_ticks() + BULLET_LIFETIME
# Sound effects of bullets shot by hero are stored in Maze to avoid if color == 'Aluminium':
# unnecessary duplication self.sfx_hit = SFX_SHOT_ENEMY
if color != 'Aluminium': else:
self.sfx_hit = Sound(SFX_SHOT_HERO) self.sfx_hit = SFX_SHOT_HERO
self.sfx_missed = Sound(SFX_MISSED) self.sfx_missed = SFX_MISSED
def update(self, fps, distance): def update(self, fps, distance):
"""Update the bullet.""" """Update the bullet."""
s = distance * BULLET_SPEED / fps s = distance * BULLET_SPEED / fps
self.x += s * cos(self.angle) self.x += s * cos(self.angle)
self.y += s * sin(self.angle) self.y += s * sin(self.angle)
pentagon = regpoly(5, distance // 4, self.angle, self.x, self.y)
def draw(self, radius):
"""Draw the bullet."""
pentagon = regpoly(5, radius, self.angle, self.x, self.y)
value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP) value = int((1-(self.fall_time-get_ticks())/BULLET_LIFETIME)*ENEMY_HP)
try: try:
fill_aapolygon(self.surface, pentagon, TANGO[self.color][value]) fill_aapolygon(self.surface, pentagon, TANGO[self.color][value])

2
wiki

@ -1 +1 @@
Subproject commit 3b014564eb185bb525a59378fd2d9ede65bf0d33 Subproject commit 34af1cf8b3e3ea8272d6793a794484a239794d50