X-Git-Url: https://plomlompom.com/repos/%7B%7Bprefix%7D%7D/balance2?a=blobdiff_plain;f=server.py;h=7f05cb5d687bec9150dabac85a36cdae734c29d1;hb=5765f660c2da8b03b7a5d1eba42bd76cf0abf815;hp=14df1f37b5b02be228ad82056813c717720df2e1;hpb=7e119240b273588b4dce81ad211aa1548a0ca7f6;p=plomrogue2-experiments diff --git a/server.py b/server.py index 14df1f3..7f05cb5 100755 --- a/server.py +++ b/server.py @@ -3,11 +3,17 @@ import socketserver import threading import queue +from parser import ArgError, Parser + # Avoid "Address already in use" errors. socketserver.TCPServer.allow_reuse_address = True +class GameError(Exception): + pass + + class Server(socketserver.ThreadingTCPServer): """Bind together threaded IO handling server and message queue.""" @@ -90,8 +96,9 @@ class Task: class Thing: - def __init__(self, type_, position): - self.type = type_ + def __init__(self, world, type_, position): + self.world = world + self.type_ = type_ self.position = position self.task = Task('wait') @@ -116,7 +123,32 @@ class Thing: else: self.set_task('wait') + def check_task(self, task, *args, **kwargs): + if task == 'move': + if len(args) > 0: + direction = args[0] + else: + direction = kwargs['direction'] + test_pos = self.position[:] + if direction == 'UP': + test_pos[0] -= 1 + elif direction == 'DOWN': + test_pos[0] += 1 + elif direction == 'RIGHT': + test_pos[1] += 1 + elif direction == 'LEFT': + test_pos[1] -= 1 + if test_pos[0] < 0 or test_pos[1] < 0 or \ + test_pos[0] >= self.world.map_size[0] or \ + test_pos[1] >= self.world.map_size[1]: + raise GameError('would move outside map bounds') + pos_i = test_pos[0] * self.world.map_size[1] + test_pos[1] + map_tile = self.world.map_[pos_i] + if map_tile != '.': + raise GameError('would move into illegal terrain') + def set_task(self, task, *args, **kwargs): + self.check_task(task, *args, **kwargs) self.task = Task(task, args, kwargs) def proceed(self, is_AI=True): @@ -140,12 +172,15 @@ 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' +\ + self.map_ = 'xxxxx' +\ + 'x...x' +\ + 'x.X.x' +\ + 'x...x' +\ 'xxxxx' - self.things = [Thing('human', [3, 3]), Thing('monster', [1, 1])] + self.things = [ + Thing(self, 'human', [3, 3]), + Thing(self, 'monster', [1, 1]) + ] self.player_i = 0 self.player = self.things[self.player_i] @@ -158,21 +193,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 +242,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 +285,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 +347,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.