From 23462b9ad5f46f8dd323aed66e557235802e3c98 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 30 Jan 2019 15:00:51 +0100 Subject: [PATCH] Lots of refactoring to enable SAVE command. --- client-curses.py | 8 +-- game_common.py | 10 ++-- parser.py | 18 +++---- server.py | 2 +- server_/game.py | 128 ++++++++++++++++++++++++++++++++++------------- server_/io.py | 39 ++++++++------- 6 files changed, 135 insertions(+), 70 deletions(-) diff --git a/client-curses.py b/client-curses.py index 9a8178d..ea5dcf3 100755 --- a/client-curses.py +++ b/client-curses.py @@ -128,12 +128,12 @@ class Game(game_common.CommonCommandsMixin): self.do_quit = True return try: - command = self.parser.parse(msg) + command, args = self.parser.parse(msg) if command is None: self.log('UNHANDLED INPUT: ' + msg) self.to_update['log'] = True else: - command() + command(*args) except ArgError as e: self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e)) self.to_update['log'] = True @@ -161,13 +161,13 @@ class Game(game_common.CommonCommandsMixin): pass cmd_TURN_FINISHED.argtypes = 'int:nonneg' - def cmd_NEW_TURN(self, n): + def cmd_TURN(self, n): """Set self.turn to n, empty self.things.""" self.world.turn = n self.world.things = [] self.to_update['turn'] = False self.to_update['map'] = False - cmd_NEW_TURN.argtypes = 'int:nonneg' + cmd_TURN.argtypes = 'int:nonneg' def cmd_VISIBLE_MAP_LINE(self, y, terrain_line): self.world.map_.set_line(y, terrain_line) diff --git a/game_common.py b/game_common.py index 3c17bd1..c0fb98b 100644 --- a/game_common.py +++ b/game_common.py @@ -54,13 +54,15 @@ class World: self.turn = 0 self.things = [] - def get_thing(self, id_): + def get_thing(self, id_, create_unfound=True): for thing in self.things: if id_ == thing.id_: return thing - t = self.Thing(self, id_) - self.things += [t] - return t + if create_unfound: + t = self.Thing(self, id_) + self.things += [t] + return t + return None def new_map(self, geometry, yx): map_type = self.game.map_manager.get_map_class(geometry) diff --git a/parser.py b/parser.py index 2292f88..03b688f 100644 --- a/parser.py +++ b/parser.py @@ -1,5 +1,4 @@ import unittest -from functools import partial class ArgError(Exception): @@ -45,25 +44,25 @@ class Parser: return tokens def parse(self, msg): - """Parse msg as call to method, return method with arguments. + """Parse msg as call to function, return function with args tuple. - Respects method signatures defined in methods' .argtypes attributes. + Respects function signature defined in function's .argtypes attribute. """ tokens = self.tokenize(msg) if len(tokens) == 0: - return None - method, argtypes = self.game.get_command_signature(tokens[0]) - if method is None: - return None + return None, () + func, argtypes = self.game.get_command_signature(tokens[0]) + if func is None: + return None, () if len(argtypes) == 0: if len(tokens) > 1: raise ArgError('Command expects no argument(s).') - return method + return func, () if len(tokens) == 1: raise ArgError('Command expects argument(s).') args_candidates = tokens[1:] args = self.argsparse(argtypes, args_candidates) - return partial(method, *args) + return func, args def parse_yx_tuple(self, yx_string, range_): """Parse yx_string as yx_tuple:nonneg argtype, return result. @@ -167,6 +166,7 @@ class TestParser(unittest.TestCase): self.assertEqual(p.parse('x'), None) def test_argsparse(self): + from functools import partial p = Parser() assertErr = partial(self.assertRaises, ArgError, p.argsparse) assertErr('', ['foo']) diff --git a/server.py b/server.py index cdeb27d..cb9150b 100755 --- a/server.py +++ b/server.py @@ -17,7 +17,7 @@ if os.path.exists(game_file_name): lines = f.readlines() for i in range(len(lines)): line = lines[i] - print("FILE INPUT LINE %s: %s" % (i, line), end='') + print("FILE INPUT LINE %5s: %s" % (i, line), end='') game.io.handle_input(line, store=False) else: game.io.handle_input('GEN_WORLD Hex Y:16,X:16 bar') diff --git a/server_/game.py b/server_/game.py index 958bdc8..0974f1b 100644 --- a/server_/game.py +++ b/server_/game.py @@ -2,6 +2,7 @@ import sys sys.path.append('../') import game_common import server_.map_ +import server_.io from parser import ArgError @@ -85,13 +86,22 @@ class Task: raise GameError(str(self.thing.id_) + ' would move into other thing') + def get_args_string(self): + stringed_args = [] + for arg in self.args: + if type(arg) == 'string': + stringed_args += [server_.io.quote(arg)] + else: + raise GameError('stringifying arg type not implemented') + return ' '.join(stringed_args) + class Thing(game_common.Thing): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.task = Task(self, 'WAIT') - self.last_task_result = None + self._last_task_result = None self._stencil = None def task_WAIT(self): @@ -189,7 +199,7 @@ class Thing(game_common.Thing): self.task.check() except GameError as e: self.task = None - self.last_task_result = e + self._last_task_result = e if is_AI: try: self.decide_task() @@ -199,7 +209,7 @@ class Thing(game_common.Thing): self.task.todo -= 1 if self.task.todo <= 0: task = getattr(self, 'task_' + self.task.name) - self.last_task_result = task(*self.task.args) + self._last_task_result = task(*self.task.args) self.task = None if is_AI and self.task is None: try: @@ -241,7 +251,6 @@ def fib(n): class Game(game_common.CommonCommandsMixin): def __init__(self, game_file_name): - import server_.io self.map_manager = server_.map_.map_manager self.world = World(self) self.io = server_.io.GameIO(game_file_name, self) @@ -254,23 +263,19 @@ class Game(game_common.CommonCommandsMixin): def send_gamestate(self, connection_id=None): """Send out game state data relevant to clients.""" - def stringify_yx(tuple_): - """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x).""" - return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1]) - - self.io.send('NEW_TURN ' + str(self.world.turn)) + self.io.send('TURN ' + str(self.world.turn)) self.io.send('MAP ' + self.world.map_.geometry +\ - ' ' + stringify_yx(self.world.map_.size)) + ' ' + server_.io.stringify_yx(self.world.map_.size)) visible_map = self.world.get_player().get_visible_map() for y, line in visible_map.lines(): - self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, self.io.quote(line))) + self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, server_.io.quote(line))) visible_things = self.world.get_player().get_visible_things() for thing in visible_things: self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_)) self.io.send('THING_POS %s %s' % (thing.id_, - stringify_yx(thing.position))) + server_.io.stringify_yx(thing.position))) player = self.world.get_player() - self.io.send('PLAYER_POS %s' % (stringify_yx(player.position))) + self.io.send('PLAYER_POS %s' % (server_.io.stringify_yx(player.position))) self.io.send('GAME_STATE_COMPLETE') def proceed(self): @@ -281,8 +286,8 @@ class Game(game_common.CommonCommandsMixin): """ self.io.send('TURN_FINISHED ' + str(self.world.turn)) self.world.proceed_to_next_player_turn() - msg = str(self.world.get_player().last_task_result) - self.io.send('LAST_PLAYER_TASK_RESULT ' + self.io.quote(msg)) + msg = str(self.world.get_player()._last_task_result) + self.io.send('LAST_PLAYER_TASK_RESULT ' + server_.io.quote(msg)) self.send_gamestate() def cmd_FIB(self, numbers, connection_id): @@ -300,16 +305,18 @@ class Game(game_common.CommonCommandsMixin): def cmd_INC_P(self, connection_id): """Increment world.turn, send game turn data to everyone. - To simulate game processing waiting times, a one second delay between - TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive - calculations are started as pool processes that need to be finished - until a further INC finishes the turn. + To simulate game processing waiting times, a one second delay + between TURN_FINISHED and TURN occurs; after TURN, some + expensive calculations are started as pool processes that need + to be finished until a further INC finishes the turn. + + This is just a demo structure for how the game loop could work + when parallelized. One might imagine a two-step game turn, + with a non-action step determining actor tasks (the AI + determinations would take the place of the fib calculations + here), and an action step wherein these tasks are performed + (where now sleep(1) is). - This is just a demo structure for how the game loop could work when - parallelized. One might imagine a two-step game turn, with a non-action - step determining actor tasks (the AI determinations would take the - place of the fib calculations here), and an action step wherein these - tasks are performed (where now sleep(1) is). """ from time import sleep if self.pool_result is not None: @@ -360,17 +367,36 @@ class Game(game_common.CommonCommandsMixin): self.world.get_player().set_task(task_name, args) self.proceed() - method = None - argtypes = '' - task_prefix = 'TASK:' - if command_name[:len(task_prefix)] == task_prefix: - task_name = command_name[len(task_prefix):] - task_method_candidate = 'task_' + task_name - if hasattr(Thing, task_method_candidate): - method = partial(cmd_TASK_colon, task_name) - task_method = getattr(Thing, task_method_candidate) - if hasattr(task_method, 'argtypes'): - argtypes = task_method.argtypes + def cmd_SET_TASK_colon(task_name, thing_id, todo, *args): + t = self.world.get_thing(thing_id, False) + if t is None: + raiseArgError('No such Thing.') + t.task = Task(t, task_name, args) + t.task.todo = todo + + def task_prefixed(command_name, task_prefix, task_command, + argtypes_prefix=''): + method = None + argtypes = '' + if command_name[:len(task_prefix)] == task_prefix: + task_name = command_name[len(task_prefix):] + task_method_candidate = 'task_' + task_name + if hasattr(Thing, task_method_candidate): + method = partial(task_command, task_name) + task_method = getattr(Thing, task_method_candidate) + if hasattr(task_method, 'argtypes'): + argtypes = task_method.argtypes + if method is not None: + return method, argtypes_prefix + argtypes + return None, argtypes + + method, argtypes = task_prefixed(command_name, 'TASK:', cmd_TASK_colon) + if method: + return method, argtypes + method, argtypes = task_prefixed(command_name, 'SET_TASK:', + cmd_SET_TASK_colon, + 'int:nonneg int:nonneg') + if method: return method, argtypes method_candidate = 'cmd_' + command_name if hasattr(self, method_candidate): @@ -385,3 +411,35 @@ class Game(game_common.CommonCommandsMixin): elif string_option_type == 'direction': return self.world.map_.get_directions() return None + + def cmd_PLAYER_ID(self, id_): + # TODO: test whether valid thing ID + self.world.player_id = id_ + cmd_PLAYER_ID.argtypes = 'int:nonneg' + + def cmd_TURN(self, n): + self.world.turn = n + cmd_TURN.argtypes = 'int:nonneg' + + def cmd_SAVE(self): + + def write(f, msg): + f.write(msg + '\n') + + save_file_name = self.io.game_file_name + '.save' + with open(save_file_name, 'w') as f: + write(f, 'TURN %s' % self.world.turn) + write(f, 'MAP ' + self.world.map_.geometry + ' ' + server_.io.stringify_yx(self.world.map_.size)) + for y, line in self.world.map_.lines(): + write(f, 'TERRAIN_LINE %5s %s' % (y, server_.io.quote(line))) + for thing in self.world.things: + write(f, 'THING_TYPE %s %s' % (thing.id_, thing.type_)) + write(f, 'THING_POS %s %s' % (thing.id_, + server_.io.stringify_yx(thing.position))) + task = thing.task + if task is not None: + task_args = task.get_args_string() + write(f, 'SET_TASK:%s %s %s %s' % (task.name, thing.id_, + task.todo, task_args)) + write(f, 'PLAYER_ID %s' % self.world.player_id) + cmd_SAVE.dont_save = True diff --git a/server_/io.py b/server_/io.py index d2d67e9..2200037 100644 --- a/server_/io.py +++ b/server_/io.py @@ -159,21 +159,21 @@ class GameIO(): print(msg) try: - command = self.parser.parse(input_) + command, args = self.parser.parse(input_) if command is None: answer(connection_id, 'UNHANDLED_INPUT') else: if 'connection_id' in list(signature(command).parameters): - command(connection_id=connection_id) + command(*args, connection_id=connection_id) else: - command() - if store: + command(*args) + if store and not hasattr(command, 'dont_save'): with open(self.game_file_name, 'a') as f: f.write(input_ + '\n') except parser.ArgError as e: - answer(connection_id, 'ARGUMENT_ERROR ' + self.quote(str(e))) + answer(connection_id, 'ARGUMENT_ERROR ' + quote(str(e))) except server_.game.GameError as e: - answer(connection_id, 'GAME_ERROR ' + self.quote(str(e))) + answer(connection_id, 'GAME_ERROR ' + quote(str(e))) def send(self, msg, connection_id=None): """Send message msg to server's client(s) via self.queues_out. @@ -189,14 +189,19 @@ class GameIO(): for connection_id in self.queues_out: self.queues_out[connection_id].put(msg) - def quote(self, string): - """Quote & escape string so client interprets it as single token.""" - # FIXME: Don't do this as a method, makes no sense. - quoted = [] - quoted += ['"'] - for c in string: - if c in {'"', '\\'}: - quoted += ['\\'] - quoted += [c] - quoted += ['"'] - return ''.join(quoted) + +def quote(string): + """Quote & escape string so client interprets it as single token.""" + quoted = [] + quoted += ['"'] + for c in string: + if c in {'"', '\\'}: + quoted += ['\\'] + quoted += [c] + quoted += ['"'] + return ''.join(quoted) + + +def stringify_yx(tuple_): + """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x).""" + return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1]) -- 2.30.2