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