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