From ba2aaeb1f1255737566ebcc9061a52f01a73bcce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= Date: Sun, 5 Aug 2018 18:05:07 +0700 Subject: [PATCH] Update socket output --- README.rst | 1 + brutalmaze/game.py | 102 +++++++++++++++++----------------------- brutalmaze/settings.ini | 2 +- screenshot.png | Bin 10078 -> 7507 bytes setup.py | 2 +- wiki | 2 +- 6 files changed, 47 insertions(+), 62 deletions(-) diff --git a/README.rst b/README.rst index 58b6e77..0d5cab0 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,7 @@ Brutal Maze has a few notable features: * Somewhat a realistic physic and logic system. * Resizable game window in-game. * Easily customizable via INI file format. +* Recordable in JSON (some kind of silent screencast). * Remote control through TCP/IP socket (can be used in AI researching). Installation diff --git a/brutalmaze/game.py b/brutalmaze/game.py index df5baaf..083125c 100644 --- a/brutalmaze/game.py +++ b/brutalmaze/game.py @@ -17,17 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with Brutal Maze. If not, see . -__version__ = '0.8.1' +__version__ = '0.8.20' import re from argparse import ArgumentParser, FileType, RawTextHelpFormatter -from collections import deque try: # Python 3 from configparser import ConfigParser except ImportError: # Python 2 from ConfigParser import ConfigParser from math import atan2, radians, pi -from os.path import join, pathsep +from os.path import join as pathjoin, pathsep from socket import socket, SOL_SOCKET, SO_REUSEADDR from sys import stdout from threading import Thread @@ -37,10 +36,9 @@ from pygame import KEYDOWN, QUIT, VIDEORESIZE from pygame.time import Clock, get_ticks from appdirs import AppDirs -from .constants import ( - SETTINGS, ICON, MUSIC, NOISE, HERO_SPEED, COLORS, MIDDLE, WALL) +from .constants import SETTINGS, ICON, MUSIC, NOISE, HERO_SPEED, MIDDLE from .maze import Maze -from .misc import deg, round2, sign +from .misc import sign, deg, join class ConfigReader: @@ -73,6 +71,8 @@ class ConfigReader: self.muted = self.config.getboolean('Sound', 'Muted') self.musicvol = self.config.getfloat('Sound', 'Music volume') self.space = self.config.getboolean('Sound', 'Space theme') + self.export_dir = self.config.get('Record', 'Directory') + self.export_rate = self.config.getint('Record', 'Frequency') self.server = self.config.getboolean('Server', 'Enable') self.host = self.config.get('Server', 'Host') self.port = self.config.getint('Server', 'Port') @@ -98,8 +98,9 @@ class ConfigReader: def read_args(self, arguments): """Read and parse a ArgumentParser.Namespace.""" - for option in ('size', 'max_fps', 'muted', 'musicvol', 'space', - 'server', 'host', 'port', 'timeout', 'headless'): + for option in ( + 'size', 'max_fps', 'muted', 'musicvol', 'space', 'export_dir', + 'export_rate', 'server', 'host', 'port', 'timeout', 'headless'): value = getattr(arguments, option) if value is not None: setattr(self, option, value) @@ -134,51 +135,20 @@ class Game: self.max_fps, self.fps = config.max_fps, float(config.max_fps) self.musicvol = config.musicvol self.key, self.mouse = config.key, config.mouse - self.maze = Maze(config.max_fps, config.size, config.headless) + self.maze = Maze(config.max_fps, config.size, config.headless, + config.export_dir, 1000.0 / config.export_rate) self.hero = self.maze.hero self.clock, self.paused = Clock(), False def __enter__(self): return self - def expos(self, x, y): - """Return position of the given coordinates in rounded percent.""" - cx = (x+self.maze.x-self.maze.centerx) / self.maze.distance * 100 - cy = (y+self.maze.y-self.maze.centery) / self.maze.distance * 100 - return round2(cx), round2(cy) - - def export(self): - """Export maze data to a bytes object.""" - maze, hero, = self.maze, self.hero - lines = deque(['{0} {4} {5} {1} {2:d} {3:d}'.format( - COLORS[hero.get_color()], deg(self.hero.angle), - hero.next_strike <= 0, hero.next_heal <= 0, - *self.expos(maze.x, maze.y))]) - - walls = [[1 if maze.map[x][y] == WALL else 0 for x in maze.rangex] - for y in maze.rangey] if maze.next_move <= 0 else [] - ne = nb = 0 - - for enemy in maze.enemies: - # Check Chameleons - if getattr(enemy, 'visible', 1) <= 0 and maze.next_move <= 0: - continue - lines.append('{0} {2} {3} {1:.0f}'.format( - COLORS[enemy.get_color()], deg(enemy.angle), - *self.expos(*enemy.get_pos()))) - ne += 1 - - for bullet in maze.bullets: - x, y = self.expos(bullet.x, bullet.y) - color, angle = COLORS[bullet.get_color()], deg(bullet.angle) - if color != '0': - lines.append('{} {} {} {:.0f}'.format(color, x, y, angle)) - nb += 1 - - if walls: lines.appendleft('\n'.join(''.join(str(cell) for cell in row) - for row in walls)) - lines.appendleft('{} {} {} {}'.format(len(walls), ne, nb, - maze.get_score())) - return '\n'.join(lines).encode() + def export_txt(self): + """Export maze data to string.""" + export = self.maze.update_export(forced=True) + return '{} {} {} {}\n{}{}{}{}'.format( + len(export['m']), len(export['e']), len(export['b']), export['s'], + ''.join(row + '\n' for row in export['m']), join(export['h']), + ''.join(map(join, export['e'])), ''.join(map(join, export['b']))) def update(self): """Draw and handle meta events on Pygame window. @@ -191,12 +161,8 @@ class Game: return False elif event.type == VIDEORESIZE: self.maze.resize((event.w, event.h)) - elif event.type == KEYDOWN and not self.server: - if event.key == self.key['new']: - self.maze.reinit() - elif event.key == self.key['pause'] and not self.hero.dead: - self.paused ^= True - elif event.key == self.key['mute']: + elif event.type == KEYDOWN: + if event.key == self.key['mute']: if pygame.mixer.get_init() is None: pygame.mixer.init(frequency=44100) pygame.mixer.music.load(MUSIC) @@ -204,6 +170,11 @@ class Game: pygame.mixer.music.play(-1) else: pygame.mixer.quit() + elif not self.server: + if event.key == self.key['new']: + self.maze.reinit() + elif event.key == self.key['pause'] and not self.hero.dead: + self.paused ^= True # Compare current FPS with the average of the last 10 frames new_fps = self.clock.get_fps() @@ -269,7 +240,8 @@ class Game: if self.hero.dead: connection.send('0000000'.encode()) break - data = self.export() + data = self.export_txt().encode() + alpha = deg(self.hero.angle) connection.send('{:07}'.format(len(data)).encode()) connection.send(data) try: @@ -282,7 +254,9 @@ class Game: except ValueError: # invalid input break y, x = (i - 1 for i in divmod(move, 3)) - self.sockinp = x, y, radians(angle), attack & 1, attack >> 1 + # Time is the essence. + angle = self.hero.angle if angle == alpha else radians(angle) + self.sockinp = x, y, angle, attack & 1, attack >> 1 clock.tick(self.fps) self.sockinp = 0, 0, -pi * 3 / 4, 0, 0 new_time = get_ticks() @@ -329,6 +303,7 @@ class Game: def __exit__(self, exc_type, exc_value, traceback): if self.server is not None: self.server.close() + if not self.hero.dead: self.maze.dump_records() pygame.quit() @@ -338,7 +313,7 @@ def main(): dirs = AppDirs(appname='brutalmaze', appauthor=False, multipath=True) parents = dirs.site_config_dir.split(pathsep) parents.append(dirs.user_config_dir) - filenames = [join(parent, 'settings.ini') for parent in parents] + filenames = [pathjoin(parent, 'settings.ini') for parent in parents] config = ConfigReader(filenames) config.parse() @@ -369,10 +344,19 @@ def main(): parser.add_argument( '--music-volume', type=float, metavar='VOL', dest='musicvol', help='between 0.0 and 1.0 (fallback: {})'.format(config.musicvol)) - parser.add_argument('--space-music', action='store_true', dest='space', - default=None, help='use space music background') + parser.add_argument( + '--space-music', action='store_true', dest='space', default=None, + help='use space music background (fallback: {})'.format(config.space)) parser.add_argument('--default-music', action='store_false', dest='space', help='use default music background') + parser.add_argument( + '--record-dir', metavar='DIR', dest='export_dir', + help='directory to write game records (fallback: {})'.format( + config.export_dir or '*disabled*')) + parser.add_argument( + '--record-rate', metavar='SPF', dest='export_rate', + help='snapshots of game state per second (fallback: {})'.format( + config.export_rate)) parser.add_argument( '--server', action='store_true', default=None, help='enable server (fallback: {})'.format(config.server)) diff --git a/brutalmaze/settings.ini b/brutalmaze/settings.ini index 75bfb72..e77eb62 100644 --- a/brutalmaze/settings.ini +++ b/brutalmaze/settings.ini @@ -31,7 +31,7 @@ Close-range attack: Space [Record] # Directory to write record of game states, leave blank to disable. -Directory: . +Directory: # Number of snapshots per second. This is preferably from 3 to 60. Frequency: 30 diff --git a/screenshot.png b/screenshot.png index 9c2a89331840f2e66075563d602da9687a585eca..48d5f88103b0607c6d0830f7657dfb5983c8c6db 100644 GIT binary patch literal 7507 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 DFy{w~ literal 10078 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