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