home · contact · privacy
Transform items into food to replenish player energy.
[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:
324             if t.position == self.tui.examiner_position:
325                 lines += [t.type_]
326         return lines
327
328
329 class PopUpWidget(Widget):
330
331     def draw(self):
332         self.safe_write(self.tui.popup_text)
333
334     def reconfigure(self):
335         size = (1, len(self.tui.popup_text))
336         self.size = size
337         self.size_def = size
338         offset_y = int((self.tui.stdscr.getmaxyx()[0] / 2) - (size[0] / 2))
339         offset_x = int((self.tui.stdscr.getmaxyx()[1] / 2) - (size[1] / 2))
340         self.start = (offset_y, offset_x)
341         self.win.mvwin(self.start[0], self.start[1])
342
343
344 class ItemsSelectorWidget(Widget):
345
346     def __init__(self, headline, selection, *args, **kwargs):
347         super().__init__(*args, **kwargs)
348         self.headline = headline
349         self.selection = selection
350
351     def ensure_freshness(self, *args, **kwargs):
352         # We only update pointer on non-empty selection so that the zero-ing
353         # of the selection at TURN_FINISHED etc. before pulling in a new
354         # state does not destroy any memory of previous item pointer positions.
355         if len(self.selection) > 0 and\
356            len(self.selection) < self.tui.item_pointer + 1 and\
357            self.tui.item_pointer > 0:
358             self.tui.item_pointer = max(0, len(self.selection) - 1)
359             self.tui.to_update[self.check_updates[0]] = True
360         super().ensure_freshness(*args, **kwargs)
361
362     def draw(self):
363         lines = [self.headline]
364         counter = 0
365         for id_ in self.selection:
366             pointer = '*' if counter == self.tui.item_pointer else ' '
367             t = self.tui.game.world.get_thing(id_)
368             lines += ['%s %s' % (pointer, t.type_)]
369             counter += 1
370         line_width = self.size[1]
371         to_join = []
372         for line in lines:
373             to_pad = line_width - (len(line) % line_width)
374             if to_pad == line_width:
375                 to_pad = 0
376             to_join += [line + ' '*to_pad]
377         self.safe_write((''.join(to_join), curses.color_pair(3)))
378
379
380 class MapWidget(Widget):
381
382     def draw(self):
383
384         def annotated_terrain():
385             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
386             for t in self.tui.game.world.things:
387                 pos_i = self.tui.game.world.map_.get_position_index(t.position)
388                 symbol = self.tui.game.symbol_for_type(t.type_)
389                 if terrain_as_list[pos_i][0] in {'f', '@', 'm'}:
390                     old_symbol = terrain_as_list[pos_i][0]
391                     if old_symbol in {'@', 'm'}:
392                         symbol = old_symbol
393                     terrain_as_list[pos_i] = (symbol, '+')
394                 else:
395                     terrain_as_list[pos_i] = symbol
396             if self.tui.examiner_mode:
397                 pos_i = self.tui.game.world.map_.\
398                         get_position_index(self.tui.examiner_position)
399                 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
400             return terrain_as_list
401
402         def pad_or_cut_x(lines):
403             line_width = self.size[1]
404             for y in range(len(lines)):
405                 line = lines[y]
406                 if line_width > len(line):
407                     to_pad = line_width - (len(line) % line_width)
408                     lines[y] = line + '0' * to_pad
409                 else:
410                     lines[y] = line[:line_width]
411
412         def pad_y(lines):
413             if len(lines) < self.size[0]:
414                 to_pad = self.size[0] - len(lines)
415                 lines += to_pad * ['0' * self.size[1]]
416
417         def lines_to_colored_chars(lines):
418             chars_with_attrs = []
419             for c in ''.join(lines):
420                 if c in {'@', 'm'}:
421                     chars_with_attrs += [(c, curses.color_pair(1))]
422                 elif c == 'f':
423                     chars_with_attrs += [(c, curses.color_pair(4))]
424                 elif c == '.':
425                     chars_with_attrs += [(c, curses.color_pair(2))]
426                 elif c in {'x', 'X', '#'}:
427                     chars_with_attrs += [(c, curses.color_pair(3))]
428                 elif c == '?':
429                     chars_with_attrs += [(c, curses.color_pair(5))]
430                 else:
431                     chars_with_attrs += [c]
432             return chars_with_attrs
433
434         if self.tui.game.world.map_.terrain == '':
435             lines = []
436             pad_y(lines)
437             self.safe_write(''.join(lines))
438             return
439
440         annotated_terrain = annotated_terrain()
441         center = self.tui.game.world.player.position
442         if self.tui.examiner_mode:
443             center = self.tui.examiner_position
444         lines = self.tui.game.world.map_.format_to_view(annotated_terrain,
445                                                         center, self.size)
446         pad_or_cut_x(lines)
447         pad_y(lines)
448         self.safe_write(lines_to_colored_chars(lines))
449
450
451 class TurnWidget(Widget):
452
453     def draw(self):
454         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
455
456
457 class HealthWidget(Widget):
458
459     def draw(self):
460         if hasattr(self.tui.game.world.player, 'health'):
461             self.safe_write((str(self.tui.game.world.player.health),
462                              curses.color_pair(2)))
463
464
465 class TextLineWidget(Widget):
466
467     def __init__(self, text_line, *args, **kwargs):
468         self.text_line = text_line
469         super().__init__(*args, **kwargs)
470
471     def draw(self):
472         self.safe_write(self.text_line)
473
474
475 class TUI:
476
477     def __init__(self, plom_socket, game, q):
478         self.socket = plom_socket
479         self.game = game
480         self.game.tui = self
481         self.queue = q
482         self.parser = Parser(self.game)
483         self.to_update = {}
484         self.item_pointer = 0
485         self.examiner_position = (0, 0)
486         self.examiner_mode = False
487         self.popup_text = 'Hi bob'
488         self.to_send = []
489         self.draw_popup_if_visible = True
490         curses.wrapper(self.loop)
491
492     def loop(self, stdscr):
493
494         def setup_screen(stdscr):
495             self.stdscr = stdscr
496             self.stdscr.refresh()  # will be called by getkey else, clearing screen
497             self.stdscr.timeout(10)
498
499         def switch_widgets(widget_1, widget_2):
500             widget_1.visible = False
501             widget_2.visible = True
502             trigger = widget_2.check_updates[0]
503             self.to_update[trigger] = True
504
505         def selectables_menu(key, widget, selectables, f):
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 not f(key, selectables):
513                 return
514             trigger = widget.check_updates[0]
515             self.to_update[trigger] = True
516
517         def pickup_menu(key):
518
519             def f(key, selectables):
520                 if key == 'p' and len(selectables) > 0:
521                     id_ = selectables[self.item_pointer]
522                     self.socket.send('TASK:PICKUP %s' % id_)
523                     self.socket.send('GET_PICKABLE_ITEMS')
524                 else:
525                     return False
526                 return True
527
528             selectables_menu(key, pickable_items_widget,
529                              self.game.world.pickable_items, f)
530
531         def inventory_menu(key):
532
533             def f(key, selectables):
534                 if key == 'd' and len(selectables) > 0:
535                     id_ = selectables[self.item_pointer]
536                     self.socket.send('TASK:DROP %s' % id_)
537                 elif key == 'e' and len(selectables) > 0:
538                     id_ = selectables[self.item_pointer]
539                     self.socket.send('TASK:EAT %s' % id_)
540                 else:
541                     return False
542                 return True
543
544             selectables_menu(key, inventory_widget,
545                              self.game.world.player_inventory, f)
546
547         def move_examiner(direction):
548             start_pos = self.examiner_position
549             new_examine_pos = self.game.world.map_.move(start_pos, direction)
550             if new_examine_pos:
551                 self.examiner_position = new_examine_pos
552             self.to_update['map'] = True
553
554         def switch_to_pick_or_drop(target_widget):
555             self.item_pointer = 0
556             switch_widgets(map_widget, target_widget)
557             if self.examiner_mode:
558                 self.examiner_mode = False
559                 switch_widgets(descriptor_widget, log_widget)
560
561         def toggle_examiner_mode():
562             if self.examiner_mode:
563                 self.examiner_mode = False
564                 switch_widgets(descriptor_widget, log_widget)
565             else:
566                 self.examiner_mode = True
567                 self.examiner_position = self.game.world.player.position
568                 switch_widgets(log_widget, descriptor_widget)
569             self.to_update['map'] = True
570
571         def toggle_popup():
572             if popup_widget.visible:
573                 popup_widget.visible = False
574                 for w in top_widgets:
575                     w.ensure_freshness(True)
576             else:
577                 self.to_update['popup'] = True
578                 popup_widget.visible = True
579                 popup_widget.reconfigure()
580                 self.draw_popup_if_visible = True
581
582         def try_write_keys():
583             if len(key) == 1 and key in ASCII_printable and \
584                     len(self.to_send) < len(edit_line_widget):
585                 self.to_send += [key]
586                 self.to_update['edit'] = True
587             elif key == 'KEY_BACKSPACE':
588                 self.to_send[:] = self.to_send[:-1]
589                 self.to_update['edit'] = True
590             elif key == '\n':  # Return key
591                 self.socket.send(''.join(self.to_send))
592                 self.to_send[:] = []
593                 self.to_update['edit'] = True
594
595         def try_examiner_keys():
596             if key == 'w':
597                 move_examiner('UPLEFT')
598             elif key == 'e':
599                 move_examiner('UPRIGHT')
600             elif key == 's':
601                 move_examiner('LEFT')
602             elif key == 'd':
603                 move_examiner('RIGHT')
604             elif key == 'x':
605                 move_examiner('DOWNLEFT')
606             elif key == 'c':
607                 move_examiner('DOWNRIGHT')
608
609         def try_player_move_keys():
610             if key == 'w':
611                 self.socket.send('TASK:MOVE UPLEFT')
612             elif key == 'e':
613                 self.socket.send('TASK:MOVE UPRIGHT')
614             elif key == 's':
615                 self.socket.send('TASK:MOVE LEFT')
616             elif key == 'd':
617                 self.socket.send('TASK:MOVE RIGHT')
618             elif key == 'x':
619                 self.socket.send('TASK:MOVE DOWNLEFT')
620             elif key == 'c':
621                 self.socket.send('TASK:MOVE DOWNRIGHT')
622
623         def init_colors():
624             curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_RED)
625             curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_GREEN)
626             curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_BLUE)
627             curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
628             curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_WHITE)
629
630         # Basic curses initialization work.
631         setup_screen(stdscr)
632         curses.curs_set(False)  # hide cursor
633         init_colors()
634
635         # With screen initialized, set up widgets with their curses windows.
636         edit_widget = TextLineWidget('SEND:', self, (0, 0), (1, 20))
637         edit_line_widget = EditWidget(self, (0, 6), (1, 14), ['edit'])
638         edit_widget.children += [edit_line_widget]
639         turn_widget = TextLineWidget('TURN:', self, (2, 0), (1, 20))
640         turn_widget.children += [TurnWidget(self, (2, 6), (1, 14), ['turn'])]
641         health_widget = TextLineWidget('HEALTH:', self, (3, 0), (1, 20))
642         health_widget.children += [HealthWidget(self, (3, 8), (1, 12), ['turn'])]
643         log_widget = LogWidget(self, (5, 0), (None, 20), ['log'])
644         descriptor_widget = DescriptorWidget(self, (5, 0), (None, 20),
645                                              ['map'], False)
646         map_widget = MapWidget(self, (0, 21), (None, None), ['map'])
647         inventory_widget = ItemsSelectorWidget('INVENTORY:',
648                                                self.game.world.player_inventory,
649                                                self, (0, 21), (None,
650                                                                None), ['inventory'],
651                                                False)
652         pickable_items_widget = ItemsSelectorWidget('PICKABLE:',
653                                                     self.game.world.pickable_items,
654                                                     self, (0, 21),
655                                                     (None, None),
656                                                     ['pickable_items'],
657                                                     False)
658         top_widgets = [edit_widget, turn_widget, health_widget, log_widget,
659                        descriptor_widget, map_widget, inventory_widget,
660                        pickable_items_widget]
661         popup_widget = PopUpWidget(self, (0, 0), (1, 1), visible=False)
662
663         # Ensure initial window state before loop starts.
664         for w in top_widgets:
665             w.ensure_freshness(True)
666         self.socket.send('GET_GAMESTATE')
667         write_mode = False
668         while True:
669
670             # Draw screen.
671             for w in top_widgets:
672                 if w.ensure_freshness():
673                     self.draw_popup_if_visible = True
674             if popup_widget.visible and self.draw_popup_if_visible:
675                 popup_widget.ensure_freshness(True)
676                 self.draw_popup_if_visible = False
677             for k in self.to_update.keys():
678                 self.to_update[k] = False
679
680             # Handle input from server.
681             while True:
682                 try:
683                     command = self.queue.get(block=False)
684                 except queue.Empty:
685                     break
686                 self.game.handle_input(command)
687
688             # Handle keys (and resize event read as key).
689             try:
690                 key = self.stdscr.getkey()
691                 if key == 'KEY_RESIZE':
692                     curses.endwin()
693                     setup_screen(curses.initscr())
694                     for w in top_widgets:
695                         w.size = w.size_def
696                         w.ensure_freshness(True)
697                 elif key == '\t':  # Tabulator key.
698                     write_mode = False if write_mode else True
699                 elif write_mode:
700                     try_write_keys()
701                 elif key == 't':
702                     toggle_popup()
703                 elif map_widget.visible:
704                     if key == '?':
705                         toggle_examiner_mode()
706                     elif key == 'p':
707                         self.socket.send('GET_PICKABLE_ITEMS')
708                         switch_to_pick_or_drop(pickable_items_widget)
709                     elif key == 'i':
710                         switch_to_pick_or_drop(inventory_widget)
711                     elif self.examiner_mode:
712                         try_examiner_keys()
713                     else:
714                         try_player_move_keys()
715                 elif pickable_items_widget.visible:
716                     pickup_menu(key)
717                 elif inventory_widget.visible:
718                     inventory_menu(key)
719             except curses.error:
720                 pass
721
722             # Quit when server recommends it.
723             if self.game.do_quit:
724                 break
725
726
727 s = socket.create_connection(('127.0.0.1', 5000))
728 plom_socket = PlomSocket(s)
729 game = Game()
730 q = queue.Queue()
731 t = threading.Thread(target=recv_loop, args=(plom_socket, game, q))
732 t.start()
733 TUI(plom_socket, game, q)