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."""
63 terrain_as_list = list(self.game.world.map_.terrain[:])
64 for t in self.game.world.things:
65 pos_i = self.game.world.map_.get_position_index(t.position)
66 terrain_as_list[pos_i] = self.game.symbol_for_type(t.type_)
68 #terrain = ''.join(terrain_as_list)
71 #while start_cut < len(terrain):
72 # limit = start_cut + self.game.world.map_.size[1]
73 # map_lines += [terrain[start_cut:limit]]
75 #return "\n".join(map_lines)
80 for c in terrain_as_list:
81 new_terrain_list += [c, ' ']
83 if x == self.game.world.map_.size[1]:
84 new_terrain_list += ['\n']
88 new_terrain_list += [' ']
89 return ''.join(new_terrain_list)
92 """Redraw all non-edit widgets."""
93 self.turn_widget.set_text('TURN: ' + str(self.game.world.turn))
94 self.log_widget.set_text(self.game.log_text)
95 self.map_widget.set_text(self.draw_map())
97 class EditToSocketWidget(urwid.Edit):
98 """Extends urwid.Edit with socket to send input on 'enter' to."""
100 def __init__(self, socket, *args, **kwargs):
101 super().__init__(*args, **kwargs)
104 def keypress(self, size, key):
105 """Extend super(): on Enter, send .edit_text, and empty it."""
107 return super().keypress(size, key)
108 plom_socket_io.send(self.socket, self.edit_text)
112 class PlomRogueClient:
114 def __init__(self, game, socket):
115 """Build client urwid interface around socket communication.
117 Sets up all widgets for writing to the socket and representing data
118 from it. Sending via a WidgetManager.EditToSocket widget is
119 straightforward; polling the socket for input from the server in
120 parallel to the urwid main loop not so much:
122 The urwid developers warn against sharing urwid resources among
123 threads, so having a socket polling thread for writing to an urwid
124 widget while other widgets are handled in other threads would be
125 dangerous. Urwid developers recommend using urwid's watch_pipe
126 mechanism instead: using a pipe from non-urwid threads into a single
127 urwid thread. We use self.recv_loop_thread to poll the socket, therein
128 write socket.recv output to an object that is then linked to by
129 self.server_output (which is known to the urwid thread), then use the
130 pipe to urwid to trigger it pulling new data from self.server_output to
131 handle via self.handle_input. (We *could* pipe socket.recv output
132 directly, but then we get complicated buffering situations here as well
133 as in the urwid code that receives the pipe output. It's easier to just
134 tell the urwid code where it finds full new server messages to handle.)
137 self.parser = Parser(self.game)
139 self.widget_manager = WidgetManager(self.socket, self.game)
140 self.server_output = []
141 self.urwid_loop = urwid.MainLoop(self.widget_manager.top)
142 self.urwid_pipe_write_fd = self.urwid_loop.watch_pipe(self.
144 self.recv_loop_thread = threading.Thread(target=self.recv_loop)
146 def handle_input(self, trigger):
147 """On input from recv_loop thread, parse and enact commands.
149 Serves as a receiver to urwid's watch_pipe mechanism, with trigger the
150 data that a pipe defined by watch_pipe delivers. To avoid buffering
151 trouble, we don't care for that data beyond the fact that its receival
152 triggers this function: The sender is to write the data it wants to
153 deliver into the container referenced by self.server_output, and just
154 pipe the trigger to inform us about this.
156 If the message delivered is 'BYE', quits Urwid. Otherwise tries to
157 parse it as a command, and enact it. In all cases but the 'BYE', calls
158 self.widget_manager.update.
160 msg = self.server_output[0]
162 raise urwid.ExitMainLoop()
164 command = self.parser.parse(msg)
166 self.game.log('UNHANDLED INPUT: ' + msg)
169 except ArgError as e:
170 self.game.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
171 self.widget_manager.update()
172 del self.server_output[0]
175 """Loop to receive messages from socket, deliver them to urwid thread.
177 Waits for self.server_output to become empty (this signals that the
178 input handler is finished / ready to receive new input), then writes
179 finished message from socket to self.server_output, then sends a single
180 b' ' through self.urwid_pipe_write_fd to trigger the input handler.
183 for msg in plom_socket_io.recv(self.socket):
184 while len(self.server_output) > 0: # Wait until self.server_output
185 pass # is emptied by input handler.
186 self.server_output += [msg]
187 os.write(self.urwid_pipe_write_fd, b' ')
190 """Run in parallel urwid_loop and recv_loop threads."""
191 self.recv_loop_thread.start()
192 self.urwid_loop.run()
193 self.recv_loop_thread.join()
196 if __name__ == '__main__':
198 s = socket.create_connection(('127.0.0.1', 5000))
199 p = PlomRogueClient(game, s)