6 from parser import ArgError, Parser
10 def get_map_class(geometry):
11 return globals()['Map' + geometry]
14 class MapSquare(game_common.Map):
16 def list_terrain_to_lines(self, terrain_as_list):
17 terrain = ''.join(terrain_as_list)
20 while start_cut < len(terrain):
21 limit = start_cut + self.game.world.map_.size[1]
22 map_lines += [terrain[start_cut:limit]]
24 return "\n".join(map_lines)
27 class MapHex(game_common.Map):
29 def list_terrain_to_lines(self, terrain_as_list):
33 for c in terrain_as_list:
34 new_terrain_list += [c, ' ']
37 new_terrain_list += ['\n']
41 new_terrain_list += [' ']
42 return ''.join(new_terrain_list)
45 class World(game_common.World):
47 def __init__(self, *args, **kwargs):
48 """Extend original with local classes and empty default map.
50 We need the empty default map because we draw the map widget
51 on any update, even before we actually receive map data.
53 super().__init__(*args, **kwargs)
54 self.get_map_class = get_map_class
55 self.map_ = self.get_map_class('Hex')()
58 class Game(game_common.CommonCommandsMixin):
63 """Prefix msg plus newline to self.log_text."""
64 self.log_text = msg + '\n' + self.log_text
66 def symbol_for_type(self, type_):
70 elif type_ == 'monster':
74 def cmd_LAST_PLAYER_TASK_RESULT(self, msg):
76 self.log_text = msg + '\n' + self.log_text
77 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
79 def cmd_TURN_FINISHED(self, n):
80 """Do nothing. (This may be extended later.)"""
82 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
84 def cmd_NEW_TURN(self, n):
85 """Set self.turn to n, empty self.things."""
87 self.world.things = []
88 cmd_NEW_TURN.argtypes = 'int:nonneg'
90 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
91 self.world.map_.set_line(y, terrain_line)
92 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
97 def __init__(self, socket, game):
98 """Set up all urwid widgets we want on the screen."""
100 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
101 self.map_widget = urwid.Text('', wrap='clip')
102 self.turn_widget = urwid.Text('')
103 self.log_widget = urwid.Text('')
104 map_box = urwid.Padding(self.map_widget, width=50)
105 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
107 self.top = urwid.Filler(widget_pile, valign='top')
110 """Draw map view from .game.map_.terrain, .game.things."""
111 terrain_as_list = list(self.game.world.map_.terrain[:])
112 for t in self.game.world.things:
113 pos_i = self.game.world.map_.get_position_index(t.position)
114 terrain_as_list[pos_i] = self.game.symbol_for_type(t.type_)
115 return self.game.world.map_.list_terrain_to_lines(terrain_as_list)
118 """Redraw all non-edit widgets."""
119 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
120 self.log_widget.set_text(self.game.log_text)
121 self.map_widget.set_text(self.draw_map())
123 class EditToSocketWidget(urwid.Edit):
124 """Extends urwid.Edit with socket to send input on 'enter' to."""
126 def __init__(self, socket, *args, **kwargs):
127 super().__init__(*args, **kwargs)
130 def keypress(self, size, key):
131 """Extend super(): on Enter, send .edit_text, and empty it."""
133 return super().keypress(size, key)
134 plom_socket_io.send(self.socket, self.edit_text)
138 class PlomRogueClient:
140 def __init__(self, game, socket):
141 """Build client urwid interface around socket communication.
143 Sets up all widgets for writing to the socket and representing data
144 from it. Sending via a WidgetManager.EditToSocket widget is
145 straightforward; polling the socket for input from the server in
146 parallel to the urwid main loop not so much:
148 The urwid developers warn against sharing urwid resources among
149 threads, so having a socket polling thread for writing to an urwid
150 widget while other widgets are handled in other threads would be
151 dangerous. Urwid developers recommend using urwid's watch_pipe
152 mechanism instead: using a pipe from non-urwid threads into a single
153 urwid thread. We use self.recv_loop_thread to poll the socket, therein
154 write socket.recv output to an object that is then linked to by
155 self.server_output (which is known to the urwid thread), then use the
156 pipe to urwid to trigger it pulling new data from self.server_output to
157 handle via self.handle_input. (We *could* pipe socket.recv output
158 directly, but then we get complicated buffering situations here as well
159 as in the urwid code that receives the pipe output. It's easier to just
160 tell the urwid code where it finds full new server messages to handle.)
163 self.parser = Parser(self.game)
165 self.widget_manager = WidgetManager(self.socket, self.game)
166 self.server_output = []
167 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
168 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
170 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
172 def handle_input(self, trigger):
173 """On input from recv_loop thread, parse and enact commands.
175 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
176 data that a pipe defined by watch_pipe delivers. To avoid buffering
177 trouble, we don't care for that data beyond the fact that its receival
178 triggers this function: The sender is to write the data it wants to
179 deliver into the container referenced by self.server_output, and just
180 pipe the trigger to inform us about this.
182 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
183 parse it as a command, and enact it. In all cases but the 'BYE', calls
184 self.widget_manager.update.
186 msg = self.server_output[0]
188 raise urwid.ExitMainLoop()
190 command = self.parser.parse(msg)
192 self.game.log('UNHANDLED INPUT: ' + msg)
195 except ArgError as e:
196 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
197 self.widget_manager.update()
198 del self.server_output[0]
201 """Loop to receive messages from socket, deliver them to urwid thread.
203 Waits for self.server_output to become empty (this signals that the
204 input handler is finished / ready to receive new input), then writes
205 finished message from socket to self.server_output, then sends a single
206 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
209 for msg in plom_socket_io.recv(self.socket):
210 while len(self.server_output) > 0: # Wait until self.server_output
211 pass # is emptied by input handler.
212 self.server_output += [msg]
213 os.write(self.urwid_pipe_write_fd, b' ')
216 """Run in parallel urwid_loop and recv_loop threads."""
217 self.recv_loop_thread.start()
218 self.urwid_loop.run()
219 self.recv_loop_thread.join()
222 if __name__ == '__main__':
224 s = socket.create_connection(('127.0.0.1', 5000))
225 p = PlomRogueClient(game, s)