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