6 from parser import ArgError, Parser
17 def __init__(self, id_, position, symbol):
19 self.position = position
23 """Prefix msg plus newline to self.log_text."""
24 self.log_text = msg + '\n' + self.log_text
26 def get_thing(self, i):
27 for thing in self.things:
30 t = self.Thing(i, [0,0], '?')
34 def cmd_THING_TYPE(self, i, type_):
39 elif type_ == 'monster':
42 cmd_THING_TYPE.argtypes = 'int:nonneg string'
44 def cmd_THING_POS(self, i, yx):
47 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
49 def cmd_THING_POS(self, i, yx):
52 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
54 def cmd_MAP_SIZE(self, yx):
55 """Set self.map_size to yx, redraw self.terrain_map as '?' cells."""
57 self.map_size = (y, x)
59 for y in range(self.map_size[0]):
60 self.terrain_map += '?' * self.map_size[1]
61 cmd_MAP_SIZE.argtypes = 'yx_tuple:nonneg'
63 def cmd_TURN_FINISHED(self, n):
64 """Do nothing. (This may be extended later.)"""
66 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
68 def cmd_NEW_TURN(self, n):
69 """Set self.turn to n, empty self.things."""
72 cmd_NEW_TURN.argtypes = 'int:nonneg'
74 def cmd_TERRAIN_LINE(self, y, terrain_line):
75 width_map = self.map_size[1]
76 if y >= self.map_size[0]:
77 raise ArgError('too large row number %s' % y)
78 width_line = len(terrain_line)
79 if width_line > width_map:
80 raise ArgError('too large map line width %s' % width_line)
81 self.terrain_map = self.terrain_map[:y * width_map] + \
82 terrain_line + self.terrain_map[(y + 1) * width_map:]
83 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
88 def __init__(self, socket, game):
89 """Set up all urwid widgets we want on the screen."""
91 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
92 self.map_widget = urwid.Text('', wrap='clip')
93 self.turn_widget = urwid.Text('')
94 self.log_widget = urwid.Text('')
95 map_box = urwid.Padding(self.map_widget, width=50)
96 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
98 self.top = urwid.Filler(widget_pile, valign='top')
101 """Draw map view from .game.terrain_map, .game.things."""
103 map_size = len(self.game.terrain_map)
105 while start_cut < map_size:
106 limit = start_cut + self.game.map_size[1]
107 map_lines += [self.game.terrain_map[start_cut:limit]]
109 for t in self.game.things:
110 line_as_list = list(map_lines[t.position[0]])
111 line_as_list[t.position[1]] = t.symbol
112 map_lines[t.position[0]] = ''.join(line_as_list)
113 return "\n".join(map_lines)
116 """Redraw all non-edit widgets."""
117 self.turn_widget.set_text('TURN: ' + str(self.game.turn))
118 self.log_widget.set_text(self.game.log_text)
119 self.map_widget.set_text(self.draw_map())
121 class EditToSocketWidget(urwid.Edit):
122 """Extends urwid.Edit with socket to send input on 'enter' to."""
124 def __init__(self, socket, *args, **kwargs):
125 super().__init__(*args, **kwargs)
128 def keypress(self, size, key):
129 """Extend super(): on Enter, send .edit_text, and empty it."""
131 return super().keypress(size, key)
132 plom_socket_io.send(self.socket, self.edit_text)
136 class PlomRogueClient:
138 def __init__(self, game, socket):
139 """Build client urwid interface around socket communication.
141 Sets up all widgets for writing to the socket and representing data
142 from it. Sending via a WidgetManager.EditToSocket widget is
143 straightforward; polling the socket for input from the server in
144 parallel to the urwid main loop not so much:
146 The urwid developers warn against sharing urwid resources among
147 threads, so having a socket polling thread for writing to an urwid
148 widget while other widgets are handled in other threads would be
149 dangerous. Urwid developers recommend using urwid's watch_pipe
150 mechanism instead: using a pipe from non-urwid threads into a single
151 urwid thread. We use self.recv_loop_thread to poll the socket, therein
152 write socket.recv output to an object that is then linked to by
153 self.server_output (which is known to the urwid thread), then use the
154 pipe to urwid to trigger it pulling new data from self.server_output to
155 handle via self.handle_input. (We *could* pipe socket.recv output
156 directly, but then we get complicated buffering situations here as well
157 as in the urwid code that receives the pipe output. It's easier to just
158 tell the urwid code where it finds full new server messages to handle.)
161 self.parser = Parser(self.game)
163 self.widget_manager = WidgetManager(self.socket, self.game)
164 self.server_output = []
165 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
166 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
168 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
170 def handle_input(self, trigger):
171 """On input from recv_loop thread, parse and enact commands.
173 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
174 data that a pipe defined by watch_pipe delivers. To avoid buffering
175 trouble, we don't care for that data beyond the fact that its receival
176 triggers this function: The sender is to write the data it wants to
177 deliver into the container referenced by self.server_output, and just
178 pipe the trigger to inform us about this.
180 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
181 parse it as a command, and enact it. In all cases but the 'BYE', calls
182 self.widget_manager.update.
184 msg = self.server_output[0]
186 raise urwid.ExitMainLoop()
188 command = self.parser.parse(msg)
190 self.game.log('UNHANDLED INPUT: ' + msg)
193 except ArgError as e:
194 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
195 self.widget_manager.update()
196 del self.server_output[0]
199 """Loop to receive messages from socket, deliver them to urwid thread.
201 Waits for self.server_output to become empty (this signals that the
202 input handler is finished / ready to receive new input), then writes
203 finished message from socket to self.server_output, then sends a single
204 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
207 for msg in plom_socket_io.recv(self.socket):
208 while len(self.server_output) > 0: # Wait until self.server_output
209 pass # is emptied by input handler.
210 self.server_output += [msg]
211 os.write(self.urwid_pipe_write_fd, b' ')
214 """Run in parallel urwid_loop and recv_loop threads."""
215 self.recv_loop_thread.start()
216 self.urwid_loop.run()
217 self.recv_loop_thread.join()
220 if __name__ == '__main__':
222 s = socket.create_connection(('127.0.0.1', 5000))
223 p = PlomRogueClient(game, s)