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