Implement game recording
And fix various bugs on game data export. Somehow they remain undiscovered the last 5 months.
This commit is contained in:
parent
eb23230acb
commit
834fa33ec0
|
@ -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."""
|
||||
|
|
|
@ -84,3 +84,5 @@ HERO_HP = 5
|
|||
MIN_BEAT = 526
|
||||
BG_COLOR = TANGO['Aluminium'][-1]
|
||||
FG_COLOR = TANGO['Aluminium'][0]
|
||||
|
||||
JSON_SEPARATORS = ',', ':'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue