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]]
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).split('\n')
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 = self.MapWidget()
104 self.turn_widget = urwid.Text('')
105 self.log_widget = urwid.Text('')
106 edit_map = urwid.AttrMap(edit_widget, 'foo')
107 turn_map = urwid.AttrMap(self.turn_widget, 'bar')
108 log_map = urwid.AttrMap(self.log_widget, 'baz')
109 widget_pile = urwid.Pile([('pack', edit_map),
110 ('pack', urwid.Divider()),
112 ('pack', urwid.Divider()),
114 urwid.SolidFill(fill_char=' ')])
115 self.top = urwid.Columns([(20, widget_pile), self.map_widget],
117 self.palette = [('foo', 'white', 'dark red'),
118 ('bar', 'white', 'dark blue'),
119 ('baz', 'white', 'dark green')]
122 """Draw map view from .game.map_.terrain, .game.things."""
123 terrain_as_list = list(self.game.world.map_.terrain[:])
124 for t in self.game.world.things:
125 pos_i = self.game.world.map_.get_position_index(t.position)
126 terrain_as_list[pos_i] = self.game.symbol_for_type(t.type_)
127 return self.game.world.map_.list_terrain_to_lines(terrain_as_list)
128 #text = self.game.world.map_.list_terrain_to_lines(terrain_as_list)
132 # new_map_text += [('foo', char)]
133 # elif char in {'x', 'X', '#'}:
134 # new_map_text += [('bar', char)]
135 # elif char in {'@', 'm'}:
136 # new_map_text += [('baz', char)]
138 # new_map_text += [char]
142 """Redraw all non-edit widgets."""
143 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
144 self.log_widget.set_text(self.game.log_text)
145 self.map_widget.text = self.draw_map()
146 self.map_widget._invalidate()
148 class EditToSocketWidget(urwid.Edit):
149 """Extends urwid.Edit with socket to send input on 'enter' to."""
151 def __init__(self, socket, *args, **kwargs):
152 super().__init__(*args, **kwargs)
155 def keypress(self, size, key):
156 """Extend super(): on Enter, send .edit_text, and empty it."""
158 return super().keypress(size, key)
159 plom_socket_io.send(self.socket, self.edit_text)
162 class MapWidget(urwid.Widget):
163 _sizing = frozenset(['box'])
166 def render(self, size, focus=False):
167 maxcol, maxrow = size
169 for y in range(len(self.text)):
172 if len(line) < maxcol:
173 line = line + '0' * (maxcol - len(line))
176 content += [line.encode('utf-8')]
177 padding_y = maxrow - len(content)
179 for y in range(padding_y):
180 content += ['0'.encode('utf-8') * maxcol]
181 return urwid.TextCanvas(content)
184 class PlomRogueClient:
186 def __init__(self, game, socket):
187 """Build client urwid interface around socket communication.
189 Sets up all widgets for writing to the socket and representing data
190 from it. Sending via a WidgetManager.EditToSocket widget is
191 straightforward; polling the socket for input from the server in
192 parallel to the urwid main loop not so much:
194 The urwid developers warn against sharing urwid resources among
195 threads, so having a socket polling thread for writing to an urwid
196 widget while other widgets are handled in other threads would be
197 dangerous. Urwid developers recommend using urwid's watch_pipe
198 mechanism instead: using a pipe from non-urwid threads into a single
199 urwid thread. We use self.recv_loop_thread to poll the socket, therein
200 write socket.recv output to an object that is then linked to by
201 self.server_output (which is known to the urwid thread), then use the
202 pipe to urwid to trigger it pulling new data from self.server_output to
203 handle via self.handle_input. (We *could* pipe socket.recv output
204 directly, but then we get complicated buffering situations here as well
205 as in the urwid code that receives the pipe output. It's easier to just
206 tell the urwid code where it finds full new server messages to handle.)
209 self.parser = Parser(self.game)
211 self.widget_manager = WidgetManager(self.socket, self.game)
212 self.server_output = []
213 self.urwid_loop = urwid.MainLoop(self.widget_manager.top,
214 self.widget_manager.palette)
215 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
217 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
219 def handle_input(self, trigger):
220 """On input from recv_loop thread, parse and enact commands.
222 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
223 data that a pipe defined by watch_pipe delivers. To avoid buffering
224 trouble, we don't care for that data beyond the fact that its receival
225 triggers this function: The sender is to write the data it wants to
226 deliver into the container referenced by self.server_output, and just
227 pipe the trigger to inform us about this.
229 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
230 parse it as a command, and enact it. In all cases but the 'BYE', calls
231 self.widget_manager.update.
233 msg = self.server_output[0]
235 raise urwid.ExitMainLoop()
237 command = self.parser.parse(msg)
239 self.game.log('UNHANDLED INPUT: ' + msg)
242 except ArgError as e:
243 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
244 self.widget_manager.update()
245 del self.server_output[0]
248 """Loop to receive messages from socket, deliver them to urwid thread.
250 Waits for self.server_output to become empty (this signals that the
251 input handler is finished / ready to receive new input), then writes
252 finished message from socket to self.server_output, then sends a single
253 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
256 for msg in plom_socket_io.recv(self.socket):
257 while len(self.server_output) > 0: # Wait until self.server_output
258 pass # is emptied by input handler.
259 self.server_output += [msg]
260 os.write(self.urwid_pipe_write_fd, b' ')
263 """Run in parallel urwid_loop and recv_loop threads."""
264 self.recv_loop_thread.start()
265 self.urwid_loop.run()
266 self.recv_loop_thread.join()
269 if __name__ == '__main__':
271 s = socket.create_connection(('127.0.0.1', 5000))
272 p = PlomRogueClient(game, s)