home · contact · privacy
Don't generate objects at illegal positions. Plus, refactor.
[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     game.tui.to_update['inventory'] = True
129 cmd_PLAYER_INVENTORY.argtypes = 'seq:int:nonneg'
130
131
132 def cmd_PICKABLE_ITEMS(game, ids):
133     game.world.pickable_items[:] = ids
134     game.tui.to_update['pickable_items'] = True
135 cmd_PICKABLE_ITEMS.argtypes = 'seq:int:nonneg'
136
137
138 class Game:
139
140     def __init__(self):
141         self.parser = Parser(self)
142         self.world = World(self)
143         self.thing_type = ThingBase
144         self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
145                          'TURN_FINISHED': cmd_TURN_FINISHED,
146                          'TURN': cmd_TURN,
147                          'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
148                          'PLAYER_ID': cmd_PLAYER_ID,
149                          'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
150                          'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
151                          'MAP': cmd_MAP,
152                          'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
153                          'THING_TYPE': cmd_THING_TYPE,
154                          'THING_HEALTH': cmd_THING_HEALTH,
155                          'THING_POS': cmd_THING_POS}
156         self.log_text = ''
157         self.do_quit = False
158         self.tui = None
159
160     def get_command(self, command_name):
161         from functools import partial
162         if command_name in self.commands:
163             f = partial(self.commands[command_name], self)
164             if hasattr(self.commands[command_name], 'argtypes'):
165                 f.argtypes = self.commands[command_name].argtypes
166             return f
167         return None
168
169     def get_string_options(self, string_option_type):
170         return None
171
172     def handle_input(self, msg):
173         self.log(msg)
174         if msg == 'BYE':
175             self.do_quit = True
176             return
177         try:
178             command, args = self.parser.parse(msg)
179             if command is None:
180                 self.log('UNHANDLED INPUT: ' + msg)
181             else:
182                 command(*args)
183         except ArgError as e:
184             self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
185
186     def log(self, msg):
187         """Prefix msg plus newline to self.log_text."""
188         self.log_text = msg + '\n' + self.log_text
189         self.tui.to_update['log'] = True
190
191     def symbol_for_type(self, type_):
192         symbol = '?'
193         if type_ == 'human':
194             symbol = '@'
195         elif type_ == 'monster':
196             symbol = 'm'
197         elif type_ == 'food':
198             symbol = 'f'
199         return symbol
200
201
202 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
203                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
204
205
206 def recv_loop(plom_socket, game, q):
207     for msg in plom_socket.recv():
208         q.put(msg)
209
210
211 class Widget:
212
213     def __init__(self, tui, start, size, check_updates=[], visible=True):
214         self.check_updates = check_updates
215         self.tui = tui
216         self.start = start
217         self.win = curses.newwin(1, 1, self.start[0], self.start[1])
218         self.size_def = size  # store for re-calling .size on SIGWINCH
219         self.size = size
220         self.do_update = True
221         self.visible = visible
222         self.children = []
223
224     @property
225     def size(self):
226         return self.win.getmaxyx()
227
228     @size.setter
229     def size(self, size):
230         """Set window size. Size be y,x tuple. If y or x None, use legal max."""
231         n_lines, n_cols = size
232         if n_lines is None:
233             n_lines = self.tui.stdscr.getmaxyx()[0] - self.start[0]
234         if n_cols is None:
235             n_cols = self.tui.stdscr.getmaxyx()[1] - self.start[1]
236         self.win.resize(n_lines, n_cols)
237
238     def __len__(self):
239         return self.win.getmaxyx()[0] * self.win.getmaxyx()[1]
240
241     def safe_write(self, foo):
242
243         def to_chars_with_attrs(part):
244             attr = curses.A_NORMAL
245             part_string = part
246             if not type(part) == str:
247                 part_string = part[0]
248                 attr = part[1]
249             return [(char, attr) for char in part_string]
250
251         chars_with_attrs = []
252         if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
253             chars_with_attrs += to_chars_with_attrs(foo)
254         else:
255             for part in foo:
256                 chars_with_attrs += to_chars_with_attrs(part)
257         self.win.move(0, 0)
258         if len(chars_with_attrs) < len(self):
259             for char_with_attr in chars_with_attrs:
260                 self.win.addstr(char_with_attr[0], char_with_attr[1])
261         else:  # workaround to <https://stackoverflow.com/q/7063128>
262             cut = chars_with_attrs[:len(self) - 1]
263             last_char_with_attr = chars_with_attrs[len(self) - 1]
264             self.win.addstr(self.size[0] - 1, self.size[1] - 2,
265                             last_char_with_attr[0], last_char_with_attr[1])
266             self.win.insstr(self.size[0] - 1, self.size[1] - 2, ' ')
267             self.win.move(0, 0)
268             for char_with_attr in cut:
269                 self.win.addstr(char_with_attr[0], char_with_attr[1])
270
271     def ensure_freshness(self, do_refresh=False):
272         did_refresh = False
273         if self.visible:
274             if not do_refresh:
275                 for key in self.check_updates:
276                     if key in self.tui.to_update and self.tui.to_update[key]:
277                         do_refresh = True
278                         break
279             if do_refresh:
280                 self.win.erase()
281                 self.draw()
282                 self.win.refresh()
283                 did_refresh = True
284             for child in self.children:
285                 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
286         return did_refresh
287
288
289 class EditWidget(Widget):
290
291     def draw(self):
292         self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
293
294
295 class TextLinesWidget(Widget):
296
297     def draw(self):
298         lines = self.get_text_lines()
299         line_width = self.size[1]
300         to_join = []
301         for line in lines:
302             to_pad = line_width - (len(line) % line_width)
303             if to_pad == line_width:
304                 to_pad = 0
305             to_join += [line + ' '*to_pad]
306         self.safe_write((''.join(to_join), curses.color_pair(3)))
307
308
309 class LogWidget(TextLinesWidget):
310
311     def get_text_lines(self):
312         return self.tui.game.log_text.split('\n')
313
314
315 class DescriptorWidget(TextLinesWidget):
316
317     def get_text_lines(self):
318         lines = []
319         pos_i = self.tui.game.world.map_.\
320                 get_position_index(self.tui.examiner_position)
321         terrain = self.tui.game.world.map_.terrain[pos_i]
322         lines = [terrain]
323         for t in self.tui.game.world.things_at_pos(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 __init__(self, headline, selection, *args, **kwargs):
346         super().__init__(*args, **kwargs)
347         self.headline = headline
348         self.selection = selection
349
350     def ensure_freshness(self, *args, **kwargs):
351         # We only update pointer on non-empty selection so that the zero-ing
352         # of the selection at TURN_FINISHED etc. before pulling in a new
353         # state does not destroy any memory of previous item pointer positions.
354         if len(self.selection) > 0 and\
355            len(self.selection) < self.tui.item_pointer + 1 and\
356            self.tui.item_pointer > 0:
357             self.tui.item_pointer = max(0, len(self.selection) - 1)
358             self.tui.to_update[self.check_updates[0]] = True
359         super().ensure_freshness(*args, **kwargs)
360
361     def draw(self):
362         lines = [self.headline]
363         counter = 0
364         for id_ in self.selection:
365             pointer = '*' if counter == self.tui.item_pointer else ' '
366             t = self.tui.game.world.get_thing(id_)
367             lines += ['%s %s' % (pointer, t.type_)]
368             counter += 1
369         line_width = self.size[1]
370         to_join = []
371         for line in lines:
372             to_pad = line_width - (len(line) % line_width)
373             if to_pad == line_width:
374                 to_pad = 0
375             to_join += [line + ' '*to_pad]
376         self.safe_write((''.join(to_join), curses.color_pair(3)))
377
378
379 class MapWidget(Widget):
380
381     def draw(self):
382
383         def annotated_terrain():
384             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
385             for t in self.tui.game.world.things:
386                 pos_i = self.tui.game.world.map_.get_position_index(t.position)
387                 symbol = self.tui.game.symbol_for_type(t.type_)
388                 if terrain_as_list[pos_i][0] in {'f', '@', 'm'}:
389                     old_symbol = terrain_as_list[pos_i][0]
390                     if old_symbol in {'@', 'm'}:
391                         symbol = old_symbol
392                     terrain_as_list[pos_i] = (symbol, '+')
393                 else:
394                     terrain_as_list[pos_i] = symbol
395             if self.tui.examiner_mode:
396                 pos_i = self.tui.game.world.map_.\
397                         get_position_index(self.tui.examiner_position)
398                 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
399             return terrain_as_list
400
401         def pad_or_cut_x(lines):
402             line_width = self.size[1]
403             for y in range(len(lines)):
404                 line = lines[y]
405                 if line_width > len(line):
406                     to_pad = line_width - (len(line) % line_width)
407                     lines[y] = line + '0' * to_pad
408                 else:
409                     lines[y] = line[:line_width]
410
411         def pad_y(lines):
412             if len(lines) < self.size[0]:
413                 to_pad = self.size[0] - len(lines)
414                 lines += to_pad * ['0' * self.size[1]]
415
416         def lines_to_colored_chars(lines):
417             chars_with_attrs = []
418             for c in ''.join(lines):
419                 if c in {'@', 'm'}:
420                     chars_with_attrs += [(c, curses.color_pair(1))]
421                 elif c == 'f':
422                     chars_with_attrs += [(c, curses.color_pair(4))]
423                 elif c == '.':
424                     chars_with_attrs += [(c, curses.color_pair(2))]
425                 elif c in {'x', 'X', '#'}:
426                     chars_with_attrs += [(c, curses.color_pair(3))]
427                 elif c == '?':
428                     chars_with_attrs += [(c, curses.color_pair(5))]
429                 else:
430                     chars_with_attrs += [c]
431             return chars_with_attrs
432
433         if self.tui.game.world.map_.terrain == '':
434             lines = []
435             pad_y(lines)
436             self.safe_write(''.join(lines))
437             return
438
439         annotated_terrain = annotated_terrain()
440         center = self.tui.game.world.player.position
441         if self.tui.examiner_mode:
442             center = self.tui.examiner_position
443         lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
444                                                         center, self.size)
445         pad_or_cut_x(lines)
446         pad_y(lines)
447         self.safe_write(lines_to_colored_chars(lines))
448
449
450 class TurnWidget(Widget):
451
452     def draw(self):
453         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
454
455
456 class HealthWidget(Widget):
457
458     def draw(self):
459         if hasattr(self.tui.game.world.player, 'health'):
460             self.safe_write((str(self.tui.game.world.player.health),
461                              curses.color_pair(2)))
462
463
464 class TextLineWidget(Widget):
465
466     def __init__(self, text_line, *args, **kwargs):
467         self.text_line = text_line
468         super().__init__(*args, **kwargs)
469
470     def draw(self):
471         self.safe_write(self.text_line)
472
473
474 class TUI:
475
476     def __init__(self, plom_socket, game, q):
477         self.socket = plom_socket
478         self.game = game
479         self.game.tui = self
480         self.queue = q
481         self.parser = Parser(self.game)
482         self.to_update = {}
483         self.item_pointer = 0
484         self.examiner_position = (0, 0)
485         self.examiner_mode = False
486         self.popup_text = 'Hi bob'
487         self.to_send = []
488         self.draw_popup_if_visible = True
489         curses.wrapper(self.loop)
490
491     def loop(self, stdscr):
492
493         def setup_screen(stdscr):
494             self.stdscr = stdscr
495             self.stdscr.refresh()  # will be called by getkey else, clearing screen
496             self.stdscr.timeout(10)
497
498         def switch_widgets(widget_1, widget_2):
499             widget_1.visible = False
500             widget_2.visible = True
501             trigger = widget_2.check_updates[0]
502             self.to_update[trigger] = True
503
504         def selectables_menu(key, widget, selectables, f):
505             if key == 'c':
506                 switch_widgets(widget, map_widget)
507             elif key == 'j':
508                 self.item_pointer += 1
509             elif key == 'k' and self.item_pointer > 0:
510                 self.item_pointer -= 1
511             elif not f(key, selectables):
512                 return
513             trigger = widget.check_updates[0]
514             self.to_update[trigger] = True
515
516         def pickup_menu(key):
517
518             def f(key, selectables):
519                 if key == 'p' and len(selectables) > 0:
520                     id_ = selectables[self.item_pointer]
521                     self.socket.send('TASK:PICKUP %s' % id_)
522                     self.socket.send('GET_PICKABLE_ITEMS')
523                 else:
524                     return False
525                 return True
526
527             selectables_menu(key, pickable_items_widget,
528                              self.game.world.pickable_items, f)
529
530         def inventory_menu(key):
531
532             def f(key, selectables):
533                 if key == 'd' and len(selectables) > 0:
534                     id_ = selectables[self.item_pointer]
535                     self.socket.send('TASK:DROP %s' % id_)
536                 elif key == 'e' and len(selectables) > 0:
537                     id_ = selectables[self.item_pointer]
538                     self.socket.send('TASK:EAT %s' % id_)
539                 else:
540                     return False
541                 return True
542
543             selectables_menu(key, inventory_widget,
544                              self.game.world.player_inventory, f)
545
546         def move_examiner(direction):
547             start_pos = self.examiner_position
548             new_examine_pos = self.game.world.map_.move(start_pos, direction)
549             if new_examine_pos:
550                 self.examiner_position = new_examine_pos
551             self.to_update['map'] = True
552
553         def switch_to_pick_or_drop(target_widget):
554             self.item_pointer = 0
555             switch_widgets(map_widget, target_widget)
556             if self.examiner_mode:
557                 self.examiner_mode = False
558                 switch_widgets(descriptor_widget, log_widget)
559
560         def toggle_examiner_mode():
561             if self.examiner_mode:
562                 self.examiner_mode = False
563                 switch_widgets(descriptor_widget, log_widget)
564             else:
565                 self.examiner_mode = True
566                 self.examiner_position = self.game.world.player.position
567                 switch_widgets(log_widget, descriptor_widget)
568             self.to_update['map'] = True
569
570         def toggle_popup():
571             if popup_widget.visible:
572                 popup_widget.visible = False
573                 for w in top_widgets:
574                     w.ensure_freshness(True)
575             else:
576                 self.to_update['popup'] = True
577                 popup_widget.visible = True
578                 popup_widget.reconfigure()
579                 self.draw_popup_if_visible = True
580
581         def try_write_keys():
582             if len(key) == 1 and key in ASCII_printable and \
583                     len(self.to_send) < len(edit_line_widget):
584                 self.to_send += [key]
585                 self.to_update['edit'] = True
586             elif key == 'KEY_BACKSPACE':
587                 self.to_send[:] = self.to_send[:-1]
588                 self.to_update['edit'] = True
589             elif key == '\n':  # Return key
590                 self.socket.send(''.join(self.to_send))
591                 self.to_send[:] = []
592                 self.to_update['edit'] = True
593
594         def try_examiner_keys():
595             if key == 'w':
596                 move_examiner('UPLEFT')
597             elif key == 'e':
598                 move_examiner('UPRIGHT')
599             elif key == 's':
600                 move_examiner('LEFT')
601             elif key == 'd':
602                 move_examiner('RIGHT')
603             elif key == 'x':
604                 move_examiner('DOWNLEFT')
605             elif key == 'c':
606                 move_examiner('DOWNRIGHT')
607
608         def try_player_move_keys():
609             if key == 'w':
610                 self.socket.send('TASK:MOVE UPLEFT')
611             elif key == 'e':
612                 self.socket.send('TASK:MOVE UPRIGHT')
613             elif key == 's':
614                 self.socket.send('TASK:MOVE LEFT')
615             elif key == 'd':
616                 self.socket.send('TASK:MOVE RIGHT')
617             elif key == 'x':
618                 self.socket.send('TASK:MOVE DOWNLEFT')
619             elif key == 'c':
620                 self.socket.send('TASK:MOVE DOWNRIGHT')
621
622         def init_colors():
623             curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
624             curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
625             curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
626             curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
627             curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
628
629         # Basic curses initialization work.
630         setup_screen(stdscr)
631         curses.curs_set(False)  # hide cursor
632         init_colors()
633
634         # With screen initialized, set up widgets with their curses windows.
635         edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
636         edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
637         edit_widget.children += [edit_line_widget]
638         turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
639         turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
640         health_widget = TextLineWidget('HEALTH:', self, (3, 0), (1, 20))
641         health_widget.children += [HealthWidget(self, (3, 8), (1, 12), ['turn'])]
642         log_widget = LogWidget(self, (5, 0), (None, 20), ['log'])
643         descriptor_widget = DescriptorWidget(self, (5, 0), (None, 20),
644                                              ['map'], False)
645         map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
646         inventory_widget = ItemsSelectorWidget('INVENTORY:',
647                                                self.game.world.player_inventory,
648                                                self, (0, 21), (None,
649                                                                None), ['inventory'],
650                                                False)
651         pickable_items_widget = ItemsSelectorWidget('PICKABLE:',
652                                                     self.game.world.pickable_items,
653                                                     self, (0, 21),
654                                                     (None, None),
655                                                     ['pickable_items'],
656                                                     False)
657         top_widgets = [edit_widget, turn_widget, health_widget, log_widget,
658                        descriptor_widget, map_widget, inventory_widget,
659                        pickable_items_widget]
660         popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
661
662         # Ensure initial window state before loop starts.
663         for w in top_widgets:
664             w.ensure_freshness(True)
665         self.socket.send('GET_GAMESTATE')
666         write_mode = False
667         while True:
668
669             # Draw screen.
670             for w in top_widgets:
671                 if w.ensure_freshness():
672                     self.draw_popup_if_visible = True
673             if popup_widget.visible and self.draw_popup_if_visible:
674                 popup_widget.ensure_freshness(True)
675                 self.draw_popup_if_visible = False
676             for k in self.to_update.keys():
677                 self.to_update[k] = False
678
679             # Handle input from server.
680             while True:
681                 try:
682                     command = self.queue.get(block=False)
683                 except queue.Empty:
684                     break
685                 self.game.handle_input(command)
686
687             # Handle keys (and resize event read as key).
688             try:
689                 key = self.stdscr.getkey()
690                 if key == 'KEY_RESIZE':
691                     curses.endwin()
692                     setup_screen(curses.initscr())
693                     for w in top_widgets:
694                         w.size = w.size_def
695                         w.ensure_freshness(True)
696                 elif key == '\t':  # Tabulator key.
697                     write_mode = False if write_mode else True
698                 elif write_mode:
699                     try_write_keys()
700                 elif key == 't':
701                     toggle_popup()
702                 elif map_widget.visible:
703                     if key == '?':
704                         toggle_examiner_mode()
705                     elif key == 'p':
706                         self.socket.send('GET_PICKABLE_ITEMS')
707                         switch_to_pick_or_drop(pickable_items_widget)
708                     elif key == 'i':
709                         switch_to_pick_or_drop(inventory_widget)
710                     elif self.examiner_mode:
711                         try_examiner_keys()
712                     else:
713                         try_player_move_keys()
714                 elif pickable_items_widget.visible:
715                     pickup_menu(key)
716                 elif inventory_widget.visible:
717                     inventory_menu(key)
718             except curses.error:
719                 pass
720
721             # Quit when server recommends it.
722             if self.game.do_quit:
723                 break
724
725
726 s = socket.create_connection(('127.0.0.1', 5000))
727 plom_socket = PlomSocket(s)
728 game = Game()
729 q = queue.Queue()
730 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
731 t.start()
732 TUI(plom_socket, game, q)