home · contact · privacy
Lots of refactoring to enable SAVE command.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 30 Jan 2019 14:00:51 +0000 (15:00 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 30 Jan 2019 14:00:51 +0000 (15:00 +0100)
client-curses.py
game_common.py
parser.py
server.py
server_/game.py
server_/io.py

index 9a8178d9d133ad5b366a43819aa5fbefd5a2588e..ea5dcf32f8ad34d9281de174566fe64f3a20601f 100755 (executable)
@@ -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)
index 3c17bd1b3c06abd2c40116291c0fc8da15cfd584..c0fb98b3fa9138a5766c3b6be70d47b12ad28240 100644 (file)
@@ -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)
index 2292f883d9d16f9bf4f113c5bfba3d773627e04e..03b688f24e8eccf18cd1c024e5804ef96e1087d5 100644 (file)
--- 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'])
index cdeb27dc6c5081093c37117d8e144818098379a1..cb9150b8d6e05a00993f6c4073057db350d8f8bc 100755 (executable)
--- 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')
index 958bdc8c7d8a5caaf43e88234a0c62b413b2728e..0974f1b7050c22891087d7fc57e1e72e92035160 100644 (file)
@@ -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
index d2d67e98969d92fde0b6eb7cc842aa9df5410f4d..220003768c598c71ed48d26fc090c68f0a0d2b5c 100644 (file)
@@ -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])