6 from parser import ArgError, Parser
7 from game_common import World, CommonCommandsMixin
10 class Game(CommonCommandsMixin):
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_LAST_PLAYER_TASK_RESULT(self, msg):
28 self.log_text = msg + '\n' + self.log_text
29 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
31 def cmd_TURN_FINISHED(self, n):
32 """Do nothing. (This may be extended later.)"""
34 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
36 def cmd_NEW_TURN(self, n):
37 """Set self.turn to n, empty self.things."""
39 self.world.things = []
40 cmd_NEW_TURN.argtypes = 'int:nonneg'
42 def cmd_VISIBLE_MAP_LINE(self, y, terrain_line):
43 self.world.map_.set_line(y, terrain_line)
44 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
49 def __init__(self, socket, game):
50 """Set up all urwid widgets we want on the screen."""
52 edit_widget = self.EditToSocketWidget(socket, 'SEND: ')
53 self.map_widget = urwid.Text('', wrap='clip')
54 self.turn_widget = urwid.Text('')
55 self.log_widget = urwid.Text('')
56 map_box = urwid.Padding(self.map_widget, width=50)
57 widget_pile = urwid.Pile([edit_widget, map_box, self.turn_widget,
59 self.top = urwid.Filler(widget_pile, valign='top')
62 """Draw map view from .game.map_.terrain, .game.things."""
64 map_size = len(self.game.world.map_.terrain)
66 while start_cut < map_size:
67 limit = start_cut + self.game.world.map_.size[1]
68 map_lines += [self.game.world.map_.terrain[start_cut:limit]]
70 for t in self.game.world.things:
71 line_as_list = list(map_lines[t.position[0]])
72 line_as_list[t.position[1]] = self.game.symbol_for_type(t.type_)
73 map_lines[t.position[0]] = ''.join(line_as_list)
74 return "\n".join(map_lines)
77 """Redraw all non-edit widgets."""
78 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
79 self.log_widget.set_text(self.game.log_text)
80 self.map_widget.set_text(self.draw_map())
82 class EditToSocketWidget(urwid.Edit):
83 """Extends urwid.Edit with socket to send input on 'enter' to."""
85 def __init__(self, socket, *args, **kwargs):
86 super().__init__(*args, **kwargs)
89 def keypress(self, size, key):
90 """Extend super(): on Enter, send .edit_text, and empty it."""
92 return super().keypress(size, key)
93 plom_socket_io.send(self.socket, self.edit_text)
97 class PlomRogueClient:
99 def __init__(self, game, socket):
100 """Build client urwid interface around socket communication.
102 Sets up all widgets for writing to the socket and representing data
103 from it. Sending via a WidgetManager.EditToSocket widget is
104 straightforward; polling the socket for input from the server in
105 parallel to the urwid main loop not so much:
107 The urwid developers warn against sharing urwid resources among
108 threads, so having a socket polling thread for writing to an urwid
109 widget while other widgets are handled in other threads would be
110 dangerous. Urwid developers recommend using urwid's watch_pipe
111 mechanism instead: using a pipe from non-urwid threads into a single
112 urwid thread. We use self.recv_loop_thread to poll the socket, therein
113 write socket.recv output to an object that is then linked to by
114 self.server_output (which is known to the urwid thread), then use the
115 pipe to urwid to trigger it pulling new data from self.server_output to
116 handle via self.handle_input. (We *could* pipe socket.recv output
117 directly, but then we get complicated buffering situations here as well
118 as in the urwid code that receives the pipe output. It's easier to just
119 tell the urwid code where it finds full new server messages to handle.)
122 self.parser = Parser(self.game)
124 self.widget_manager = WidgetManager(self.socket, self.game)
125 self.server_output = []
126 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
127 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
129 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
131 def handle_input(self, trigger):
132 """On input from recv_loop thread, parse and enact commands.
134 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
135 data that a pipe defined by watch_pipe delivers. To avoid buffering
136 trouble, we don't care for that data beyond the fact that its receival
137 triggers this function: The sender is to write the data it wants to
138 deliver into the container referenced by self.server_output, and just
139 pipe the trigger to inform us about this.
141 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
142 parse it as a command, and enact it. In all cases but the 'BYE', calls
143 self.widget_manager.update.
145 msg = self.server_output[0]
147 raise urwid.ExitMainLoop()
149 command = self.parser.parse(msg)
151 self.game.log('UNHANDLED INPUT: ' + msg)
154 except ArgError as e:
155 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
156 self.widget_manager.update()
157 del self.server_output[0]
160 """Loop to receive messages from socket, deliver them to urwid thread.
162 Waits for self.server_output to become empty (this signals that the
163 input handler is finished / ready to receive new input), then writes
164 finished message from socket to self.server_output, then sends a single
165 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
168 for msg in plom_socket_io.recv(self.socket):
169 while len(self.server_output) > 0: # Wait until self.server_output
170 pass # is emptied by input handler.
171 self.server_output += [msg]
172 os.write(self.urwid_pipe_write_fd, b' ')
175 """Run in parallel urwid_loop and recv_loop threads."""
176 self.recv_loop_thread.start()
177 self.urwid_loop.run()
178 self.recv_loop_thread.join()
181 if __name__ == '__main__':
183 s = socket.create_connection(('127.0.0.1', 5000))
184 p = PlomRogueClient(game, s)