6 from parser import ArgError, Parser
10 class MapSquare(game_common.Map):
12 def list_terrain_to_lines(self, terrain_as_list):
13 terrain = ''.join(terrain_as_list)
16 while start_cut < len(terrain):
17 limit = start_cut + self.size[1]
18 map_lines += [terrain[start_cut:limit]]
20 return "\n".join(map_lines)
23 class MapHex(game_common.Map):
25 def list_terrain_to_lines(self, terrain_as_list):
26 new_terrain_list = [' ']
29 for c in terrain_as_list:
30 new_terrain_list += [c, ' ']
33 new_terrain_list += ['\n']
37 new_terrain_list += [' ']
38 return ''.join(new_terrain_list)
41 map_manager = game_common.MapManager(globals())
44 class World(game_common.World):
46 def __init__(self, game, *args, **kwargs):
47 """Extend original with local classes and empty default map.
49 We need the empty default map because we draw the map widget
50 on any update, even before we actually receive map data.
52 super().__init__(*args, **kwargs)
54 self.map_ = self.game.map_manager.get_map_class('Hex')()
57 class Game(game_common.CommonCommandsMixin):
60 self.map_manager = map_manager
61 self.world = World(self)
65 """Prefix msg plus newline to self.log_text."""
66 self.log_text = msg + '\n' + self.log_text
68 def symbol_for_type(self, type_):
72 elif type_ == 'monster':
76 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
78 self.log_text = msg + '\n' + self.log_text
79 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
81 def cmd_TURN_FINISHED(self, n):
82 """Do nothing. (This may be extended later.)"""
84 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
86 def cmd_NEW_TURN(self, n):
87 """Set self.turn to n, empty self.things."""
89 self.world.things = []
90 cmd_NEW_TURN.argtypes = 'int:nonneg'
92 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
93 self.world.map_.set_line(y, terrain_line)
94 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
99 def __init__(self, socket, game):
100 """Set up all urwid widgets we want on the screen."""
102 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
103 self.map_widget = urwid.Text('', wrap='clip')
104 self.turn_widget = urwid.Text('')
105 self.log_widget = urwid.Text('')
107 edit_map = urwid.AttrMap(edit_widget, 'foo')
108 turn_map = urwid.AttrMap(self.turn_widget, 'bar')
109 log_map = urwid.AttrMap(self.log_widget, 'baz')
110 widget_pile = urwid.Pile([edit_map,
115 widget_columns = urwid.Columns([(20, widget_pile), self.map_widget],
118 self.top = urwid.Filler(widget_columns, valign='top')
121 """Draw map view from .game.map_.terrain, .game.things."""
122 terrain_as_list = list(self.game.world.map_.terrain[:])
123 for t in self.game.world.things:
124 pos_i = self.game.world.map_.get_position_index(t.position)
125 terrain_as_list[pos_i] = self.game.symbol_for_type(t.type_)
126 return self.game.world.map_.list_terrain_to_lines(terrain_as_list)
129 """Redraw all non-edit widgets."""
130 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
131 self.log_widget.set_text(self.game.log_text)
132 map_lines = self.draw_map()
134 for char in map_lines:
136 new_map_text += [('foo', char)]
137 elif char in {'x', 'X', '#'}:
138 new_map_text += [('bar', char)]
139 elif char in {'@', 'm'}:
140 new_map_text += [('baz', char)]
142 new_map_text += [char]
143 self.map_widget.set_text(new_map_text)
145 class EditToSocketWidget(urwid.Edit):
146 """Extends urwid.Edit with socket to send input on 'enter' to."""
148 def __init__(self, socket, *args, **kwargs):
149 super().__init__(*args, **kwargs)
152 def keypress(self, size, key):
153 """Extend super(): on Enter, send .edit_text, and empty it."""
155 return super().keypress(size, key)
156 plom_socket_io.send(self.socket, self.edit_text)
160 class PlomRogueClient:
162 def __init__(self, game, socket):
163 """Build client urwid interface around socket communication.
165 Sets up all widgets for writing to the socket and representing data
166 from it. Sending via a WidgetManager.EditToSocket widget is
167 straightforward; polling the socket for input from the server in
168 parallel to the urwid main loop not so much:
170 The urwid developers warn against sharing urwid resources among
171 threads, so having a socket polling thread for writing to an urwid
172 widget while other widgets are handled in other threads would be
173 dangerous. Urwid developers recommend using urwid's watch_pipe
174 mechanism instead: using a pipe from non-urwid threads into a single
175 urwid thread. We use self.recv_loop_thread to poll the socket, therein
176 write socket.recv output to an object that is then linked to by
177 self.server_output (which is known to the urwid thread), then use the
178 pipe to urwid to trigger it pulling new data from self.server_output to
179 handle via self.handle_input. (We *could* pipe socket.recv output
180 directly, but then we get complicated buffering situations here as well
181 as in the urwid code that receives the pipe output. It's easier to just
182 tell the urwid code where it finds full new server messages to handle.)
185 self.parser = Parser(self.game)
187 self.widget_manager = WidgetManager(self.socket, self.game)
188 self.server_output = []
189 palette = [('foo', 'white', 'dark red'),
190 ('bar', 'white', 'dark blue'),
191 ('baz', 'white', 'dark green')]
192 self.urwid_loop = urwid.MainLoop(self.widget_manager.top, palette)
193 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
195 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
197 def handle_input(self, trigger):
198 """On input from recv_loop thread, parse and enact commands.
200 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
201 data that a pipe defined by watch_pipe delivers. To avoid buffering
202 trouble, we don't care for that data beyond the fact that its receival
203 triggers this function: The sender is to write the data it wants to
204 deliver into the container referenced by self.server_output, and just
205 pipe the trigger to inform us about this.
207 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
208 parse it as a command, and enact it. In all cases but the 'BYE', calls
209 self.widget_manager.update.
211 msg = self.server_output[0]
213 raise urwid.ExitMainLoop()
215 command = self.parser.parse(msg)
217 self.game.log('UNHANDLED INPUT: ' + msg)
220 except ArgError as e:
221 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
222 self.widget_manager.update()
223 del self.server_output[0]
226 """Loop to receive messages from socket, deliver them to urwid thread.
228 Waits for self.server_output to become empty (this signals that the
229 input handler is finished / ready to receive new input), then writes
230 finished message from socket to self.server_output, then sends a single
231 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
234 for msg in plom_socket_io.recv(self.socket):
235 while len(self.server_output) > 0: # Wait until self.server_output
236 pass # is emptied by input handler.
237 self.server_output += [msg]
238 os.write(self.urwid_pipe_write_fd, b' ')
241 """Run in parallel urwid_loop and recv_loop threads."""
242 self.recv_loop_thread.start()
243 self.urwid_loop.run()
244 self.recv_loop_thread.join()
247 if __name__ == '__main__':
249 s = socket.create_connection(('127.0.0.1', 5000))
250 p = PlomRogueClient(game, s)