X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=server.py;h=6008346cfde27f0374e9b46817a5812c9dafcdd1;hb=3c28c76381a634dd23642abeb4f03a92ec4f1f94;hp=14df1f37b5b02be228ad82056813c717720df2e1;hpb=7e119240b273588b4dce81ad211aa1548a0ca7f6;p=plomrogue2-experiments diff --git a/server.py b/server.py index 14df1f3..6008346 100755 --- a/server.py +++ b/server.py @@ -3,6 +3,9 @@ import socketserver import threading import queue +from parser import ArgError, Parser +from server_.game import World, GameError + # Avoid "Address already in use" errors. socketserver.TCPServer.allow_reuse_address = True @@ -79,77 +82,6 @@ class IO_Handler(socketserver.BaseRequestHandler): self.request.close() -class Task: - - def __init__(self, name, args=(), kwargs={}): - self.name = name - self.args = args - self.kwargs = kwargs - self.todo = 1 - - -class Thing: - - def __init__(self, type_, position): - self.type = type_ - self.position = position - self.task = Task('wait') - - def task_wait(self): - pass - - def task_move(self, direction): - if direction == 'UP': - self.position[0] -= 1 - elif direction == 'DOWN': - self.position[0] += 1 - elif direction == 'RIGHT': - self.position[1] += 1 - elif direction == 'LEFT': - self.position[1] -= 1 - - def decide_task(self): - if self.position[1] > 1: - self.set_task('move', 'LEFT') - elif self.position[1] < 3: - self.set_task('move', 'RIGHT') - else: - self.set_task('wait') - - def set_task(self, task, *args, **kwargs): - self.task = Task(task, args, kwargs) - - def proceed(self, is_AI=True): - """Further the thing in its tasks. - - Decrements .task.todo; if it thus falls to <= 0, enacts method whose - name is 'task_' + self.task.name and sets .task = None. If is_AI, calls - .decide_task to decide a self.task. - """ - self.task.todo -= 1 - if self.task.todo <= 0: - task = getattr(self, 'task_' + self.task.name) - task(*self.task.args, **self.task.kwargs) - self.task = None - if is_AI and self.task is None: - self.decide_task() - - -class World: - - def __init__(self): - self.turn = 0 - self.map_size = (5, 5) - self.map_ = 'xxxxx\n' +\ - 'x...x\n' +\ - 'x.X.x\n' +\ - 'x...x\n' +\ - 'xxxxx' - self.things = [Thing('human', [3, 3]), Thing('monster', [1, 1])] - self.player_i = 0 - self.player = self.things[self.player_i] - - def fib(n): """Calculate n-th Fibonacci number. Very inefficiently.""" if n in (1, 2): @@ -158,21 +90,31 @@ def fib(n): return fib(n-1) + fib(n-2) -class ArgumentError(Exception): - pass - - 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): + """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)) + except GameError as e: + self.send_to(connection_id, 'GAME ERROR: ' + str(e)) + def send_to(self, connection_id, msg): """Send msg to client of connection_id.""" self.queues_out[connection_id].put(msg) @@ -197,6 +139,27 @@ class CommandHandler: quoted += ['"'] return ''.join(quoted) + def quoted_map(self, map_string, map_width): + """Put \n into map_string at map_width intervals, return quoted whole.""" + map_lines = [] + map_size = len(map_string) + start_cut = 0 + while start_cut < map_size: + limit = start_cut + map_width + map_lines += [map_string[start_cut:limit]] + start_cut = limit + return self.quoted("\n".join(map_lines)) + + 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)) + self.send_all('TERRAIN\n' + self.quoted_map(self.world.map_, + self.world.map_size[1])) + for thing in self.world.things: + self.send_all('THING TYPE:' + thing.type_ + ' ' + + self.stringify_yx(thing.position)) + def proceed_to_next_player_turn(self, connection_id): """Run game world turns until player can decide their next step. @@ -219,32 +182,49 @@ class CommandHandler: self.world.player.proceed(is_AI=False) if self.world.player.task is None: break - self.send_all('NEW_TURN ' + str(self.world.turn)) - self.send_all('MAP_SIZE ' + self.stringify_yx(self.world.map_size)) - self.send_all('TERRAIN\n' + self.quoted(self.world.map_)) - for thing in self.world.things: - self.send_all('THING TYPE:' + thing.type + ' ' - + self.stringify_yx(thing.position)) + self.send_all_gamestate() - def cmd_fib(self, tokens, connection_id): + 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_to_next_player_turn(connection_id) + 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_to_next_player_turn(connection_id) + + 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. """ - if len(tokens) < 2: - raise ArgumentError('FIB NEEDS AT LEAST ONE ARGUMENT') - numbers = [] - for token in tokens[1:]: - if token == '0' or not token.isdigit(): - raise ArgumentError('FIB ARGUMENTS MUST BE INTEGERS > 0') - numbers += [int(token)] 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): + 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 @@ -264,67 +244,9 @@ class CommandHandler: self.send_all('TURN_FINISHED ' + str(self.world.turn)) sleep(1) self.world.turn += 1 - self.send_all('NEW_TURN ' + str(self.world.turn)) - self.send_all('MAP_SIZE ' + self.stringify_yx(self.world.map_size)) - self.send_all('TERRAIN\n' + self.quoted(self.world.map_)) - for thing in self.world.things: - self.send_all('THING TYPE:' + thing.type + ' ' - + self.stringify_yx(thing.position)) + self.send_all_gamestate() self.pool_result = self.pool.map_async(fib, (35, 35)) - def cmd_get_turn(self, connection_id): - """Send world.turn to caller.""" - self.send_to(connection_id, str(self.world.turn)) - - 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 ArgumentError('MOVE ARGUMENT MUST BE ONE OF: ' - 'UP, DOWN, RIGHT, LEFT') - self.world.player.set_task('move', direction=direction) - self.proceed_to_next_player_turn(connection_id) - - def cmd_wait(self, connection_id): - """Set player task to 'wait', finish player turn.""" - self.world.player.set_task('wait') - self.proceed_to_next_player_turn(connection_id) - - def cmd_echo(self, tokens, input_, connection_id): - """Send message in input_ beyond tokens[0] to caller.""" - msg = input_[len(tokens[0]) + 1:] - self.send_to(connection_id, msg) - - def cmd_all(self, tokens, input_): - """Send message in input_ beyond tokens[0] to all clients.""" - msg = input_[len(tokens[0]) + 1:] - self.send_all(msg) - - def handle_input(self, input_, connection_id): - """Process input_ to command grammar, call command handler if found.""" - tokens = [token for token in input_.split(' ') if len(token) > 0] - try: - if len(tokens) == 0: - self.send_to(connection_id, 'EMPTY COMMAND') - elif len(tokens) == 1 and tokens[0] == 'INC_P': - self.cmd_inc_p(connection_id) - elif len(tokens) == 1 and tokens[0] == 'GET_TURN': - self.cmd_get_turn(connection_id) - elif len(tokens) == 1 and tokens[0] == 'WAIT': - self.cmd_wait(connection_id) - elif len(tokens) == 2 and tokens[0] == 'MOVE': - self.cmd_move(tokens[1], connection_id) - elif len(tokens) >= 1 and tokens[0] == 'ECHO': - self.cmd_echo(tokens, input_, connection_id) - elif len(tokens) >= 1 and tokens[0] == 'ALL': - self.cmd_all(tokens, input_) - elif len(tokens) >= 1 and tokens[0] == 'FIB': - # TODO: Should this really block the whole loop? - self.cmd_fib(tokens, connection_id) - else: - self.send_to(connection_id, 'UNKNOWN COMMAND') - except ArgumentError as e: - self.send_to(connection_id, 'ARGUMENT ERROR: ' + str(e)) - def io_loop(q): """Handle commands coming through queue q, send results back.