6 from parser import ArgError, Parser
7 from game_common import World
15 """Prefix msg plus newline to self.log_text."""
16 self.log_text = msg + '\n' + self.log_text
18 def symbol_for_type(self, type_):
22 elif type_ == 'monster':
26 def cmd_MAP_SIZE(self, yx):
27 """Set self.map_size to yx, redraw self.terrain_map as '?' cells."""
28 self.world.set_map_size(yx)
29 cmd_MAP_SIZE.argtypes = 'yx_tuple:nonneg'
31 def cmd_TERRAIN_LINE(self, y, terrain_line):
32 self.world.set_map_line(y, terrain_line)
33 cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'
35 def cmd_THING_TYPE(self, i, type_):
36 t = self.world.get_thing(i)
38 cmd_THING_TYPE.argtypes = 'int:nonneg string'
40 def cmd_THING_POS(self, i, yx):
41 t = self.world.get_thing(i)
43 cmd_THING_POS.argtypes = 'int:nonneg 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."""
53 self.world.things = []
54 cmd_NEW_TURN.argtypes = 'int:nonneg'
59 def __init__(self, socket, game):
60 """Set up all urwid widgets we want on the screen."""
62 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
63 self.map_widget = urwid.Text('', wrap='clip')
64 self.turn_widget = urwid.Text('')
65 self.log_widget = urwid.Text('')
66 map_box = urwid.Padding(self.map_widget, width=50)
67 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
69 self.top = urwid.Filler(widget_pile, valign='top')
72 """Draw map view from .game.terrain_map, .game.things."""
74 map_size = len(self.game.world.terrain_map)
76 while start_cut < map_size:
77 limit = start_cut + self.game.world.map_size[1]
78 map_lines += [self.game.world.terrain_map[start_cut:limit]]
80 for t in self.game.world.things:
81 line_as_list = list(map_lines[t.position[0]])
82 line_as_list[t.position[1]] = self.game.symbol_for_type(t.type_)
83 map_lines[t.position[0]] = ''.join(line_as_list)
84 return "\n".join(map_lines)
87 """Redraw all non-edit widgets."""
88 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
89 self.log_widget.set_text(self.game.log_text)
90 self.map_widget.set_text(self.draw_map())
92 class EditToSocketWidget(urwid.Edit):
93 """Extends urwid.Edit with socket to send input on 'enter' to."""
95 def __init__(self, socket, *args, **kwargs):
96 super().__init__(*args, **kwargs)
99 def keypress(self, size, key):
100 """Extend super(): on Enter, send .edit_text, and empty it."""
102 return super().keypress(size, key)
103 plom_socket_io.send(self.socket, self.edit_text)
107 class PlomRogueClient:
109 def __init__(self, game, socket):
110 """Build client urwid interface around socket communication.
112 Sets up all widgets for writing to the socket and representing data
113 from it. Sending via a WidgetManager.EditToSocket widget is
114 straightforward; polling the socket for input from the server in
115 parallel to the urwid main loop not so much:
117 The urwid developers warn against sharing urwid resources among
118 threads, so having a socket polling thread for writing to an urwid
119 widget while other widgets are handled in other threads would be
120 dangerous. Urwid developers recommend using urwid's watch_pipe
121 mechanism instead: using a pipe from non-urwid threads into a single
122 urwid thread. We use self.recv_loop_thread to poll the socket, therein
123 write socket.recv output to an object that is then linked to by
124 self.server_output (which is known to the urwid thread), then use the
125 pipe to urwid to trigger it pulling new data from self.server_output to
126 handle via self.handle_input. (We *could* pipe socket.recv output
127 directly, but then we get complicated buffering situations here as well
128 as in the urwid code that receives the pipe output. It's easier to just
129 tell the urwid code where it finds full new server messages to handle.)
132 self.parser = Parser(self.game)
134 self.widget_manager = WidgetManager(self.socket, self.game)
135 self.server_output = []
136 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
137 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
139 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
141 def handle_input(self, trigger):
142 """On input from recv_loop thread, parse and enact commands.
144 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
145 data that a pipe defined by watch_pipe delivers. To avoid buffering
146 trouble, we don't care for that data beyond the fact that its receival
147 triggers this function: The sender is to write the data it wants to
148 deliver into the container referenced by self.server_output, and just
149 pipe the trigger to inform us about this.
151 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
152 parse it as a command, and enact it. In all cases but the 'BYE', calls
153 self.widget_manager.update.
155 msg = self.server_output[0]
157 raise urwid.ExitMainLoop()
159 command = self.parser.parse(msg)
161 self.game.log('UNHANDLED INPUT: ' + msg)
164 except ArgError as e:
165 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
166 self.widget_manager.update()
167 del self.server_output[0]
170 """Loop to receive messages from socket, deliver them to urwid thread.
172 Waits for self.server_output to become empty (this signals that the
173 input handler is finished / ready to receive new input), then writes
174 finished message from socket to self.server_output, then sends a single
175 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
178 for msg in plom_socket_io.recv(self.socket):
179 while len(self.server_output) > 0: # Wait until self.server_output
180 pass # is emptied by input handler.
181 self.server_output += [msg]
182 os.write(self.urwid_pipe_write_fd, b' ')
185 """Run in parallel urwid_loop and recv_loop threads."""
186 self.recv_loop_thread.start()
187 self.urwid_loop.run()
188 self.recv_loop_thread.join()
191 if __name__ == '__main__':
193 s = socket.create_connection(('127.0.0.1', 5000))
194 p = PlomRogueClient(game, s)