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