diff --git a/brutalmaze/characters.py b/brutalmaze/characters.py index e3e6db2..51a1eb7 100644 --- a/brutalmaze/characters.py +++ b/brutalmaze/characters.py @@ -286,15 +286,17 @@ class Enemy: x, y = self.get_pos() return atan2(y - self.maze.y, x - self.maze.x) - def get_color(self): """Return current color of the enemy.""" return TANGO[self.color][int(self.wound)] + def isunnoticeable(self): + """Return whether the enemy can be noticed.""" + return not self.awake or self.wound >= ENEMY_HP def draw(self): """Draw the enemy.""" - if not self.awake: return + if self.isunnoticeable(): return radius = self.maze.distance / SQRT2 square = regpoly(4, radius, self.angle, *self.get_pos()) fill_aapolygon(self.maze.surface, square, self.get_color()) @@ -344,10 +346,11 @@ class Chameleon(Enemy): if Enemy.wake(self) is True: self.visible = 1000.0 / ENEMY_SPEED - def draw(self): - """Draw the Chameleon.""" - if not self.awake or self.visible > 0 or self.spin_queue: - Enemy.draw(self) + def isunnoticeable(self): + """Return whether the enemy can be noticed.""" + return (Enemy.isunnoticeable(self) + or self.visible <= 0 and not self.spin_queue + and self.maze.next_move <= 0) def update(self): """Update the Chameleon.""" diff --git a/brutalmaze/constants.py b/brutalmaze/constants.py index 82e76c9..7d7ba00 100644 --- a/brutalmaze/constants.py +++ b/brutalmaze/constants.py @@ -84,3 +84,5 @@ HERO_HP = 5 MIN_BEAT = 526 BG_COLOR = TANGO['Aluminium'][-1] FG_COLOR = TANGO['Aluminium'][0] + +JSON_SEPARATORS = ',', ':' diff --git a/brutalmaze/maze.py b/brutalmaze/maze.py index 6a5f139..28dc912 100644 --- a/brutalmaze/maze.py +++ b/brutalmaze/maze.py @@ -20,7 +20,9 @@ __doc__ = 'Brutal Maze module for the maze class' from collections import defaultdict, deque +import json from math import pi, log +from os import path from random import choice, sample, uniform import pygame @@ -30,9 +32,10 @@ from .constants import ( EMPTY, WALL, HERO, ENEMY, ROAD_WIDTH, WALL_WIDTH, CELL_WIDTH, CELL_NODES, MAZE_SIZE, MIDDLE, INIT_SCORE, ENEMIES, MINW, MAXW, SQRT2, SFX_SPAWN, SFX_SLASH_ENEMY, SFX_LOSE, ADJACENTS, TANGO_VALUES, BG_COLOR, FG_COLOR, - HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED, BULLET_LIFETIME) -from .misc import round2, sign, around, regpoly, fill_aapolygon, play -from .weapons import Bullet + COLORS, HERO_HP, ENEMY_HP, ATTACK_SPEED, MAX_WOUND, HERO_SPEED, + BULLET_LIFETIME, JSON_SEPARATORS) +from .misc import ( + round2, sign, deg, around, regpoly, fill_aapolygon, play, json_rec) class Maze: @@ -50,7 +53,7 @@ class Maze: map (deque of deque): map of grids representing objects on the maze vx, vy (float): velocity of the maze movement (in pixels per frame) rotatex, rotatey (int): grids rotated - bullets (list of Bullet): flying bullets + bullets (list of .weapons.Bullet): flying bullets enemy_weights (dict): probabilities of enemies to be created enemies (list of Enemy): alive enemies hero (Hero): the hero @@ -62,14 +65,21 @@ class Maze: slashd (float): minimum distance for slashes to be effective sfx_slash (pygame.mixer.Sound): sound effect of slashed enemy sfx_lose (pygame.mixer.Sound): sound effect to be played when you lose + export (list of defaultdict): records of game states + export_dir (str): directory containing records of game states + export_rate (float): milliseconds per snapshot + next_export (float): time until next snapshot (in ms) """ - def __init__(self, fps, size, headless): + def __init__(self, fps, size, headless, export_dir, export_rate): self.fps = fps self.w, self.h = size if headless: self.surface = None else: self.surface = pygame.display.set_mode(size, pygame.RESIZABLE) + self.export_dir = path.abspath(export_dir) + self.next_export = self.export_rate = export_rate + self.export = [] self.distance = (self.w * self.h / 416) ** 0.5 self.x, self.y = self.w // 2, self.h // 2 @@ -336,6 +346,46 @@ class Maze: return 0.0 return vx or vy + def expos(self, x, y): + """Return position of the given coordinates in rounded percent.""" + cx = len(self.rangex)*50 + (x - self.centerx)/self.distance*100 + cy = len(self.rangey)*50 + (y - self.centery)/self.distance*100 + return round2(cx), round2(cy) + + def update_export(self, forced=False): + """Update the maze's data export and return the last record.""" + if self.next_export > 0 and not forced or self.hero.dead: return + export = defaultdict(list) + export['s'] = self.get_score() + + if self.next_move <= 0: + for y in self.rangey: + export['m'].append(''.join( + COLORS[self.get_color()] if self.map[x][y] == WALL else '0' + for x in self.rangex)) + + x, y = self.expos(self.x, self.y) + export['h'] = [ + COLORS[self.hero.get_color()], x, y, deg(self.hero.angle), + int(self.hero.next_strike <= 0), int(self.hero.next_heal <= 0)] + + for enemy in self.enemies: + if enemy.isunnoticeable(): continue + x, y = self.expos(*enemy.get_pos()) + color, angle = COLORS[enemy.get_color()], deg(enemy.angle) + export['e'].append([color, x, y, angle]) + + for bullet in self.bullets: + x, y = self.expos(bullet.x, bullet.y) + color, angle = COLORS[bullet.get_color()], deg(bullet.angle) + if color != '0': export['b'].append([color, x, y, angle]) + + if self.next_export <= 0: + export['t'] = round2(self.export_rate - self.next_export) + self.export.append(export) + self.next_export = self.export_rate + return export + def update(self, fps): """Update the maze.""" self.fps = fps @@ -347,6 +397,7 @@ class Maze: self.next_move -= 1000.0 / fps self.glitch -= 1000.0 / fps self.next_slashfx -= 1000.0 / fps + self.next_export -= 1000.0 / fps self.rotate() if self.vx or self.vy or self.hero.firing or self.hero.slashing: @@ -358,7 +409,8 @@ class Maze: if not self.hero.dead: self.hero.update(fps) self.slash() - if self.hero.wound > HERO_HP: self.lose() + if self.hero.wound >= HERO_HP: self.lose() + self.update_export() def resize(self, size): """Resize the maze.""" @@ -419,19 +471,27 @@ class Maze: """Return if the hero is moving faster than HERO_SPEED.""" return (self.vx**2+self.vy**2)**0.5*self.fps > HERO_SPEED*self.distance + def dump_records(self): + """Dump JSON records.""" + if self.export_dir: + with open(json_rec(self.export_dir), 'w') as f: + json.dump(self.export, f, separators=JSON_SEPARATORS) + def lose(self): """Handle loses.""" self.hero.dead = True + self.hero.wound = HERO_HP self.hero.slashing = self.hero.firing = False self.destx = self.desty = MIDDLE self.stepx = self.stepy = 0 self.vx = self.vy = 0.0 play(self.sfx_lose) + self.dump_records() def reinit(self): """Open new game.""" self.centerx, self.centery = self.w / 2.0, self.h / 2.0 - self.score = INIT_SCORE + self.score, self.export = INIT_SCORE, [] self.map = deque(deque(EMPTY for _ in range(MAZE_SIZE * CELL_WIDTH)) for _ in range(MAZE_SIZE * CELL_WIDTH)) for x in range(MAZE_SIZE): diff --git a/brutalmaze/misc.py b/brutalmaze/misc.py index d26edf4..008d7cf 100644 --- a/brutalmaze/misc.py +++ b/brutalmaze/misc.py @@ -19,8 +19,10 @@ __doc__ = 'Brutal Maze module for miscellaneous functions' +from datetime import datetime from itertools import chain from math import degrees, cos, sin, pi +from os import path from random import shuffle, uniform import pygame @@ -40,9 +42,9 @@ def randsign(): def regpoly(n, R, r, x, y): - """Return the pointlist of the regular polygon with n sides, - circumradius of R, the center point I(x, y) and one point A make the - vector IA with angle r (in radians). + """Return pointlist of a regular n-gon with circumradius of R, + center point I(x, y) and corner A that angle of vector IA is r + (in radians). """ r %= pi * 2 angles = [r + pi*2*side/n for side in range(n)] @@ -50,7 +52,7 @@ def regpoly(n, R, r, x, y): def fill_aapolygon(surface, points, color): - """Draw a filled polygon with anti aliased edges onto a surface.""" + """Draw a filled polygon with anti-aliased edges onto a surface.""" aapolygon(surface, points, color) filled_polygon(surface, points, color) @@ -72,6 +74,15 @@ def cosin(x): return cos(x) + sin(x) +def join(iterable, sep=' ', end='\n'): + """Return a string which is the concatenation of string + representations of objects in the iterable, separated by sep. + + end is appended to the resulting string. + """ + return sep.join(map(str, iterable)) + end + + def around(x, y): """Return grids around the given one in random order.""" a = [(x + i, y + j) for i, j in ADJACENTS] @@ -111,3 +122,11 @@ def play(sound, volume=1.0, angle=None): volumes[i] = 1.0 sound.set_volume(1.0) channel.set_volume(*volumes) + + +def json_rec(directory): + """Return path to JSON file to be created inside the given directory + based on current time local to timezone in ISO 8601 format. + """ + return path.join( + directory, '{}.json'.format(datetime.now().isoformat()[:19])) diff --git a/brutalmaze/settings.ini b/brutalmaze/settings.ini index b426aab..75bfb72 100644 --- a/brutalmaze/settings.ini +++ b/brutalmaze/settings.ini @@ -6,9 +6,9 @@ Maximum FPS: 60 [Sound] Muted: no -# Volume must be between 0.0 and 1.0 +# Volume must be between 0.0 and 1.0. Music volume: 1.0 -# Use space music background, which sounds cold and creepy +# Use space music background, which sounds cold and creepy. Space theme: no [Control] @@ -29,6 +29,12 @@ Auto move: Mouse3 Long-range attack: Mouse1 Close-range attack: Space +[Record] +# Directory to write record of game states, leave blank to disable. +Directory: . +# Number of snapshots per second. This is preferably from 3 to 60. +Frequency: 30 + [Server] # Enabling remote control will disable control via keyboard and mouse. Enable: no