6 from plomrogue.game import GameBase
7 from plomrogue.parser import Parser
8 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
9 from plomrogue.things import ThingBase
10 from plomrogue.misc import quote
11 from plomrogue.errors import BrokenSocketConnection
13 from ws4py.client import WebSocketBaseClient
14 class WebSocketClient(WebSocketBaseClient):
16 def __init__(self, recv_handler, *args, **kwargs):
17 super().__init__(*args, **kwargs)
18 self.recv_handler = recv_handler
21 def received_message(self, message):
23 message = str(message)
24 self.recv_handler(message)
27 def plom_closed(self):
28 return self.client_terminated
30 from plomrogue.io_tcp import PlomSocket
31 class PlomSocketClient(PlomSocket):
33 def __init__(self, recv_handler, url):
35 self.recv_handler = recv_handler
36 host, port = url.split(':')
37 super().__init__(socket.create_connection((host, port)))
45 for msg in self.recv():
47 self.socket = ssl.wrap_socket(self.socket)
49 self.recv_handler(msg)
50 except BrokenSocketConnection:
51 pass # we assume socket will be known as dead by now
53 def cmd_TURN(game, n):
57 game.turn_complete = False
58 cmd_TURN.argtypes = 'int:nonneg'
60 def cmd_LOGIN_OK(game):
61 game.tui.switch_mode('post_login_wait')
62 game.tui.send('GET_GAMESTATE')
63 game.tui.log_msg('@ welcome')
64 cmd_LOGIN_OK.argtypes = ''
66 def cmd_CHAT(game, msg):
67 game.tui.log_msg('# ' + msg)
68 game.tui.do_refresh = True
69 cmd_CHAT.argtypes = 'string'
71 def cmd_PLAYER_ID(game, player_id):
72 game.player_id = player_id
73 cmd_PLAYER_ID.argtypes = 'int:nonneg'
75 def cmd_THING(game, yx, thing_type, thing_id):
76 t = game.get_thing(thing_id)
78 t = ThingBase(game, thing_id)
82 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
84 def cmd_THING_NAME(game, thing_id, name):
85 t = game.get_thing(thing_id)
88 cmd_THING_NAME.argtypes = 'int:nonneg string'
90 def cmd_MAP(game, geometry, size, content):
91 map_geometry_class = globals()['MapGeometry' + geometry]
92 game.map_geometry = map_geometry_class(size)
93 game.map_content = content
94 if type(game.map_geometry) == MapGeometrySquare:
95 game.tui.movement_keys = {
96 game.tui.keys['square_move_up']: 'UP',
97 game.tui.keys['square_move_left']: 'LEFT',
98 game.tui.keys['square_move_down']: 'DOWN',
99 game.tui.keys['square_move_right']: 'RIGHT',
101 elif type(game.map_geometry) == MapGeometryHex:
102 game.tui.movement_keys = {
103 game.tui.keys['hex_move_upleft']: 'UPLEFT',
104 game.tui.keys['hex_move_upright']: 'UPRIGHT',
105 game.tui.keys['hex_move_right']: 'RIGHT',
106 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
107 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
108 game.tui.keys['hex_move_left']: 'LEFT',
110 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
112 def cmd_FOV(game, content):
114 cmd_FOV.argtypes = 'string'
116 def cmd_MAP_CONTROL(game, content):
117 game.map_control_content = content
118 cmd_MAP_CONTROL.argtypes = 'string'
120 def cmd_GAME_STATE_COMPLETE(game):
122 if game.tui.mode.name == 'post_login_wait':
123 game.tui.switch_mode('play')
124 if game.tui.mode.shows_info:
125 game.tui.query_info()
126 player = game.get_thing(game.player_id)
127 if player.position in game.portals:
128 game.tui.teleport_target_host = game.portals[player.position]
129 game.tui.switch_mode('teleport')
130 game.turn_complete = True
131 game.tui.do_refresh = True
132 cmd_GAME_STATE_COMPLETE.argtypes = ''
134 def cmd_PORTAL(game, position, msg):
135 game.portals[position] = msg
136 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
138 def cmd_PLAY_ERROR(game, msg):
140 game.tui.do_refresh = True
141 cmd_PLAY_ERROR.argtypes = 'string'
143 def cmd_GAME_ERROR(game, msg):
144 game.tui.log_msg('? game error: ' + msg)
145 game.tui.do_refresh = True
146 cmd_GAME_ERROR.argtypes = 'string'
148 def cmd_ARGUMENT_ERROR(game, msg):
149 game.tui.log_msg('? syntax error: ' + msg)
150 game.tui.do_refresh = True
151 cmd_ARGUMENT_ERROR.argtypes = 'string'
153 def cmd_ANNOTATION(game, position, msg):
154 game.info_db[position] = msg
155 if game.tui.mode.shows_info:
156 game.tui.do_refresh = True
157 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
159 def cmd_TASKS(game, tasks_comma_separated):
160 game.tasks = tasks_comma_separated.split(',')
161 cmd_TASKS.argtypes = 'string'
163 def cmd_THING_TYPE(game, thing_type, symbol_hint):
164 game.thing_types[thing_type] = symbol_hint
165 cmd_THING_TYPE.argtypes = 'string char'
169 cmd_PONG.argtypes = ''
171 class Game(GameBase):
172 turn_complete = False
176 def __init__(self, *args, **kwargs):
177 super().__init__(*args, **kwargs)
178 self.register_command(cmd_LOGIN_OK)
179 self.register_command(cmd_PONG)
180 self.register_command(cmd_CHAT)
181 self.register_command(cmd_PLAYER_ID)
182 self.register_command(cmd_TURN)
183 self.register_command(cmd_THING)
184 self.register_command(cmd_THING_TYPE)
185 self.register_command(cmd_THING_NAME)
186 self.register_command(cmd_MAP)
187 self.register_command(cmd_MAP_CONTROL)
188 self.register_command(cmd_PORTAL)
189 self.register_command(cmd_ANNOTATION)
190 self.register_command(cmd_GAME_STATE_COMPLETE)
191 self.register_command(cmd_ARGUMENT_ERROR)
192 self.register_command(cmd_GAME_ERROR)
193 self.register_command(cmd_PLAY_ERROR)
194 self.register_command(cmd_TASKS)
195 self.register_command(cmd_FOV)
196 self.map_content = ''
201 def get_string_options(self, string_option_type):
202 if string_option_type == 'map_geometry':
203 return ['Hex', 'Square']
204 elif string_option_type == 'thing_type':
205 return self.thing_types.keys()
208 def get_command(self, command_name):
209 from functools import partial
210 f = partial(self.commands[command_name], self)
211 f.argtypes = self.commands[command_name].argtypes
218 def __init__(self, name, help_intro, has_input_prompt=False,
219 shows_info=False, is_intro = False):
221 self.has_input_prompt = has_input_prompt
222 self.shows_info = shows_info
223 self.is_intro = is_intro
224 self.help_intro = help_intro
226 def __init__(self, host):
230 self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
231 self.mode_study = self.Mode('study', 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it.', shows_info=True)
232 self.mode_edit = self.Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.')
233 self.mode_annotate = self.Mode('annotate', 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.', has_input_prompt=True, shows_info=True)
234 self.mode_portal = self.Mode('portal', 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.', has_input_prompt=True, shows_info=True)
235 self.mode_chat = self.Mode('chat', 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:', has_input_prompt=True)
236 self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
237 self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
238 self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
239 self.mode_teleport = self.Mode('teleport', 'Follow the instructions to re-connect and log-in to another server, or enter anything else to abort.', has_input_prompt=True)
240 self.mode_password = self.Mode('password', 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.', has_input_prompt=True)
243 self.parser = Parser(self.game)
245 self.do_refresh = True
246 self.queue = queue.Queue()
247 self.login_name = None
248 self.map_mode = 'terrain'
249 self.password = 'foo'
250 self.switch_mode('waiting_for_server')
252 'switch_to_chat': 't',
253 'switch_to_play': 'p',
254 'switch_to_password': 'P',
255 'switch_to_annotate': 'M',
256 'switch_to_portal': 'T',
257 'switch_to_study': '?',
258 'switch_to_edit': 'm',
260 'toggle_map_mode': 'M',
261 'hex_move_upleft': 'w',
262 'hex_move_upright': 'e',
263 'hex_move_right': 'd',
264 'hex_move_downright': 'x',
265 'hex_move_downleft': 'y',
266 'hex_move_left': 'a',
267 'square_move_up': 'w',
268 'square_move_left': 'a',
269 'square_move_down': 's',
270 'square_move_right': 'd',
272 if os.path.isfile('config.json'):
273 with open('config.json', 'r') as f:
274 keys_conf = json.loads(f.read())
276 self.keys[k] = keys_conf[k]
277 self.show_help = False
278 self.disconnected = True
279 self.force_instant_connect = True
280 self.input_lines = []
282 curses.wrapper(self.loop)
289 def handle_recv(msg):
295 self.log_msg('@ attempting connect')
296 socket_client_class = PlomSocketClient
297 if self.host.startswith('ws://') or self.host.startswith('wss://'):
298 socket_client_class = WebSocketClient
300 self.socket = socket_client_class(handle_recv, self.host)
301 self.socket_thread = threading.Thread(target=self.socket.run)
302 self.socket_thread.start()
303 self.disconnected = False
304 self.game.thing_types = {}
305 self.socket.send('TASKS')
306 self.socket.send('THING_TYPES')
307 self.switch_mode('login')
308 except ConnectionRefusedError:
309 self.log_msg('@ server connect failure')
310 self.disconnected = True
311 self.switch_mode('waiting_for_server')
312 self.do_refresh = True
315 self.log_msg('@ attempting reconnect')
317 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
318 # conditions with ws4py, find out what exactly
319 self.switch_mode('waiting_for_server')
324 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
325 raise BrokenSocketConnection
326 self.socket.send(msg)
327 except (BrokenPipeError, BrokenSocketConnection):
328 self.log_msg('@ server disconnected :(')
329 self.disconnected = True
330 self.force_instant_connect = True
331 self.do_refresh = True
333 def log_msg(self, msg):
335 if len(self.log) > 100:
336 self.log = self.log[-100:]
338 def query_info(self):
339 self.send('GET_ANNOTATION ' + str(self.explorer))
341 def restore_input_values(self):
342 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
343 info = self.game.info_db[self.explorer]
346 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
347 self.input_ = self.game.portals[self.explorer]
348 elif self.mode.name == 'password':
349 self.input_ = self.password
351 def switch_mode(self, mode_name):
352 self.map_mode = 'terrain'
353 self.mode = getattr(self, 'mode_' + mode_name)
354 if self.mode.shows_info:
355 player = self.game.get_thing(self.game.player_id)
356 self.explorer = YX(player.position.y, player.position.x)
357 if self.mode.name == 'waiting_for_server':
358 self.log_msg('@ waiting for server …')
359 if self.mode.name == 'edit':
360 self.show_help = True
361 elif self.mode.name == 'login':
363 self.send('LOGIN ' + quote(self.login_name))
365 self.log_msg('@ enter username')
366 elif self.mode.name == 'teleport':
367 self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
368 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
369 self.restore_input_values()
371 def loop(self, stdscr):
374 def safe_addstr(y, x, line):
375 if y < self.size.y - 1 or x + len(line) < self.size.x:
376 stdscr.addstr(y, x, line)
377 else: # workaround to <https://stackoverflow.com/q/7063128>
378 cut_i = self.size.x - x - 1
380 last_char = line[cut_i]
381 stdscr.addstr(y, self.size.x - 2, last_char)
382 stdscr.insstr(y, self.size.x - 2, ' ')
383 stdscr.addstr(y, x, cut)
385 def handle_input(msg):
386 command, args = self.parser.parse(msg)
389 def msg_into_lines_of_width(msg, width):
393 for i in range(len(msg)):
394 if x >= width or msg[i] == "\n":
404 def reset_screen_size():
405 self.size = YX(*stdscr.getmaxyx())
406 self.size = self.size - YX(self.size.y % 4, 0)
407 self.size = self.size - YX(0, self.size.x % 4)
408 self.window_width = int(self.size.x / 2)
410 def recalc_input_lines():
411 if not self.mode.has_input_prompt:
412 self.input_lines = []
414 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
417 def move_explorer(direction):
418 target = self.game.map_geometry.move(self.explorer, direction)
420 self.explorer = target
427 for line in self.log:
428 lines += msg_into_lines_of_width(line, self.window_width)
431 max_y = self.size.y - len(self.input_lines)
432 for i in range(len(lines)):
433 if (i >= max_y - height_header):
435 safe_addstr(max_y - i - 1, self.window_width, lines[i])
438 if not self.game.turn_complete:
440 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
441 info = 'outside field of view'
442 if self.game.fov[pos_i] == '.':
443 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
444 for t in self.game.things:
445 if t.position == self.explorer:
446 info += 'THING: %s' % t.type_
447 if hasattr(t, 'name'):
448 info += ' (name: %s)' % t.name
450 if self.explorer in self.game.portals:
451 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
453 info += 'PORTAL: (none)\n'
454 if self.explorer in self.game.info_db:
455 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
457 info += 'ANNOTATION: waiting …'
458 lines = msg_into_lines_of_width(info, self.window_width)
460 for i in range(len(lines)):
461 y = height_header + i
462 if y >= self.size.y - len(self.input_lines):
464 safe_addstr(y, self.window_width, lines[i])
467 y = self.size.y - len(self.input_lines)
468 for i in range(len(self.input_lines)):
469 safe_addstr(y, self.window_width, self.input_lines[i])
473 if not self.game.turn_complete:
475 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
478 help = "hit [%s] for help" % self.keys['help']
479 if self.mode.has_input_prompt:
480 help = "enter /help for help"
481 safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
484 if not self.game.turn_complete:
487 map_content = self.game.map_content
488 if self.map_mode == 'control':
489 map_content = self.game.map_control_content
490 for y in range(self.game.map_geometry.size.y):
491 start = self.game.map_geometry.size.x * y
492 end = start + self.game.map_geometry.size.x
493 map_lines_split += [list(map_content[start:end])]
494 if self.map_mode == 'terrain':
495 for t in self.game.things:
496 symbol = self.game.thing_types[t.type_]
497 map_lines_split[t.position.y][t.position.x] = symbol
498 if self.mode.shows_info:
499 map_lines_split[self.explorer.y][self.explorer.x] = '?'
501 if type(self.game.map_geometry) == MapGeometryHex:
503 for line in map_lines_split:
504 map_lines += [indent*' ' + ' '.join(line)]
505 indent = 0 if indent else 1
507 for line in map_lines_split:
508 map_lines += [' '.join(line)]
509 window_center = YX(int(self.size.y / 2),
510 int(self.window_width / 2))
511 player = self.game.get_thing(self.game.player_id)
512 center = player.position
513 if self.mode.shows_info:
514 center = self.explorer
515 center = YX(center.y, center.x * 2)
516 offset = center - window_center
517 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
519 term_y = max(0, -offset.y)
520 term_x = max(0, -offset.x)
521 map_y = max(0, offset.y)
522 map_x = max(0, offset.x)
523 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
524 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
525 safe_addstr(term_y, term_x, to_draw)
530 content = "%s mode help\n\n%s\n\n" % (self.mode.name,
531 self.mode.help_intro)
532 if self.mode == self.mode_play:
533 content += "Available actions:\n"
534 if 'MOVE' in self.game.tasks:
535 content += "[%s] – move player\n" % ','.join(self.movement_keys)
536 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
537 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
538 content += 'Other modes available from here:\n'
539 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
540 content += '[%s] – study mode\n' % self.keys['switch_to_study']
541 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
542 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
543 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
544 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
545 elif self.mode == self.mode_study:
546 content += 'Available actions:\n'
547 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
548 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
549 content += '\n\nOther modes available from here:'
550 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
551 content += '[%s] – play mode\n' % self.keys['switch_to_play']
552 elif self.mode == self.mode_chat:
553 content += '/nick NAME – re-name yourself to NAME\n'
554 #content += '/msg USER TEXT – send TEXT to USER\n'
555 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
556 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
557 for i in range(self.size.y):
559 self.window_width * (not self.mode.has_input_prompt),
560 ' '*self.window_width)
562 for line in content.split('\n'):
563 lines += msg_into_lines_of_width(line, self.window_width)
564 for i in range(len(lines)):
568 self.window_width * (not self.mode.has_input_prompt),
573 if self.mode.has_input_prompt:
576 if self.mode.shows_info:
581 if not self.mode.is_intro:
587 curses.curs_set(False) # hide cursor
588 curses.use_default_colors();
591 self.explorer = YX(0, 0)
594 interval = datetime.timedelta(seconds=5)
595 last_ping = datetime.datetime.now() - interval
597 if self.disconnected and self.force_instant_connect:
598 self.force_instant_connect = False
600 now = datetime.datetime.now()
601 if now - last_ping > interval:
602 if self.disconnected:
609 self.do_refresh = False
612 msg = self.queue.get(block=False)
617 key = stdscr.getkey()
618 self.do_refresh = True
621 self.show_help = False
622 if key == 'KEY_RESIZE':
624 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
625 self.input_ = self.input_[:-1]
626 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
627 self.show_help = True
629 self.restore_input_values()
630 elif self.mode.has_input_prompt and key != '\n': # Return key
632 max_length = self.window_width * self.size.y - len(input_prompt) - 1
633 if len(self.input_) > max_length:
634 self.input_ = self.input_[:max_length]
635 elif key == self.keys['help'] and self.mode != self.mode_edit:
636 self.show_help = True
637 elif self.mode == self.mode_login and key == '\n':
638 self.login_name = self.input_
639 self.send('LOGIN ' + quote(self.input_))
641 elif self.mode == self.mode_password and key == '\n':
642 if self.input_ == '':
644 self.password = self.input_
646 self.switch_mode('play')
647 elif self.mode == self.mode_chat and key == '\n':
648 if self.input_ == '':
650 if self.input_[0] == '/': # FIXME fails on empty input
651 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
652 self.switch_mode('play')
653 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
654 self.switch_mode('study')
655 elif self.input_.startswith('/nick'):
656 tokens = self.input_.split(maxsplit=1)
658 self.send('NICK ' + quote(tokens[1]))
660 self.log_msg('? need login name')
661 #elif self.input_.startswith('/msg'):
662 # tokens = self.input_.split(maxsplit=2)
663 # if len(tokens) == 3:
664 # self.send('QUERY %s %s' % (quote(tokens[1]),
667 # self.log_msg('? need message target and message')
669 self.log_msg('? unknown command')
671 self.send('ALL ' + quote(self.input_))
673 elif self.mode == self.mode_annotate and key == '\n':
674 if self.input_ == '':
676 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
677 quote(self.password)))
679 self.switch_mode('play')
680 elif self.mode == self.mode_portal and key == '\n':
681 if self.input_ == '':
683 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
684 quote(self.password)))
686 self.switch_mode('play')
687 elif self.mode == self.mode_teleport and key == '\n':
688 if self.input_ == 'YES!':
689 self.host = self.teleport_target_host
692 self.log_msg('@ teleport aborted')
693 self.switch_mode('play')
695 elif self.mode == self.mode_study:
696 if key == self.keys['switch_to_chat']:
697 self.switch_mode('chat')
698 elif key == self.keys['switch_to_play']:
699 self.switch_mode('play')
700 elif key == self.keys['toggle_map_mode']:
701 if self.map_mode == 'terrain':
702 self.map_mode = 'control'
704 self.map_mode = 'terrain'
705 elif key in self.movement_keys:
706 move_explorer(self.movement_keys[key])
707 elif self.mode == self.mode_play:
708 if key == self.keys['switch_to_chat']:
709 self.switch_mode('chat')
710 elif key == self.keys['switch_to_study']:
711 self.switch_mode('study')
712 elif key == self.keys['switch_to_annotate']:
713 self.switch_mode('annotate')
714 elif key == self.keys['switch_to_portal']:
715 self.switch_mode('portal')
716 elif key == self.keys['switch_to_password']:
717 self.switch_mode('password')
718 if key == self.keys['switch_to_edit'] and\
719 'WRITE' in self.game.tasks:
720 self.switch_mode('edit')
721 elif key == self.keys['flatten'] and\
722 'FLATTEN_SURROUNDINGS' in self.game.tasks:
723 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
724 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
725 self.send('TASK:MOVE ' + self.movement_keys[key])
726 elif self.mode == self.mode_edit:
727 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
728 self.switch_mode('play')
730 TUI('localhost:5000')