+ self.main_loop = urwid.MainLoop(self.setup_widgets())
+ self.server_output = []
+ input_handler = getattr(self.InputHandler(self.reply_widget,
+ self.map_widget,
+ self.server_output),
+ 'handle_input')
+ self.urwid_pipe_write_fd = self.main_loop.watch_pipe(input_handler)
+ self.recv_loop_thread = threading.Thread(target=self.recv_loop)
+
+ def setup_widgets(self):
+ """Return container widget with all widgets we want on our screen.
+
+ Sets up an urwid.Pile inside a returned urwid.Filler; top to bottom:
+ - an EditToSocketWidget, prefixing self.socket input with 'SEND: '
+ - a 50-col wide urwid.Padding container for self.map_widget, which is
+ to print clipped map representations
+ - self.reply_widget, a urwid.Text widget printing self.socket replies
+ """
+ edit_widget = self.EditToSocketWidget(self.socket, 'SEND: ')
+ self.reply_widget = urwid.Text('')
+ self.map_widget = self.MapWidget('', wrap='clip')
+ map_box = urwid.Padding(self.map_widget, width=50)
+ widget_pile = urwid.Pile([edit_widget, map_box, self.reply_widget])
+ return urwid.Filler(widget_pile, valign='top')
+
+ class EditToSocketWidget(urwid.Edit):
+ """Extends urwid.Edit with socket to send input on 'enter' to."""
+
+ def __init__(self, socket, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.socket = socket
+
+ def keypress(self, size, key):
+ """Extend super(): on Enter, send .edit_text, and empty it."""
+ if key != 'enter':
+ return super().keypress(size, key)
+ plom_socket_io.send(self.socket, self.edit_text)
+ self.edit_text = ''
+
+ class MapWidget(urwid.Text):
+ """Stores/updates/draws game map."""
+ map_size = (5, 5)
+ terrain_map = ' ' * 25
+ position = (0, 0)
+
+ def draw_map(self):
+ """Draw map view from .map_size, .terrain_map, .position."""
+ whole_map = []
+ for c in self.terrain_map:
+ whole_map += [c]
+ pos_i = self.position[0] * (self.map_size[1] + 1) + self.position[1]
+ whole_map[pos_i] = '@'
+ self.set_text(''.join(whole_map))
+
+ def get_yx(self, yx_string):
+
+ def get_axis_position_from_argument(axis, token):
+ if len(token) < 3 or token[:2] != axis + ':' or \
+ not token[2:].isdigit():
+ raise ArgumentError('Bad arg for ' + axis + ' position.')
+ return int(token[2:])
+
+ tokens = yx_string.split(',')
+ if len(tokens) != 2:
+ raise ArgumentError('wrong number of ","-separated arguments')
+ y = get_axis_position_from_argument('Y', tokens[0])
+ x = get_axis_position_from_argument('X', tokens[1])
+ return (y, x)
+
+ def update_map_size(self, size_string):
+ """Set map size, redo self.terrain_map in new size, '?'-filled."""
+ new_map_size = self.get_yx(size_string)
+ if 0 in new_map_size:
+ raise ArgumentError('size value for either axis must be >0')
+ self.map_size = new_map_size
+ self.terrain_map = ''
+ for y in range(self.map_size[0]):
+ self.terrain_map += '?' * self.map_size[1] + '\n'
+ self.draw_map()
+
+ def update_terrain(self, terrain_map):
+ """Update self.terrain_map. Ensure size matching self.map_size."""
+ 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
+ self.draw_map()
+
+ def update_position(self, position_string):
+ """Update self.position, ensure it's within map bounds."""
+
+ def get_axis_position_from_argument(axis, token):
+ if len(token) < 3 or token[:2] != axis + ':' or \
+ not token[2:].isdigit():
+ raise ArgumentError('Bad arg for ' + axis + ' position.')
+ return int(token[2:])
+
+ new_position = self.get_yx(position_string)
+ if new_position[0] >= self.map_size[0] or \
+ new_position[1] >= self.map_size[1]:
+ raise ArgumentError('Position outside of map size bounds.')
+ self.position = new_position
+ self.draw_map()
+
+ class InputHandler:
+ """Delivers data from other thread to widget via message_container.
+
+ The class only exists to provide handle_input as a bound method, with
+ widget and message_container pre-set, as (bound) handle_input is used
+ as a callback in urwid's watch_pipe – which merely provides its
+ callback target with one parameter for a pipe to read data from an
+ urwid-external thread.
+ """