X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=client.py;h=6e22d0006f4d911d06c1373faf968a836b17a206;hb=5f2e6342f11b5fd438f4c3d60f48f0bd08f32590;hp=f356442390d22e2bb53d98521481b7da48d28a4b;hpb=7e119240b273588b4dce81ad211aa1548a0ca7f6;p=plomrogue2-experiments diff --git a/client.py b/client.py index f356442..6e22d00 100755 --- a/client.py +++ b/client.py @@ -3,162 +3,80 @@ import urwid import plom_socket_io import socket import threading -from functools import partial -import unittest +from parser import ArgError, Parser +import game_common -class ArgumentError(Exception): - pass +class MapSquare(game_common.Map): + def list_terrain_to_lines(self, terrain_as_list): + terrain = ''.join(terrain_as_list) + map_lines = [] + start_cut = 0 + while start_cut < len(terrain): + limit = start_cut + self.size[1] + map_lines += [terrain[start_cut:limit]] + start_cut = limit + return map_lines -class ParseError(Exception): - pass +class MapHex(game_common.Map): -class Parser: + def list_terrain_to_lines(self, terrain_as_list): + new_terrain_list = [' '] + x = 0 + y = 0 + for c in terrain_as_list: + new_terrain_list += [c, ' '] + x += 1 + if x == self.size[1]: + new_terrain_list += ['\n'] + x = 0 + y += 1 + if y % 2 == 0: + new_terrain_list += [' '] + return ''.join(new_terrain_list).split('\n') - def __init__(self, game): - self.game = game - def tokenize(self, msg): - """Parse msg string into tokens. +map_manager = game_common.MapManager(globals()) - Separates by ' ' and '\n', but allows whitespace in tokens quoted by - '"', and allows escaping within quoted tokens by a prefixed backslash. - """ - tokens = [] - token = '' - quoted = False - escaped = False - for c in msg: - if quoted: - if escaped: - token += c - escaped = False - elif c == '\\': - escaped = True - elif c == '"': - quoted = False - else: - token += c - elif c == '"': - quoted = True - elif c in {' ', '\n'}: - if len(token) > 0: - tokens += [token] - token = '' - else: - token += c - if len(token) > 0: - tokens += [token] - return tokens - def parse(self, msg): - """Parse msg as call to self.game method, return method with arguments. +class World(game_common.World): - Respects method signatures defined in methods' .argtypes attributes. - """ - tokens = self.tokenize(msg) - if len(tokens) == 0: - return None - method_candidate = 'cmd_' + tokens[0] - if not hasattr(self.game, method_candidate): - return None - method = getattr(self.game, method_candidate) - if len(tokens) == 1: - if not hasattr(method, 'argtypes'): - return method - else: - raise ParseError('Command expects argument(s).') - args_candidates = tokens[1:] - if not hasattr(method, 'argtypes'): - raise ParseError('Command expects no argument(s).') - args, kwargs = self.argsparse(method.argtypes, args_candidates) - return partial(method, *args, **kwargs) - - def parse_yx_tuple(self, yx_string): - """Parse yx_string as yx_tuple:nonneg argtype, return result.""" - - def get_axis_position_from_argument(axis, token): - if len(token) < 3 or token[:2] != axis + ':' or \ - not token[2:].isdigit(): - raise ParseError('Non-int arg for ' + axis + ' position.') - n = int(token[2:]) - if n < 1: - raise ParseError('Arg for ' + axis + ' position < 1.') - return n - - tokens = yx_string.split(',') - if len(tokens) != 2: - raise ParseError('Wrong number of yx-tuple arguments.') - y = get_axis_position_from_argument('Y', tokens[0]) - x = get_axis_position_from_argument('X', tokens[1]) - return (y, x) - - def argsparse(self, signature, args_tokens): - """Parse into / return args_tokens as args/kwargs defined by signature. - - Expects signature to be a ' '-delimited sequence of any of the strings - 'int:nonneg', 'yx_tuple:nonneg', 'string', defining the respective - argument types. + def __init__(self, game, *args, **kwargs): + """Extend original with local classes and empty default map. + + We need the empty default map because we draw the map widget + on any update, even before we actually receive map data. """ - tmpl_tokens = signature.split() - if len(tmpl_tokens) != len(args_tokens): - raise ParseError('Number of arguments (' + str(len(args_tokens)) + - ') not expected number (' + str(len(tmpl_tokens)) - + ').') - args = [] - for i in range(len(tmpl_tokens)): - tmpl = tmpl_tokens[i] - arg = args_tokens[i] - if tmpl == 'int:nonneg': - if not arg.isdigit(): - raise ParseError('Argument must be non-negative integer.') - args += [int(arg)] - elif tmpl == 'yx_tuple:nonneg': - args += [self.parse_yx_tuple(arg)] - elif tmpl == 'string': - args += [arg] - else: - raise ParseError('Unknown argument type.') - return args, {} + super().__init__(*args, **kwargs) + self.game = game + self.map_ = self.game.map_manager.get_map_class('Hex')() -class Game: - turn = 0 - log_text = '' - map_size = (5, 5) - terrain_map = ('?'*5+'\n')*4+'?'*5 - things = [] +class Game(game_common.CommonCommandsMixin): - class Thing: - def __init__(self, position, symbol): - self.position = position - self.symbol = symbol + def __init__(self): + self.map_manager = map_manager + self.world = World(self) + self.log_text = '' def log(self, msg): """Prefix msg plus newline to self.log_text.""" self.log_text = msg + '\n' + self.log_text - def cmd_THING(self, type_, yx): - """Add to self.things at .position yx with .symbol defined by type_.""" + def symbol_for_type(self, type_): symbol = '?' - if type_ == 'TYPE:human': + if type_ == 'human': symbol = '@' - elif type_ == 'TYPE:monster': + elif type_ == 'monster': symbol = 'm' - self.things += [self.Thing(yx, symbol)] - cmd_THING.argtypes = 'string yx_tuple:nonneg' - - def cmd_MAP_SIZE(self, yx): - """Set self.map_size to yx, redraw self.terrain_map as '?' cells.""" - y, x = yx - self.map_size = (y, x) - self.terrain_map = '' - for y in range(self.map_size[0]): - self.terrain_map += '?' * self.map_size[1] + '\n' - self.terrain_map = self.terrain_map[:-1] - cmd_MAP_SIZE.argtypes = 'yx_tuple:nonneg' + return symbol + + def cmd_LAST_PLAYER_TASK_RESULT(self, msg): + if msg != "success": + self.log_text = msg + '\n' + self.log_text + cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string' def cmd_TURN_FINISHED(self, n): """Do nothing. (This may be extended later.)""" @@ -167,20 +85,13 @@ class Game: def cmd_NEW_TURN(self, n): """Set self.turn to n, empty self.things.""" - self.turn = n - self.things = [] + self.world.turn = n + self.world.things = [] cmd_NEW_TURN.argtypes = 'int:nonneg' - def cmd_TERRAIN(self, terrain_map): - """Reset self.terrain_map from terrain_map.""" - lines = terrain_map.split('\n') - if len(lines) != self.map_size[0]: - raise ArgumentError('wrong map height') - for line in lines: - if len(line) != self.map_size[1]: - raise ArgumentError('wrong map width') - self.terrain_map = terrain_map - cmd_TERRAIN.argtypes = 'string' + def cmd_VISIBLE_MAP_LINE(self, y, terrain_line): + self.world.map_.set_line(y, terrain_line) + cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string' class WidgetManager: @@ -189,29 +100,50 @@ class WidgetManager: """Set up all urwid widgets we want on the screen.""" self.game = game edit_widget = self.EditToSocketWidget(socket, 'SEND: ') - self.map_widget = urwid.Text('', wrap='clip') + self.map_widget = self.MapWidget() self.turn_widget = urwid.Text('') self.log_widget = urwid.Text('') - map_box = urwid.Padding(self.map_widget, width=50) - widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget, - self.log_widget]) - self.top = urwid.Filler(widget_pile, valign='top') + edit_map = urwid.AttrMap(edit_widget, 'foo') + turn_map = urwid.AttrMap(self.turn_widget, 'bar') + log_map = urwid.AttrMap(self.log_widget, 'baz') + widget_pile = urwid.Pile([('pack', edit_map), + ('pack', urwid.Divider()), + ('pack', turn_map), + ('pack', urwid.Divider()), + ('pack', log_map), + urwid.SolidFill(fill_char=' ')]) + self.top = urwid.Columns([(20, widget_pile), self.map_widget], + dividechars=1) + self.palette = [('foo', 'white', 'dark red'), + ('bar', 'white', 'dark blue'), + ('baz', 'white', 'dark green')] def draw_map(self): - """Draw map view from .game.terrain_map, .game.things.""" - whole_map = [] - for c in self.game.terrain_map: - whole_map += [c] - for t in self.game.things: - pos_i = t.position[0] * (self.game.map_size[1] + 1) + t.position[1] - whole_map[pos_i] = t.symbol - return ''.join(whole_map) + """Draw map view from .game.map_.terrain, .game.things.""" + terrain_as_list = list(self.game.world.map_.terrain[:]) + for t in self.game.world.things: + pos_i = self.game.world.map_.get_position_index(t.position) + terrain_as_list[pos_i] = self.game.symbol_for_type(t.type_) + return self.game.world.map_.list_terrain_to_lines(terrain_as_list) + #text = self.game.world.map_.list_terrain_to_lines(terrain_as_list) + #new_map_text = [] + #for char in text: + # if char == '.': + # new_map_text += [('foo', char)] + # elif char in {'x', 'X', '#'}: + # new_map_text += [('bar', char)] + # elif char in {'@', 'm'}: + # new_map_text += [('baz', char)] + # else: + # new_map_text += [char] + #return new_map_text def update(self): """Redraw all non-edit widgets.""" - self.turn_widget.set_text('TURN: ' + str(self.game.turn)) + self.turn_widget.set_text('TURN: ' + str(self.game.world.turn)) self.log_widget.set_text(self.game.log_text) - self.map_widget.set_text(self.draw_map()) + self.map_widget.text = self.draw_map() + self.map_widget._invalidate() class EditToSocketWidget(urwid.Edit): """Extends urwid.Edit with socket to send input on 'enter' to.""" @@ -227,6 +159,27 @@ class WidgetManager: plom_socket_io.send(self.socket, self.edit_text) self.edit_text = '' + class MapWidget(urwid.Widget): + _sizing = frozenset(['box']) + text = [''] + + def render(self, size, focus=False): + maxcol, maxrow = size + content = [] + for y in range(len(self.text)): + if y < maxrow: + line = self.text[y] + if len(line) < maxcol: + line = line + '0' * (maxcol - len(line)) + else: + line = line[:maxcol] + content += [line.encode('utf-8')] + padding_y = maxrow - len(content) + if padding_y > 0: + for y in range(padding_y): + content += ['0'.encode('utf-8') * maxcol] + return urwid.TextCanvas(content) + class PlomRogueClient: @@ -257,7 +210,8 @@ class PlomRogueClient: self.socket = socket self.widget_manager = WidgetManager(self.socket, self.game) self.server_output = [] - self.urwid_loop = urwid.MainLoop(self.widget_manager.top) + self.urwid_loop = urwid.MainLoop(self.widget_manager.top, + self.widget_manager.palette) self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self. handle_input) self.recv_loop_thread = threading.Thread(target=self.recv_loop) @@ -285,7 +239,7 @@ class PlomRogueClient: self.game.log('UNHANDLED INPUT: ' + msg) else: command() - except (ArgumentError, ParseError) as e: + except ArgError as e: self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e)) self.widget_manager.update() del self.server_output[0] @@ -312,52 +266,6 @@ class PlomRogueClient: self.recv_loop_thread.join() -class TestParser(unittest.TestCase): - - def test_tokenizer(self): - p = Parser(Game()) - self.assertEqual(p.tokenize(''), []) - self.assertEqual(p.tokenize(' '), []) - self.assertEqual(p.tokenize('abc'), ['abc']) - self.assertEqual(p.tokenize('a b\nc "d"'), ['a', 'b', 'c', 'd']) - self.assertEqual(p.tokenize('a "b\nc d"'), ['a', 'b\nc d']) - self.assertEqual(p.tokenize('a"b"c'), ['abc']) - self.assertEqual(p.tokenize('a\\b'), ['a\\b']) - self.assertEqual(p.tokenize('"a\\b"'), ['ab']) - self.assertEqual(p.tokenize('a"b'), ['ab']) - self.assertEqual(p.tokenize('a"\\"b'), ['a"b']) - - def test_unhandled(self): - p = Parser(Game()) - self.assertEqual(p.parse(''), None) - self.assertEqual(p.parse(' '), None) - self.assertEqual(p.parse('x'), None) - - def test_argsparse(self): - p = Parser(Game()) - assertErr = partial(self.assertRaises, ParseError, p.argsparse) - assertErr('', ['foo']) - assertErr('string', []) - assertErr('string string', ['foo']) - self.assertEqual(p.argsparse('string', ('foo',)), - (['foo'], {})) - self.assertEqual(p.argsparse('string string', ('foo', 'bar')), - (['foo', 'bar'], {})) - assertErr('int:nonneg', ['']) - assertErr('int:nonneg', ['x']) - assertErr('int:nonneg', ['-1']) - assertErr('int:nonneg', ['0.1']) - self.assertEqual(p.argsparse('int:nonneg', ('0',)), - ([0], {})) - assertErr('yx_tuple:nonneg', ['x']) - assertErr('yx_tuple:nonneg', ['Y:0,X:1']) - assertErr('yx_tuple:nonneg', ['Y:1,X:0']) - assertErr('yx_tuple:nonneg', ['Y:1.1,X:1']) - assertErr('yx_tuple:nonneg', ['Y:1,X:1.1']) - self.assertEqual(p.argsparse('yx_tuple:nonneg', ('Y:1,X:2',)), - ([(1, 2)], {})) - - if __name__ == '__main__': game = Game() s = socket.create_connection(('127.0.0.1', 5000))