X-Git-Url: https://plomlompom.com/repos/berlin_corona.txt?a=blobdiff_plain;f=server.py;h=e289e0a73a03ea123aa4ab1524cef8ea792a2616;hb=7a02f223961c532d9423433427c0e1b3e1cdb871;hp=22cd7ec3f8ffa9a338888830a68b764549d811cb;hpb=877b51134a2ce2e808e9403194d05b88b1b3b801;p=plomrogue2-experiments diff --git a/server.py b/server.py index 22cd7ec..e289e0a 100755 --- a/server.py +++ b/server.py @@ -3,17 +3,17 @@ import socketserver import threading import queue +import sys +import os from parser import ArgError, Parser +from server_.game import World, GameError +from game_common import Commander # 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.""" @@ -85,104 +85,6 @@ class IO_Handler(socketserver.BaseRequestHandler): self.request.close() -def move_pos(direction, pos_yx): - if direction == 'UP': - pos_yx[0] -= 1 - elif direction == 'DOWN': - pos_yx[0] += 1 - elif direction == 'RIGHT': - pos_yx[1] += 1 - elif direction == 'LEFT': - pos_yx[1] -= 1 - - -class Task: - - def __init__(self, thing, name, args=(), kwargs={}): - self.name = name - self.thing = thing - self.args = args - self.kwargs = kwargs - self.todo = 1 - - def check(self): - if self.name == 'move': - if len(self.args) > 0: - direction = self.args[0] - else: - direction = self.kwargs['direction'] - test_pos = self.thing.position[:] - move_pos(direction, test_pos) - if test_pos[0] < 0 or test_pos[1] < 0 or \ - test_pos[0] >= self.thing.world.map_size[0] or \ - test_pos[1] >= self.thing.world.map_size[1]: - raise GameError('would move outside map bounds') - pos_i = test_pos[0] * self.thing.world.map_size[1] + test_pos[1] - map_tile = self.thing.world.map_[pos_i] - if map_tile != '.': - raise GameError('would move into illegal terrain') - - -class Thing: - - def __init__(self, world, type_, position): - self.world = world - self.type_ = type_ - self.position = position - self.task = Task(self, 'wait') - - def task_wait(self): - pass - - def task_move(self, direction): - move_pos(direction, self.position) - - 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(self, task, args, kwargs) - self.task.check() - - 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' +\ - 'x...x' +\ - 'x.X.x' +\ - 'x...x' +\ - 'xxxxx' - self.things = [ - Thing(self, 'human', [3, 3]), - Thing(self, '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): @@ -191,11 +93,11 @@ def fib(n): return fib(n-1) + fib(n-2) -class CommandHandler: +class CommandHandler(Commander): - def __init__(self, queues_out): + def __init__(self): from multiprocessing import Pool - self.queues_out = queues_out + self.queues_out = {} self.world = World() self.parser = Parser(self) # self.pool and self.pool_result are currently only needed by the FIB @@ -203,101 +105,91 @@ class CommandHandler: self.pool = Pool() self.pool_result = None - def handle_input(self, input_, connection_id): + def handle_input(self, input_, connection_id=None, abort_on_error=False): """Process input_ to command grammar, call command handler if found.""" + from inspect import signature try: command = self.parser.parse(input_) if command is None: self.send_to(connection_id, 'UNHANDLED INPUT') else: - command(connection_id=connection_id) + if 'connection_id' in list(signature(command).parameters): + command(connection_id=connection_id) + else: + command() 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.""" - self.queues_out[connection_id].put(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 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.""" + + 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]) + + def quoted(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) + 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])) + self.send_all('MAP_SIZE ' + 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.terrain_map[y * width:(y + 1) * width] + self.send_all('TERRAIN_LINE %5s %s' % (y, quoted(terrain_line))) 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. - - Sends a 'TURN_FINISHED' message, then iterates through all non-player - things, on each step furthering them in their tasks (and letting them - decide new ones if they finish). The iteration order is: first all - things that come after the player in the world things list, then (after - incrementing the world turn) all that come before the player; then the - player's .proceed() is run, and if it does not finish his task, the - loop starts at the beginning. Once the player's task is finished, the - loop breaks, and client-relevant game data is sent. + self.send_all('THING_TYPE %s %s' % (thing.id_, thing.type_)) + self.send_all('THING_POS %s %s' % (thing.id_, + 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)) - while True: - for thing in self.world.things[self.world.player_i+1:]: - thing.proceed() - self.world.turn += 1 - for thing in self.world.things[:self.world.player_i]: - thing.proceed() - self.world.player.proceed(is_AI=False) - if self.world.player.task is None: - break + self.world.proceed_to_next_player_turn() self.send_all_gamestate() - def cmd_MOVE(self, direction, connection_id): + def cmd_MOVE(self, direction): """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) + self.world.get_player().set_task('move', direction=direction) + self.proceed() cmd_MOVE.argtypes = 'string' - def cmd_WAIT(self, connection_id): + def cmd_WAIT(self): """Set player task to 'wait', finish player turn.""" - self.world.player.set_task('wait') - self.proceed_to_next_player_turn(connection_id) + self.world.get_player().set_task('wait') + self.proceed() def cmd_GET_TURN(self, connection_id): """Send world.turn to caller.""" @@ -349,7 +241,7 @@ class CommandHandler: self.pool_result = self.pool.map_async(fib, (35, 35)) -def io_loop(q): +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 @@ -363,26 +255,50 @@ def io_loop(q): which to send replies. A 'COMMAND' command is specified in greater detail by a string that is the - tuple's third element. CommandHandler takes care of processing this and - sending out replies. + tuple's third element. The commander CommandHandler takes care of processing + this and sending out replies. """ - queues_out = {} - command_handler = CommandHandler(queues_out) while True: x = q.get() command_type = x[0] connection_id = x[1] content = None if len(x) == 2 else x[2] if command_type == 'ADD_QUEUE': - queues_out[connection_id] = content + commander.queues_out[connection_id] = content elif command_type == 'COMMAND': - command_handler.handle_input(content, connection_id) + commander.handle_input(content, connection_id) elif command_type == 'KILL_QUEUE': - del queues_out[connection_id] + del commander.queues_out[connection_id] +if len(sys.argv) != 2: + print('wrong number of arguments, expected one (game file)') + exit(1) +game_file_name = sys.argv[1] +commander = CommandHandler() +if os.path.exists(game_file_name): + if not os.path.isfile(game_file_name): + print('game file name does not refer to a valid game file') + else: + with open(game_file_name, 'r') as f: + lines = f.readlines() + for i in range(len(lines)): + line = lines[i] + print("FILE INPUT LINE %s: %s" % (i, line), end='') + commander.handle_input(line, abort_on_error=True) +else: + commander.handle_input('MAP_SIZE Y:5,X:5') + commander.handle_input('TERRAIN_LINE 0 "xxxxx"') + commander.handle_input('TERRAIN_LINE 1 "x...x"') + commander.handle_input('TERRAIN_LINE 2 "x.X.x"') + commander.handle_input('TERRAIN_LINE 3 "x...x"') + commander.handle_input('TERRAIN_LINE 4 "xxxxx"') + commander.handle_input('THING_TYPE 0 human') + commander.handle_input('THING_POS 0 Y:3,X:3') + commander.handle_input('THING_TYPE 1 monster') + commander.handle_input('THING_POS 1 Y:1,X:1') q = queue.Queue() -c = threading.Thread(target=io_loop, daemon=True, args=(q,)) +c = threading.Thread(target=io_loop, daemon=True, args=(q, commander)) c.start() server = Server(q, ('localhost', 5000), IO_Handler) try: