home · contact · privacy
Add ever-decreasing health to animate things, and death.
[plomrogue2-experiments] / new / example_client.py
1 #!/usr/bin/env python3
2 import curses
3 import socket
4 import threading
5 from plomrogue.parser import ArgError, Parser
6 from plomrogue.commands import (cmd_MAP, cmd_THING_POS, cmd_PLAYER_ID,
7                                 cmd_THING_HEALTH)
8 from plomrogue.game import Game, WorldBase
9 from plomrogue.mapping import MapHex
10 from plomrogue.io import PlomSocket
11 from plomrogue.things import ThingBase
12 import types
13 import queue
14
15
16 class ClientMap(MapHex):
17
18     def y_cut(self, map_lines, center_y, view_height):
19         map_height = len(map_lines)
20         if map_height > view_height and center_y > view_height / 2:
21             if center_y > map_height - view_height / 2:
22                 map_lines[:] = map_lines[map_height - view_height:]
23             else:
24                 start = center_y - int(view_height / 2) - 1
25                 map_lines[:] = map_lines[start:start + view_height]
26
27     def x_cut(self, map_lines, center_x, view_width, map_width):
28         if map_width > view_width and center_x > view_width / 2:
29             if center_x > map_width - view_width / 2:
30                 cut_start = map_width - view_width
31                 cut_end = None
32             else:
33                 cut_start = center_x - int(view_width / 2)
34                 cut_end = cut_start + view_width
35             map_lines[:] = [line[cut_start:cut_end] for line in map_lines]
36
37     def format_to_view(self, map_cells, center, size):
38
39         def map_cells_to_lines(map_cells):
40             map_view_chars = ['0']
41             x = 0
42             y = 0
43             for cell in map_cells:
44                 if type(cell) == str:
45                     map_view_chars += [cell, ' ']
46                 else:
47                     map_view_chars += [cell[0], cell[1]]
48                 x += 1
49                 if x == self.size[1]:
50                     map_view_chars += ['\n']
51                     x = 0
52                     y += 1
53                     if y % 2 == 0:
54                         map_view_chars += ['0']
55             if y % 2 == 0:
56                 map_view_chars = map_view_chars[:-1]
57             map_view_chars = map_view_chars[:-1]
58             return ''.join(map_view_chars).split('\n')
59
60         map_lines = map_cells_to_lines(map_cells)
61         self.y_cut(map_lines, center[0], size[0])
62         map_width = self.size[1] * 2 + 1
63         self.x_cut(map_lines, center[1] * 2, size[1], map_width)
64         return map_lines
65
66
67 class World(WorldBase):
68
69     def __init__(self, *args, **kwargs):
70         """Extend original with local classes and empty default map.
71
72         We need the empty default map because we draw the map widget
73         on any update, even before we actually receive map data.
74         """
75         super().__init__(*args, **kwargs)
76         self.map_ = ClientMap()
77         self.player_inventory = []
78         self.player_id = 0
79         self.pickable_items = []
80
81     def new_map(self, yx):
82         self.map_ = ClientMap(yx)
83
84     @property
85     def player(self):
86         return self.get_thing(self.player_id)
87
88
89 def cmd_LAST_PLAYER_TASK_RESULT(game, msg):
90     if msg != "success":
91         game.log(msg)
92 cmd_LAST_PLAYER_TASK_RESULT.argtypes = 'string'
93
94
95 def cmd_TURN_FINISHED(game, n):
96     """Do nothing. (This may be extended later.)"""
97     pass
98 cmd_TURN_FINISHED.argtypes = 'int:nonneg'
99
100
101 def cmd_TURN(game, n):
102     """Set game.turn to n, empty game.things."""
103     game.world.turn = n
104     game.world.things = []
105     game.world.pickable_items = []
106 cmd_TURN.argtypes = 'int:nonneg'
107
108
109 def cmd_VISIBLE_MAP_LINE(game, y, terrain_line):
110     game.world.map_.set_line(y, terrain_line)
111 cmd_VISIBLE_MAP_LINE.argtypes = 'int:nonneg string'
112
113
114 def cmd_GAME_STATE_COMPLETE(game):
115     game.tui.to_update['turn'] = True
116     game.tui.to_update['map'] = True
117     game.tui.to_update['inventory'] = True
118
119
120 def cmd_THING_TYPE(game, i, type_):
121     t = game.world.get_thing(i)
122     t.type_ = type_
123 cmd_THING_TYPE.argtypes = 'int:nonneg string'
124
125
126 def cmd_PLAYER_INVENTORY(game, ids):
127     game.world.player_inventory = ids  # TODO: test whether valid IDs
128 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
129
130
131 def cmd_PICKABLE_ITEMS(game, ids):
132     game.world.pickable_items = ids
133     game.tui.to_update['pickable_items'] = True
134 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
135
136
137 class Game:
138
139     def __init__(self):
140         self.parser = Parser(self)
141         self.world = World(self)
142         self.thing_type = ThingBase
143         self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
144                          'TURN_FINISHED': cmd_TURN_FINISHED,
145                          'TURN': cmd_TURN,
146                          'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
147                          'PLAYER_ID': cmd_PLAYER_ID,
148                          'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
149                          'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
150                          'MAP': cmd_MAP,
151                          'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
152                          'THING_TYPE': cmd_THING_TYPE,
153                          'THING_HEALTH': cmd_THING_HEALTH,
154                          'THING_POS': cmd_THING_POS}
155         self.log_text = ''
156         self.do_quit = False
157         self.tui = None
158
159     def get_command(self, command_name):
160         from functools import partial
161         if command_name in self.commands:
162             f = partial(self.commands[command_name], self)
163             if hasattr(self.commands[command_name], 'argtypes'):
164                 f.argtypes = self.commands[command_name].argtypes
165             return f
166         return None
167
168     def get_string_options(self, string_option_type):
169         return None
170
171     def handle_input(self, msg):
172         self.log(msg)
173         if msg == 'BYE':
174             self.do_quit = True
175             return
176         try:
177             command, args = self.parser.parse(msg)
178             if command is None:
179                 self.log('UNHANDLED INPUT: ' + msg)
180             else:
181                 command(*args)
182         except ArgError as e:
183             self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
184
185     def log(self, msg):
186         """Prefix msg plus newline to self.log_text."""
187         self.log_text = msg + '\n' + self.log_text
188         self.tui.to_update['log'] = True
189
190     def symbol_for_type(self, type_):
191         symbol = '?'
192         if type_ == 'human':
193             symbol = '@'
194         elif type_ == 'monster':
195             symbol = 'm'
196         elif type_ == 'item':
197             symbol = 'i'
198         return symbol
199
200
201 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
202                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
203
204
205 def recv_loop(plom_socket, game, q):
206     for msg in plom_socket.recv():
207         q.put(msg)
208
209
210 class Widget:
211
212     def __init__(self, tui, start, size, check_updates=[], visible=True):
213         self.check_updates = check_updates
214         self.tui = tui
215         self.start = start
216         self.win = curses.newwin(1, 1, self.start[0], self.start[1])
217         self.size_def = size  # store for re-calling .size on SIGWINCH
218         self.size = size
219         self.do_update = True
220         self.visible = visible
221         self.children = []
222
223     @property
224     def size(self):
225         return self.win.getmaxyx()
226
227     @size.setter
228     def size(self, size):
229         """Set window size. Size be y,x tuple. If y or x None, use legal max."""
230         n_lines, n_cols = size
231         if n_lines is None:
232             n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
233         if n_cols is None:
234             n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
235         self.win.resize(n_lines, n_cols)
236
237     def __len__(self):
238         return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
239
240     def safe_write(self, foo):
241
242         def to_chars_with_attrs(part):
243             attr = curses.A_NORMAL
244             part_string = part
245             if not type(part) == str:
246                 part_string = part[0]
247                 attr = part[1]
248             return [(char, attr) for char in part_string]
249
250         chars_with_attrs = []
251         if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
252             chars_with_attrs += to_chars_with_attrs(foo)
253         else:
254             for part in foo:
255                 chars_with_attrs += to_chars_with_attrs(part)
256         self.win.move(0, 0)
257         if len(chars_with_attrs) < len(self):
258             for char_with_attr in chars_with_attrs:
259                 self.win.addstr(char_with_attr[0], char_with_attr[1])
260         else:  # workaround to <https://stackoverflow.com/q/7063128>
261             cut = chars_with_attrs[:len(self) - 1]
262             last_char_with_attr = chars_with_attrs[len(self) - 1]
263             self.win.addstr(self.size[0] - 1, self.size[1] - 2,
264                             last_char_with_attr[0], last_char_with_attr[1])
265             self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
266             self.win.move(0, 0)
267             for char_with_attr in cut:
268                 self.win.addstr(char_with_attr[0], char_with_attr[1])
269
270     def ensure_freshness(self, do_refresh=False):
271         did_refresh = False
272         if self.visible:
273             if not do_refresh:
274                 for key in self.check_updates:
275                     if key in self.tui.to_update and self.tui.to_update[key]:
276                         do_refresh = True
277                         break
278             if do_refresh:
279                 self.win.erase()
280                 self.draw()
281                 self.win.refresh()
282                 did_refresh = True
283             for child in self.children:
284                 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
285         return did_refresh
286
287
288 class EditWidget(Widget):
289
290     def draw(self):
291         self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
292
293
294 class TextLinesWidget(Widget):
295
296     def draw(self):
297         lines = self.get_text_lines()
298         line_width = self.size[1]
299         to_join = []
300         for line in lines:
301             to_pad = line_width - (len(line) % line_width)
302             if to_pad == line_width:
303                 to_pad = 0
304             to_join += [line + ' '*to_pad]
305         self.safe_write((''.join(to_join), curses.color_pair(3)))
306
307
308 class LogWidget(TextLinesWidget):
309
310     def get_text_lines(self):
311         return self.tui.game.log_text.split('\n')
312
313
314 class DescriptorWidget(TextLinesWidget):
315
316     def get_text_lines(self):
317         lines = []
318         pos_i = self.tui.game.world.map_.\
319                 get_position_index(self.tui.examiner_position)
320         terrain = self.tui.game.world.map_.terrain[pos_i]
321         lines = [terrain]
322         for t in self.tui.game.world.things:
323             if t.position == self.tui.examiner_position:
324                 lines += [t.type_]
325         return lines
326
327
328 class PopUpWidget(Widget):
329
330     def draw(self):
331         self.safe_write(self.tui.popup_text)
332
333     def reconfigure(self):
334         size = (1, len(self.tui.popup_text))
335         self.size = size
336         self.size_def = size
337         offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
338         offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
339         self.start = (offset_y, offset_x)
340         self.win.mvwin(self.start[0], self.start[1])
341
342
343 class ItemsSelectorWidget(Widget):
344
345     def draw_item_selector(self, title, selection):
346         lines = [title]
347         counter = 0
348         for id_ in selection:
349             pointer = '*' if counter == self.tui.item_pointer else ' '
350             t = self.tui.game.world.get_thing(id_)
351             lines += ['%s %s' % (pointer, t.type_)]
352             counter += 1
353         line_width = self.size[1]
354         to_join = []
355         for line in lines:
356             to_pad = line_width - (len(line) % line_width)
357             if to_pad == line_width:
358                 to_pad = 0
359             to_join += [line + ' '*to_pad]
360         self.safe_write((''.join(to_join), curses.color_pair(3)))
361
362
363 class InventoryWidget(ItemsSelectorWidget):
364
365     def draw(self):
366         self.draw_item_selector('INVENTORY:',
367                                 self.tui.game.world.player_inventory)
368
369 class PickableItemsWidget(ItemsSelectorWidget):
370
371     def draw(self):
372         self.draw_item_selector('PICKABLE:',
373                                 self.tui.game.world.pickable_items)
374
375
376 class MapWidget(Widget):
377
378     def draw(self):
379
380         def annotated_terrain():
381             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
382             for t in self.tui.game.world.things:
383                 pos_i = self.tui.game.world.map_.get_position_index(t.position)
384                 symbol = self.tui.game.symbol_for_type(t.type_)
385                 if terrain_as_list[pos_i][0] in {'i', '@', 'm'}:
386                     old_symbol = terrain_as_list[pos_i][0]
387                     if old_symbol in {'@', 'm'}:
388                         symbol = old_symbol
389                     terrain_as_list[pos_i] = (symbol, '+')
390                 else:
391                     terrain_as_list[pos_i] = symbol
392             if self.tui.examiner_mode:
393                 pos_i = self.tui.game.world.map_.\
394                         get_position_index(self.tui.examiner_position)
395                 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
396             return terrain_as_list
397
398         def pad_or_cut_x(lines):
399             line_width = self.size[1]
400             for y in range(len(lines)):
401                 line = lines[y]
402                 if line_width > len(line):
403                     to_pad = line_width - (len(line) % line_width)
404                     lines[y] = line + '0' * to_pad
405                 else:
406                     lines[y] = line[:line_width]
407
408         def pad_y(lines):
409             if len(lines) < self.size[0]:
410                 to_pad = self.size[0] - len(lines)
411                 lines += to_pad * ['0' * self.size[1]]
412
413         def lines_to_colored_chars(lines):
414             chars_with_attrs = []
415             for c in ''.join(lines):
416                 if c in {'@', 'm'}:
417                     chars_with_attrs += [(c, curses.color_pair(1))]
418                 elif c == 'i':
419                     chars_with_attrs += [(c, curses.color_pair(4))]
420                 elif c == '.':
421                     chars_with_attrs += [(c, curses.color_pair(2))]
422                 elif c in {'x', 'X', '#'}:
423                     chars_with_attrs += [(c, curses.color_pair(3))]
424                 elif c == '?':
425                     chars_with_attrs += [(c, curses.color_pair(5))]
426                 else:
427                     chars_with_attrs += [c]
428             return chars_with_attrs
429
430         if self.tui.game.world.map_.terrain == '':
431             lines = []
432             pad_y(lines)
433             self.safe_write(''.join(lines))
434             return
435
436         annotated_terrain = annotated_terrain()
437         center = self.tui.game.world.player.position
438         if self.tui.examiner_mode:
439             center = self.tui.examiner_position
440         lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
441                                                         center, self.size)
442         pad_or_cut_x(lines)
443         pad_y(lines)
444         self.safe_write(lines_to_colored_chars(lines))
445
446
447 class TurnWidget(Widget):
448
449     def draw(self):
450         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
451
452
453 class HealthWidget(Widget):
454
455     def draw(self):
456         if hasattr(self.tui.game.world.player, 'health'):
457             self.safe_write((str(self.tui.game.world.player.health),
458                              curses.color_pair(2)))
459
460
461 class TextLineWidget(Widget):
462
463     def __init__(self, text_line, *args, **kwargs):
464         self.text_line = text_line
465         super().__init__(*args, **kwargs)
466
467     def draw(self):
468         self.safe_write(self.text_line)
469
470
471 class TUI:
472
473     def __init__(self, plom_socket, game, q):
474         self.socket = plom_socket
475         self.game = game
476         self.game.tui = self
477         self.queue = q
478         self.parser = Parser(self.game)
479         self.to_update = {}
480         self.item_pointer = 0
481         self.examiner_position = (0, 0)
482         self.examiner_mode = False
483         self.popup_text = 'Hi bob'
484         self.to_send = []
485         self.draw_popup_if_visible = True
486         curses.wrapper(self.loop)
487
488     def loop(self, stdscr):
489
490         def setup_screen(stdscr):
491             self.stdscr = stdscr
492             self.stdscr.refresh()  # will be called by getkey else, clearing screen
493             self.stdscr.timeout(10)
494
495         def switch_widgets(widget_1, widget_2):
496             widget_1.visible = False
497             widget_2.visible = True
498             trigger = widget_2.check_updates[0]
499             self.to_update[trigger] = True
500
501         def pick_or_drop_menu(action_key, widget, selectables, task,
502                               bonus_command=None):
503             if len(selectables) < self.item_pointer + 1 and\
504                self.item_pointer > 0:
505                 self.item_pointer = len(selectables) - 1
506             if key == 'c':
507                 switch_widgets(widget, map_widget)
508             elif key == 'j':
509                 self.item_pointer += 1
510             elif key == 'k' and self.item_pointer > 0:
511                 self.item_pointer -= 1
512             elif key == action_key and len(selectables) > 0:
513                 id_ = selectables[self.item_pointer]
514                 self.socket.send('TASK:%s %s' % (task, id_))
515                 if bonus_command:
516                     self.socket.send(bonus_command)
517                 if self.item_pointer > 0:
518                     self.item_pointer -= 1
519             else:
520                 return
521             trigger = widget.check_updates[0]
522             self.to_update[trigger] = True
523
524         def move_examiner(direction):
525             start_pos = self.examiner_position
526             new_examine_pos = self.game.world.map_.move(start_pos, direction)
527             if new_examine_pos:
528                 self.examiner_position = new_examine_pos
529             self.to_update['map'] = True
530
531         def switch_to_pick_or_drop(target_widget):
532             self.item_pointer = 0
533             switch_widgets(map_widget, target_widget)
534             if self.examiner_mode:
535                 self.examiner_mode = False
536                 switch_widgets(descriptor_widget, log_widget)
537
538         def toggle_examiner_mode():
539             if self.examiner_mode:
540                 self.examiner_mode = False
541                 switch_widgets(descriptor_widget, log_widget)
542             else:
543                 self.examiner_mode = True
544                 self.examiner_position = self.game.world.player.position
545                 switch_widgets(log_widget, descriptor_widget)
546             self.to_update['map'] = True
547
548         def toggle_popup():
549             if popup_widget.visible:
550                 popup_widget.visible = False
551                 for w in top_widgets:
552                     w.ensure_freshness(True)
553             else:
554                 self.to_update['popup'] = True
555                 popup_widget.visible = True
556                 popup_widget.reconfigure()
557                 self.draw_popup_if_visible = True
558
559         def try_write_keys():
560             if len(key) == 1 and key in ASCII_printable and \
561                     len(self.to_send) < len(edit_line_widget):
562                 self.to_send += [key]
563                 self.to_update['edit'] = True
564             elif key == 'KEY_BACKSPACE':
565                 self.to_send[:] = self.to_send[:-1]
566                 self.to_update['edit'] = True
567             elif key == '\n':  # Return key
568                 self.socket.send(''.join(self.to_send))
569                 self.to_send[:] = []
570                 self.to_update['edit'] = True
571
572         def try_examiner_keys():
573             if key == 'w':
574                 move_examiner('UPLEFT')
575             elif key == 'e':
576                 move_examiner('UPRIGHT')
577             elif key == 's':
578                 move_examiner('LEFT')
579             elif key == 'd':
580                 move_examiner('RIGHT')
581             elif key == 'x':
582                 move_examiner('DOWNLEFT')
583             elif key == 'c':
584                 move_examiner('DOWNRIGHT')
585
586         def try_player_move_keys():
587             if key == 'w':
588                 self.socket.send('TASK:MOVE UPLEFT')
589             elif key == 'e':
590                 self.socket.send('TASK:MOVE UPRIGHT')
591             elif key == 's':
592                 self.socket.send('TASK:MOVE LEFT')
593             elif key == 'd':
594                 self.socket.send('TASK:MOVE RIGHT')
595             elif key == 'x':
596                 self.socket.send('TASK:MOVE DOWNLEFT')
597             elif key == 'c':
598                 self.socket.send('TASK:MOVE DOWNRIGHT')
599
600         def init_colors():
601             curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
602             curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
603             curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
604             curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
605             curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
606
607         # Basic curses initialization work.
608         setup_screen(stdscr)
609         curses.curs_set(False)  # hide cursor
610         init_colors()
611
612         # With screen initialized, set up widgets with their curses windows.
613         edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
614         edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
615         edit_widget.children += [edit_line_widget]
616         turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
617         turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
618         health_widget = TextLineWidget('HEALTH:', self, (3, 0), (1, 20))
619         health_widget.children += [HealthWidget(self, (3, 8), (1, 12), ['turn'])]
620         log_widget = LogWidget(self, (5, 0), (None, 20), ['log'])
621         descriptor_widget = DescriptorWidget(self, (5, 0), (None, 20),
622                                              ['map'], False)
623         map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
624         inventory_widget = InventoryWidget(self, (0, 21), (None, None),
625                                            ['inventory'], False)
626         pickable_items_widget = PickableItemsWidget(self, (0, 21), (None, None),
627                                                     ['pickable_items'], False)
628         top_widgets = [edit_widget, turn_widget, health_widget, log_widget,
629                        descriptor_widget, map_widget, inventory_widget,
630                        pickable_items_widget]
631         popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
632
633         # Ensure initial window state before loop starts.
634         for w in top_widgets:
635             w.ensure_freshness(True)
636         self.socket.send('GET_GAMESTATE')
637         write_mode = False
638         while True:
639
640             # Draw screen.
641             for w in top_widgets:
642                 if w.ensure_freshness():
643                     self.draw_popup_if_visible = True
644             if popup_widget.visible and self.draw_popup_if_visible:
645                 popup_widget.ensure_freshness(True)
646                 self.draw_popup_if_visible = False
647             for k in self.to_update.keys():
648                 self.to_update[k] = False
649
650             # Handle input from server.
651             while True:
652                 try:
653                     command = self.queue.get(block=False)
654                 except queue.Empty:
655                     break
656                 self.game.handle_input(command)
657
658             # Handle keys (and resize event read as key).
659             try:
660                 key = self.stdscr.getkey()
661                 if key == 'KEY_RESIZE':
662                     curses.endwin()
663                     setup_screen(curses.initscr())
664                     for w in top_widgets:
665                         w.size = w.size_def
666                         w.ensure_freshness(True)
667                 elif key == '\t':  # Tabulator key.
668                     write_mode = False if write_mode else True
669                 elif write_mode:
670                     try_write_keys()
671                 elif key == 't':
672                     toggle_popup()
673                 elif map_widget.visible:
674                     if key == '?':
675                         toggle_examiner_mode()
676                     elif key == 'p':
677                         self.socket.send('GET_PICKABLE_ITEMS')
678                         switch_to_pick_or_drop(pickable_items_widget)
679                     elif key == 'i':
680                         switch_to_pick_or_drop(inventory_widget)
681                     elif self.examiner_mode:
682                         try_examiner_keys()
683                     else:
684                         try_player_move_keys()
685                 elif pickable_items_widget.visible:
686                     pick_or_drop_menu('p', pickable_items_widget,
687                                       self.game.world.pickable_items,
688                                       'PICKUP', 'GET_PICKABLE_ITEMS')
689                 elif inventory_widget.visible:
690                     pick_or_drop_menu('d', inventory_widget,
691                                       self.game.world.player_inventory,
692                                       'DROP')
693             except curses.error:
694                 pass
695
696             # Quit when server recommends it.
697             if self.game.do_quit:
698                 break
699
700
701 s = socket.create_connection(('127.0.0.1', 5000))
702 plom_socket = PlomSocket(s)
703 game = Game()
704 q = queue.Queue()
705 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
706 t.start()
707 TUI(plom_socket, game, q)