home · contact · privacy
0ea2db602ff55b91aff63e6f5120d410f169b022
[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):
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[1].y, size.y)
66         map_width = self.size.x * 2 + 1
67         self.x_cut(map_lines, center[1].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(0,0), 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.map_geometry = MapGeometryHex()
161         self.thing_type = ThingBase
162         self.commands = {'LAST_PLAYER_TASK_RESULT': cmd_LAST_PLAYER_TASK_RESULT,
163                          'TURN_FINISHED': cmd_TURN_FINISHED,
164                          'TURN': cmd_TURN,
165                          'VISIBLE_MAP_LINE': cmd_VISIBLE_MAP_LINE,
166                          'PLAYER_ID': cmd_PLAYER_ID,
167                          'PLAYER_INVENTORY': cmd_PLAYER_INVENTORY,
168                          'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
169                          'VISIBLE_MAP': cmd_VISIBLE_MAP,
170                          'PICKABLE_ITEMS': cmd_PICKABLE_ITEMS,
171                          'THING_TYPE': cmd_THING_TYPE,
172                          'THING_HEALTH': cmd_THING_HEALTH,
173                          'THING_POS': cmd_THING_POS}
174         self.log_text = ''
175         self.do_quit = False
176         self.tui = None
177
178     def get_command(self, command_name):
179         from functools import partial
180         if command_name in self.commands:
181             f = partial(self.commands[command_name], self)
182             if hasattr(self.commands[command_name], 'argtypes'):
183                 f.argtypes = self.commands[command_name].argtypes
184             return f
185         return None
186
187     def get_string_options(self, string_option_type):
188         return None
189
190     def handle_input(self, msg):
191         self.log(msg)
192         if msg == 'BYE':
193             self.do_quit = True
194             return
195         try:
196             command, args = self.parser.parse(msg)
197             if command is None:
198                 self.log('UNHANDLED INPUT: ' + msg)
199             else:
200                 command(*args)
201         except ArgError as e:
202             self.log('ARGUMENT ERROR: ' + msg + '\n' + str(e))
203
204     def log(self, msg):
205         """Prefix msg plus newline to self.log_text."""
206         self.log_text = msg + '\n' + self.log_text
207         self.tui.to_update['log'] = True
208
209     def symbol_for_type(self, type_):
210         symbol = '?'
211         if type_ == 'human':
212             symbol = '@'
213         elif type_ == 'monster':
214             symbol = 'm'
215         elif type_ == 'food':
216             symbol = 'f'
217         return symbol
218
219
220 ASCII_printable = ' !"#$%&\'\(\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX'\
221                   'YZ[\\]^_\`abcdefghijklmnopqrstuvwxyz{|}~'
222
223
224 def recv_loop(plom_socket, game, q):
225     for msg in plom_socket.recv():
226         q.put(msg)
227
228
229 class Widget:
230
231     def __init__(self, tui, start, size, check_updates=[], visible=True):
232         self.check_updates = check_updates
233         self.tui = tui
234         self.start = start
235         self.win = curses.newwin(1, 1, self.start.y, self.start.x)
236         self.size_def = size  # store for re-calling .size on SIGWINCH
237         self.size = size
238         self.do_update = True
239         self.visible = visible
240         self.children = []
241
242     @property
243     def size(self):
244         return YX(*self.win.getmaxyx())
245
246     @size.setter
247     def size(self, size):
248         """Set window size. Size be y,x tuple. If y or x None, use legal max."""
249         n_lines, n_cols = size
250         getmaxyx = YX(*self.tui.stdscr.getmaxyx())
251         if n_lines is None:
252             n_lines = getmaxyx.y - self.start.y
253         if n_cols is None:
254             n_cols = getmaxyx.x - self.start.x
255         self.win.resize(n_lines, n_cols)
256
257     def __len__(self):
258         getmaxyx = YX(*self.win.getmaxyx())
259         return getmaxyx.y * getmaxyx.x
260
261     def safe_write(self, foo):
262
263         def to_chars_with_attrs(part):
264             attr = curses.A_NORMAL
265             part_string = part
266             if not type(part) == str:
267                 part_string = part[0]
268                 attr = part[1]
269             return [(char, attr) for char in part_string]
270
271         chars_with_attrs = []
272         if type(foo) == str or (len(foo) == 2 and type(foo[1]) == int):
273             chars_with_attrs += to_chars_with_attrs(foo)
274         else:
275             for part in foo:
276                 chars_with_attrs += to_chars_with_attrs(part)
277         self.win.move(0, 0)
278         if len(chars_with_attrs) < len(self):
279             for char_with_attr in chars_with_attrs:
280                 self.win.addstr(char_with_attr[0], char_with_attr[1])
281         else:  # workaround to <https://stackoverflow.com/q/7063128>
282             cut = chars_with_attrs[:len(self) - 1]
283             last_char_with_attr = chars_with_attrs[len(self) - 1]
284             self.win.addstr(self.size.y - 1, self.size.x - 2,
285                             last_char_with_attr[0], last_char_with_attr[1])
286             self.win.insstr(self.size.y - 1, self.size.x - 2, ' ')
287             self.win.move(0, 0)
288             for char_with_attr in cut:
289                 self.win.addstr(char_with_attr[0], char_with_attr[1])
290
291     def ensure_freshness(self, do_refresh=False):
292         did_refresh = False
293         if self.visible:
294             if not do_refresh:
295                 for key in self.check_updates:
296                     if key in self.tui.to_update and self.tui.to_update[key]:
297                         do_refresh = True
298                         break
299             if do_refresh:
300                 self.win.erase()
301                 self.draw()
302                 self.win.refresh()
303                 did_refresh = True
304             for child in self.children:
305                 did_refresh = child.ensure_freshness(do_refresh) | did_refresh
306         return did_refresh
307
308
309 class EditWidget(Widget):
310
311     def draw(self):
312         self.safe_write((''.join(self.tui.to_send), curses.color_pair(1)))
313
314
315 class TextLinesWidget(Widget):
316
317     def draw(self):
318         lines = self.get_text_lines()
319         line_width = self.size.x
320         to_join = []
321         for line in lines:
322             to_pad = line_width - (len(line) % line_width)
323             if to_pad == line_width:
324                 to_pad = 0
325             to_join += [line + ' '*to_pad]
326         self.safe_write((''.join(to_join), curses.color_pair(3)))
327
328
329 class LogWidget(TextLinesWidget):
330
331     def get_text_lines(self):
332         return self.tui.game.log_text.split('\n')
333
334
335 class DescriptorWidget(TextLinesWidget):
336
337     def get_text_lines(self):
338         lines = []
339         pos_i = self.tui.game.world.map_.\
340                 get_position_index(self.tui.examiner_position[1])
341         terrain = self.tui.game.world.map_.terrain[pos_i]
342         lines = [terrain]
343         for t in self.tui.game.world.things_at_pos(self.tui.examiner_position):
344             lines += [t.type_]
345         return lines
346
347
348 class PopUpWidget(Widget):
349
350     def draw(self):
351         self.safe_write(self.tui.popup_text)
352
353     def reconfigure(self):
354         size = (1, len(self.tui.popup_text))
355         self.size = size
356         self.size_def = size
357         getmaxyx = YX(*self.tui.stdscr.getmaxyx())
358         offset_y = int(getmaxyx.y / 2 - size.y / 2)
359         offset_x = int(getmaxyx.x / 2 - size.x / 2)
360         self.start = YX(offset_y, offset_x)
361         self.win.mvwin(self.start.y, self.start.x)
362
363
364 class ItemsSelectorWidget(Widget):
365
366     def __init__(self, headline, selection, *args, **kwargs):
367         super().__init__(*args, **kwargs)
368         self.headline = headline
369         self.selection = selection
370
371     def ensure_freshness(self, *args, **kwargs):
372         # We only update pointer on non-empty selection so that the zero-ing
373         # of the selection at TURN_FINISHED etc. before pulling in a new
374         # state does not destroy any memory of previous item pointer positions.
375         if len(self.selection) > 0 and\
376            len(self.selection) < self.tui.item_pointer + 1 and\
377            self.tui.item_pointer > 0:
378             self.tui.item_pointer = max(0, len(self.selection) - 1)
379             self.tui.to_update[self.check_updates[0]] = True
380         super().ensure_freshness(*args, **kwargs)
381
382     def draw(self):
383         lines = [self.headline]
384         counter = 0
385         for id_ in self.selection:
386             pointer = '*' if counter == self.tui.item_pointer else ' '
387             t = self.tui.game.world.get_thing(id_)
388             lines += ['%s %s' % (pointer, t.type_)]
389             counter += 1
390         line_width = self.size.x
391         to_join = []
392         for line in lines:
393             to_pad = line_width - (len(line) % line_width)
394             if to_pad == line_width:
395                 to_pad = 0
396             to_join += [line + ' '*to_pad]
397         self.safe_write((''.join(to_join), curses.color_pair(3)))
398
399
400 class MapWidget(Widget):
401
402     def draw(self):
403
404         def annotated_terrain():
405             terrain_as_list = list(self.tui.game.world.map_.terrain[:])
406             for t in self.tui.game.world.things:
407                 if t.id_ in self.tui.game.world.player_inventory:
408                     continue
409                 pos_i = self.tui.game.world.map_.\
410                         get_position_index(t.position[1])
411                 symbol = self.tui.game.symbol_for_type(t.type_)
412                 if terrain_as_list[pos_i][0] in {'f', '@', 'm'}:
413                     old_symbol = terrain_as_list[pos_i][0]
414                     if old_symbol in {'@', 'm'}:
415                         symbol = old_symbol
416                     terrain_as_list[pos_i] = (symbol, '+')
417                 else:
418                     terrain_as_list[pos_i] = symbol
419             if self.tui.examiner_mode:
420                 pos_i = self.tui.game.world.map_.\
421                         get_position_index(self.tui.examiner_position[1])
422                 terrain_as_list[pos_i] = (terrain_as_list[pos_i][0], '?')
423             return terrain_as_list
424
425         def pad_or_cut_x(lines):
426             line_width = self.size.x
427             for y in range(len(lines)):
428                 line = lines[y]
429                 if line_width > len(line):
430                     to_pad = line_width - (len(line) % line_width)
431                     lines[y] = line + '0' * to_pad
432                 else:
433                     lines[y] = line[:line_width]
434
435         def pad_y(lines):
436             if len(lines) < self.size.y:
437                 to_pad = self.size.y - len(lines)
438                 lines += to_pad * ['0' * self.size.x]
439
440         def lines_to_colored_chars(lines):
441             chars_with_attrs = []
442             for c in ''.join(lines):
443                 if c in {'@', 'm'}:
444                     chars_with_attrs += [(c, curses.color_pair(1))]
445                 elif c == 'f':
446                     chars_with_attrs += [(c, curses.color_pair(4))]
447                 elif c == '.':
448                     chars_with_attrs += [(c, curses.color_pair(2))]
449                 elif c in {'x', 'X', '#'}:
450                     chars_with_attrs += [(c, curses.color_pair(3))]
451                 elif c == '?':
452                     chars_with_attrs += [(c, curses.color_pair(5))]
453                 else:
454                     chars_with_attrs += [c]
455             return chars_with_attrs
456
457         if self.tui.game.world.map_.terrain == '':
458             lines = []
459             pad_y(lines)
460             self.safe_write(''.join(lines))
461             return
462
463         annotated_terrain = annotated_terrain()
464         center = self.tui.game.world.player.position
465         if self.tui.examiner_mode:
466             center = self.tui.examiner_position
467         lines = self.tui.game.world.map_.\
468                 format_to_view(annotated_terrain, center, self.size)
469         pad_or_cut_x(lines)
470         pad_y(lines)
471         self.safe_write(lines_to_colored_chars(lines))
472
473
474 class TurnWidget(Widget):
475
476     def draw(self):
477         self.safe_write((str(self.tui.game.world.turn), curses.color_pair(2)))
478
479
480 class HealthWidget(Widget):
481
482     def draw(self):
483         if hasattr(self.tui.game.world.player, 'health'):
484             self.safe_write((str(self.tui.game.world.player.health),
485                              curses.color_pair(2)))
486
487
488 class TextLineWidget(Widget):
489
490     def __init__(self, text_line, *args, **kwargs):
491         self.text_line = text_line
492         super().__init__(*args, **kwargs)
493
494     def draw(self):
495         self.safe_write(self.text_line)
496
497
498 class TUI:
499
500     def __init__(self, plom_socket, game, q):
501         self.socket = plom_socket
502         self.game = game
503         self.game.tui = self
504         self.queue = q
505         self.parser = Parser(self.game)
506         self.to_update = {}
507         self.item_pointer = 0
508         self.examiner_position = (YX(0,0), YX(0, 0))
509         self.examiner_mode = False
510         self.popup_text = 'Hi bob'
511         self.to_send = []
512         self.draw_popup_if_visible = True
513         curses.wrapper(self.loop)
514
515     def loop(self, stdscr):
516
517         def setup_screen(stdscr):
518             self.stdscr = stdscr
519             self.stdscr.refresh()  # will be called by getkey else, clearing screen
520             self.stdscr.timeout(10)
521
522         def switch_widgets(widget_1, widget_2):
523             widget_1.visible = False
524             widget_2.visible = True
525             trigger = widget_2.check_updates[0]
526             self.to_update[trigger] = True
527
528         def selectables_menu(key, widget, selectables, f):
529             if key == 'c':
530                 switch_widgets(widget, map_widget)
531             elif key == 'j':
532                 self.item_pointer += 1
533             elif key == 'k' and self.item_pointer > 0:
534                 self.item_pointer -= 1
535             elif not f(key, selectables):
536                 return
537             trigger = widget.check_updates[0]
538             self.to_update[trigger] = True
539
540         def pickup_menu(key):
541
542             def f(key, selectables):
543                 if key == 'p' and len(selectables) > 0:
544                     id_ = selectables[self.item_pointer]
545                     self.socket.send('TASK:PICKUP %s' % id_)
546                     self.socket.send('GET_PICKABLE_ITEMS')
547                 else:
548                     return False
549                 return True
550
551             selectables_menu(key, pickable_items_widget,
552                              self.game.world.pickable_items, f)
553
554         def inventory_menu(key):
555
556             def f(key, selectables):
557                 if key == 'd' and len(selectables) > 0:
558                     id_ = selectables[self.item_pointer]
559                     self.socket.send('TASK:DROP %s' % id_)
560                 elif key == 'e' and len(selectables) > 0:
561                     id_ = selectables[self.item_pointer]
562                     self.socket.send('TASK:EAT %s' % id_)
563                 else:
564                     return False
565                 return True
566
567             selectables_menu(key, inventory_widget,
568                              self.game.world.player_inventory, f)
569
570         def move_examiner(direction):
571             start_pos = self.examiner_position
572             new_examine_pos = self.game.map_geometry.move(start_pos, direction,
573                                                           self.game.world.map_.size)
574             if new_examine_pos[0] == (0,0):
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)