6 from parser import ArgError, Parser
13 terrain_map = ('?'*5)*5
17 def __init__(self, position, symbol):
18 self.position = position
22 """Prefix msg plus newline to self.log_text."""
23 self.log_text = msg + '\n' + self.log_text
25 def cmd_THING(self, type_, yx):
26 """Add to self.things at .position yx with .symbol defined by type_."""
28 if type_ == 'TYPE:human':
30 elif type_ == 'TYPE:monster':
32 self.things += [self.Thing(yx, symbol)]
33 cmd_THING.argtypes = 'string yx_tuple:nonneg'
35 def cmd_MAP_SIZE(self, yx):
36 """Set self.map_size to yx, redraw self.terrain_map as '?' cells."""
38 self.map_size = (y, x)
40 for y in range(self.map_size[0]):
41 self.terrain_map += '?' * self.map_size[1]# + '\n'
42 self.terrain_map = self.terrain_map[:-1]
43 cmd_MAP_SIZE.argtypes = 'yx_tuple:nonneg'
45 def cmd_TURN_FINISHED(self, n):
46 """Do nothing. (This may be extended later.)"""
48 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
50 def cmd_NEW_TURN(self, n):
51 """Set self.turn to n, empty self.things."""
54 cmd_NEW_TURN.argtypes = 'int:nonneg'
56 def cmd_TERRAIN_LINE(self, y, terrain_line):
57 width_map = self.map_size[1]
58 if y >= self.map_size[0]:
59 raise ArgError('too large row number %s' % y)
60 width_line = len(terrain_line)
61 if width_line > width_map:
62 raise ArgError('too large map line width %s' % width_line)
63 self.terrain_map = self.terrain_map[:y * width_map] + \
64 terrain_line + self.terrain_map[(y + 1) * width_map:]
65 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
70 def __init__(self, socket, game):
71 """Set up all urwid widgets we want on the screen."""
73 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
74 self.map_widget = urwid.Text('', wrap='clip')
75 self.turn_widget = urwid.Text('')
76 self.log_widget = urwid.Text('')
77 map_box = urwid.Padding(self.map_widget, width=50)
78 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
80 self.top = urwid.Filler(widget_pile, valign='top')
83 """Draw map view from .game.terrain_map, .game.things."""
85 map_size = len(self.game.terrain_map)
87 while start_cut < map_size:
88 limit = start_cut + self.game.map_size[1]
89 map_lines += [self.game.terrain_map[start_cut:limit]]
91 for t in self.game.things:
92 line_as_list = list(map_lines[t.position[0]])
93 line_as_list[t.position[1]] = t.symbol
94 map_lines[t.position[0]] = ''.join(line_as_list)
95 return "\n".join(map_lines)
98 """Redraw all non-edit widgets."""
99 self.turn_widget.set_text('TURN: ' + str(self.game.turn))
100 self.log_widget.set_text(self.game.log_text)
101 self.map_widget.set_text(self.draw_map())
103 class EditToSocketWidget(urwid.Edit):
104 """Extends urwid.Edit with socket to send input on 'enter' to."""
106 def __init__(self, socket, *args, **kwargs):
107 super().__init__(*args, **kwargs)
110 def keypress(self, size, key):
111 """Extend super(): on Enter, send .edit_text, and empty it."""
113 return super().keypress(size, key)
114 plom_socket_io.send(self.socket, self.edit_text)
118 class PlomRogueClient:
120 def __init__(self, game, socket):
121 """Build client urwid interface around socket communication.
123 Sets up all widgets for writing to the socket and representing data
124 from it. Sending via a WidgetManager.EditToSocket widget is
125 straightforward; polling the socket for input from the server in
126 parallel to the urwid main loop not so much:
128 The urwid developers warn against sharing urwid resources among
129 threads, so having a socket polling thread for writing to an urwid
130 widget while other widgets are handled in other threads would be
131 dangerous. Urwid developers recommend using urwid's watch_pipe
132 mechanism instead: using a pipe from non-urwid threads into a single
133 urwid thread. We use self.recv_loop_thread to poll the socket, therein
134 write socket.recv output to an object that is then linked to by
135 self.server_output (which is known to the urwid thread), then use the
136 pipe to urwid to trigger it pulling new data from self.server_output to
137 handle via self.handle_input. (We *could* pipe socket.recv output
138 directly, but then we get complicated buffering situations here as well
139 as in the urwid code that receives the pipe output. It's easier to just
140 tell the urwid code where it finds full new server messages to handle.)
143 self.parser = Parser(self.game)
145 self.widget_manager = WidgetManager(self.socket, self.game)
146 self.server_output = []
147 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
148 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
150 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
152 def handle_input(self, trigger):
153 """On input from recv_loop thread, parse and enact commands.
155 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
156 data that a pipe defined by watch_pipe delivers. To avoid buffering
157 trouble, we don't care for that data beyond the fact that its receival
158 triggers this function: The sender is to write the data it wants to
159 deliver into the container referenced by self.server_output, and just
160 pipe the trigger to inform us about this.
162 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
163 parse it as a command, and enact it. In all cases but the 'BYE', calls
164 self.widget_manager.update.
166 msg = self.server_output[0]
168 raise urwid.ExitMainLoop()
170 command = self.parser.parse(msg)
172 self.game.log('UNHANDLED INPUT: ' + msg)
175 except ArgError as e:
176 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
177 self.widget_manager.update()
178 del self.server_output[0]
181 """Loop to receive messages from socket, deliver them to urwid thread.
183 Waits for self.server_output to become empty (this signals that the
184 input handler is finished / ready to receive new input), then writes
185 finished message from socket to self.server_output, then sends a single
186 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
189 for msg in plom_socket_io.recv(self.socket):
190 while len(self.server_output) > 0: # Wait until self.server_output
191 pass # is emptied by input handler.
192 self.server_output += [msg]
193 os.write(self.urwid_pipe_write_fd, b' ')
196 """Run in parallel urwid_loop and recv_loop threads."""
197 self.recv_loop_thread.start()
198 self.urwid_loop.run()
199 self.recv_loop_thread.join()
202 if __name__ == '__main__':
204 s = socket.create_connection(('127.0.0.1', 5000))
205 p = PlomRogueClient(game, s)