6 from parser import ArgError, Parser
7 from game_common import World
11 def __init__(self, id_, position, symbol):
14 self.position = position
21 """Prefix msg plus newline to self.log_text."""
22 self.log_text = msg + '\n' + self.log_text
24 def cmd_THING_TYPE(self, i, type_):
25 t = self.world.get_thing(i)
29 elif type_ == 'monster':
32 cmd_THING_TYPE.argtypes = 'int:nonneg string'
34 def cmd_THING_POS(self, i, yx):
35 t = self.world.get_thing(i)
37 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
39 def cmd_THING_POS(self, i, yx):
40 t = self.world.get_thing(i)
42 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
44 def cmd_MAP_SIZE(self, yx):
45 """Set self.map_size to yx, redraw self.terrain_map as '?' cells."""
46 self.world.set_map_size(yx)
47 cmd_MAP_SIZE.argtypes = 'yx_tuple:nonneg'
49 def cmd_TURN_FINISHED(self, n):
50 """Do nothing. (This may be extended later.)"""
52 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
54 def cmd_NEW_TURN(self, n):
55 """Set self.turn to n, empty self.things."""
57 self.world.things = []
58 cmd_NEW_TURN.argtypes = 'int:nonneg'
60 def cmd_TERRAIN_LINE(self, y, terrain_line):
61 self.world.set_map_line(y, terrain_line)
62 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
67 def __init__(self, socket, game):
68 """Set up all urwid widgets we want on the screen."""
70 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
71 self.map_widget = urwid.Text('', wrap='clip')
72 self.turn_widget = urwid.Text('')
73 self.log_widget = urwid.Text('')
74 map_box = urwid.Padding(self.map_widget, width=50)
75 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
77 self.top = urwid.Filler(widget_pile, valign='top')
80 """Draw map view from .game.terrain_map, .game.things."""
82 map_size = len(self.game.world.terrain_map)
84 while start_cut < map_size:
85 limit = start_cut + self.game.world.map_size[1]
86 map_lines += [self.game.world.terrain_map[start_cut:limit]]
88 for t in self.game.world.things:
89 line_as_list = list(map_lines[t.position[0]])
90 line_as_list[t.position[1]] = t.symbol
91 map_lines[t.position[0]] = ''.join(line_as_list)
92 return "\n".join(map_lines)
95 """Redraw all non-edit widgets."""
96 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
97 self.log_widget.set_text(self.game.log_text)
98 self.map_widget.set_text(self.draw_map())
100 class EditToSocketWidget(urwid.Edit):
101 """Extends urwid.Edit with socket to send input on 'enter' to."""
103 def __init__(self, socket, *args, **kwargs):
104 super().__init__(*args, **kwargs)
107 def keypress(self, size, key):
108 """Extend super(): on Enter, send .edit_text, and empty it."""
110 return super().keypress(size, key)
111 plom_socket_io.send(self.socket, self.edit_text)
115 class PlomRogueClient:
117 def __init__(self, game, socket):
118 """Build client urwid interface around socket communication.
120 Sets up all widgets for writing to the socket and representing data
121 from it. Sending via a WidgetManager.EditToSocket widget is
122 straightforward; polling the socket for input from the server in
123 parallel to the urwid main loop not so much:
125 The urwid developers warn against sharing urwid resources among
126 threads, so having a socket polling thread for writing to an urwid
127 widget while other widgets are handled in other threads would be
128 dangerous. Urwid developers recommend using urwid's watch_pipe
129 mechanism instead: using a pipe from non-urwid threads into a single
130 urwid thread. We use self.recv_loop_thread to poll the socket, therein
131 write socket.recv output to an object that is then linked to by
132 self.server_output (which is known to the urwid thread), then use the
133 pipe to urwid to trigger it pulling new data from self.server_output to
134 handle via self.handle_input. (We *could* pipe socket.recv output
135 directly, but then we get complicated buffering situations here as well
136 as in the urwid code that receives the pipe output. It's easier to just
137 tell the urwid code where it finds full new server messages to handle.)
140 self.parser = Parser(self.game)
142 self.widget_manager = WidgetManager(self.socket, self.game)
143 self.server_output = []
144 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
145 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
147 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
149 def handle_input(self, trigger):
150 """On input from recv_loop thread, parse and enact commands.
152 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
153 data that a pipe defined by watch_pipe delivers. To avoid buffering
154 trouble, we don't care for that data beyond the fact that its receival
155 triggers this function: The sender is to write the data it wants to
156 deliver into the container referenced by self.server_output, and just
157 pipe the trigger to inform us about this.
159 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
160 parse it as a command, and enact it. In all cases but the 'BYE', calls
161 self.widget_manager.update.
163 msg = self.server_output[0]
165 raise urwid.ExitMainLoop()
167 command = self.parser.parse(msg)
169 self.game.log('UNHANDLED INPUT: ' + msg)
172 except ArgError as e:
173 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
174 self.widget_manager.update()
175 del self.server_output[0]
178 """Loop to receive messages from socket, deliver them to urwid thread.
180 Waits for self.server_output to become empty (this signals that the
181 input handler is finished / ready to receive new input), then writes
182 finished message from socket to self.server_output, then sends a single
183 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
186 for msg in plom_socket_io.recv(self.socket):
187 while len(self.server_output) > 0: # Wait until self.server_output
188 pass # is emptied by input handler.
189 self.server_output += [msg]
190 os.write(self.urwid_pipe_write_fd, b' ')
193 """Run in parallel urwid_loop and recv_loop threads."""
194 self.recv_loop_thread.start()
195 self.urwid_loop.run()
196 self.recv_loop_thread.join()
199 if __name__ == '__main__':
201 s = socket.create_connection(('127.0.0.1', 5000))
202 p = PlomRogueClient(game, s)