+def fib(n):
+ """Calculate n-th Fibonacci number. Very inefficiently."""
+ if n in (1, 2):
+ return 1
+ else:
+ return fib(n-1) + fib(n-2)
+
+
+class CommandHandler:
+
+ def __init__(self, queues_out={}):
+ from multiprocessing import Pool
+ self.queues_out = queues_out
+ self.world = World()
+ self.parser = Parser(self)
+ # self.pool and self.pool_result are currently only needed by the FIB
+ # command and the demo of a parallelized game loop in cmd_inc_p.
+ self.pool = Pool()
+ self.pool_result = None
+
+ def handle_input(self, input_, connection_id=None, abort_on_error=False):
+ """Process input_ to command grammar, call command handler if found."""
+ try:
+ command = self.parser.parse(input_)
+ if command is None:
+ self.send_to(connection_id, 'UNHANDLED INPUT')
+ else:
+ command(connection_id=connection_id)
+ except ArgError as e:
+ self.send_to(connection_id, 'ARGUMENT ERROR: ' + str(e))
+ if abort_on_error:
+ exit(1)
+ except GameError as e:
+ self.send_to(connection_id, 'GAME ERROR: ' + str(e))
+ if abort_on_error:
+ exit(1)
+
+ def send_to(self, connection_id, msg):
+ """Send msg to client of connection_id; if no later, print instead."""
+ if connection_id:
+ self.queues_out[connection_id].put(msg)
+ else:
+ print(msg)
+
+ def send_all(self, msg):
+ """Send msg to all clients."""
+ for connection_id in self.queues_out:
+ self.send_to(connection_id, msg)
+
+ def stringify_yx(self, tuple_):
+ """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
+ return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
+
+ def quoted(self, string):
+ """Quote and 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 send_all_gamestate(self):
+ """Send out game state data relevant to clients."""
+ self.send_all('NEW_TURN ' + str(self.world.turn))
+ self.send_all('MAP_SIZE ' + self.stringify_yx(self.world.map_size))
+ for y in range(self.world.map_size[0]):
+ width = self.world.map_size[1]
+ terrain_line = self.world.map_[y * width:(y + 1) * width]
+ self.send_all('TERRAIN_LINE %5s %s' % (y, self.quoted(terrain_line)))
+ for thing in self.world.things:
+ self.send_all('THING TYPE:' + thing.type_ + ' '
+ + self.stringify_yx(thing.position))
+
+ def proceed(self):
+ """Send turn finish signal, run game world, send new world data.
+
+ First sends 'TURN_FINISHED' message, then runs game world
+ until new player input is needed, then sends game state.
+ """
+ self.send_all('TURN_FINISHED ' + str(self.world.turn))
+ self.world.proceed_to_next_player_turn()
+ self.send_all_gamestate()
+
+ def cmd_MOVE(self, direction, connection_id):
+ """Set player task to 'move' with direction arg, finish player turn."""
+ if direction not in {'UP', 'DOWN', 'RIGHT', 'LEFT'}:
+ raise ArgError('Move argument must be one of: '
+ 'UP, DOWN, RIGHT, LEFT')
+ self.world.player.set_task('move', direction=direction)
+ self.proceed()
+ cmd_MOVE.argtypes = 'string'
+
+ def cmd_WAIT(self, connection_id):
+ """Set player task to 'wait', finish player turn."""
+ self.world.player.set_task('wait')
+ self.proceed()
+
+ def cmd_GET_TURN(self, connection_id):
+ """Send world.turn to caller."""
+ self.send_to(connection_id, str(self.world.turn))
+
+ def cmd_ECHO(self, msg, connection_id):
+ """Send msg to caller."""
+ self.send_to(connection_id, msg)
+ cmd_ECHO.argtypes = 'string'
+
+ def cmd_ALL(self, msg, connection_id):
+ """Send msg to all clients."""
+ self.send_all(msg)
+ cmd_ALL.argtypes = 'string'
+
+ def cmd_FIB(self, numbers, connection_id):
+ """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
+
+ Numbers are calculated in parallel as far as possible, using fib().
+ A 'CALCULATING …' message is sent to caller before the result.
+ """
+ self.send_to(connection_id, 'CALCULATING …')
+ results = self.pool.map(fib, numbers)
+ reply = ' '.join([str(r) for r in results])
+ self.send_to(connection_id, reply)
+ cmd_FIB.argtypes = 'seq:int:nonneg'
+
+ 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.
+
+ 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:
+ self.pool_result.wait()
+ self.send_all('TURN_FINISHED ' + str(self.world.turn))
+ sleep(1)
+ self.world.turn += 1
+ self.send_all_gamestate()
+ self.pool_result = self.pool.map_async(fib, (35, 35))
+
+
+def io_loop(q, commander):
+ """Handle commands coming through queue q, send results back.
+
+ Commands from q are expected to be tuples, with the first element either
+ 'ADD_QUEUE', 'COMMAND', or 'KILL_QUEUE', the second element a UUID, and
+ an optional third element of arbitrary type. The UUID identifies a
+ receiver for replies.
+
+ An 'ADD_QUEUE' command should contain as third element a queue through
+ which to send messages back to the sender of the command. A 'KILL_QUEUE'
+ command removes the queue for that receiver from the list of queues through
+ which to send replies.
+
+ A 'COMMAND' command is specified in greater detail by a string that is the
+ tuple's third element. The commander CommandHandler takes care of processing
+ this and sending out replies.
+ """