home · contact · privacy
Simplify using the curses client to connect to current demo server.
[plomrogue2] / rogue_chat_curses.py
1 #!/usr/bin/env python3
2 import curses
3 import queue
4 import threading
5 import time
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
12
13 from ws4py.client import WebSocketBaseClient
14 class WebSocketClient(WebSocketBaseClient):
15
16     def __init__(self, recv_handler, *args, **kwargs):
17         super().__init__(*args, **kwargs)
18         self.recv_handler = recv_handler
19         self.connect()
20
21     def received_message(self, message):
22         if message.is_text:
23             message = str(message)
24             self.recv_handler(message)
25
26     @property
27     def plom_closed(self):
28         return self.client_terminated
29
30 from plomrogue.io_tcp import PlomSocket
31 class PlomSocketClient(PlomSocket):
32
33     def __init__(self, recv_handler, url):
34         import socket
35         self.recv_handler = recv_handler
36         host, port = url.split(':')
37         super().__init__(socket.create_connection((host, port)))
38
39     def close(self):
40         self.socket.close()
41
42     def run(self):
43         import ssl
44         try:
45             for msg in self.recv():
46                 if msg == 'NEED_SSL':
47                     self.socket = ssl.wrap_socket(self.socket)
48                     continue
49                 self.recv_handler(msg)
50         except BrokenSocketConnection:
51             pass  # we assume socket will be known as dead by now
52
53 def cmd_TURN(game, n):
54     game.turn = n
55     game.things = []
56     game.portals = {}
57     game.turn_complete = False
58 cmd_TURN.argtypes = 'int:nonneg'
59
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 = ''
65
66 def cmd_CHAT(game, msg):
67     game.tui.log_msg('# ' + msg)
68     game.tui.do_refresh = True
69 cmd_CHAT.argtypes = 'string'
70
71 def cmd_PLAYER_ID(game, player_id):
72     game.player_id = player_id
73 cmd_PLAYER_ID.argtypes = 'int:nonneg'
74
75 def cmd_THING(game, yx, thing_type, thing_id):
76     t = game.get_thing(thing_id)
77     if not t:
78         t = ThingBase(game, thing_id)
79         game.things += [t]
80     t.position = yx
81     t.type_ = thing_type
82 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
83
84 def cmd_THING_NAME(game, thing_id, name):
85     t = game.get_thing(thing_id)
86     if t:
87         t.name = name
88 cmd_THING_NAME.argtypes = 'int:nonneg string'
89
90 def cmd_THING_CHAR(game, thing_id, c):
91     t = game.get_thing(thing_id)
92     if t:
93         t.player_char = c
94 cmd_THING_CHAR.argtypes = 'int:nonneg char'
95
96 def cmd_MAP(game, geometry, size, content):
97     map_geometry_class = globals()['MapGeometry' + geometry]
98     game.map_geometry = map_geometry_class(size)
99     game.map_content = content
100     if type(game.map_geometry) == MapGeometrySquare:
101         game.tui.movement_keys = {
102             game.tui.keys['square_move_up']: 'UP',
103             game.tui.keys['square_move_left']: 'LEFT',
104             game.tui.keys['square_move_down']: 'DOWN',
105             game.tui.keys['square_move_right']: 'RIGHT',
106         }
107     elif type(game.map_geometry) == MapGeometryHex:
108         game.tui.movement_keys = {
109             game.tui.keys['hex_move_upleft']: 'UPLEFT',
110             game.tui.keys['hex_move_upright']: 'UPRIGHT',
111             game.tui.keys['hex_move_right']: 'RIGHT',
112             game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
113             game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
114             game.tui.keys['hex_move_left']: 'LEFT',
115         }
116 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
117
118 def cmd_FOV(game, content):
119     game.fov = content
120 cmd_FOV.argtypes = 'string'
121
122 def cmd_MAP_CONTROL(game, content):
123     game.map_control_content = content
124 cmd_MAP_CONTROL.argtypes = 'string'
125
126 def cmd_GAME_STATE_COMPLETE(game):
127     game.info_db = {}
128     if game.tui.mode.name == 'post_login_wait':
129         game.tui.switch_mode('play')
130     if game.tui.mode.shows_info:
131         game.tui.query_info()
132     game.turn_complete = True
133     game.tui.do_refresh = True
134 cmd_GAME_STATE_COMPLETE.argtypes = ''
135
136 def cmd_PORTAL(game, position, msg):
137     game.portals[position] = msg
138 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
139
140 def cmd_PLAY_ERROR(game, msg):
141     game.tui.flash()
142     game.tui.do_refresh = True
143 cmd_PLAY_ERROR.argtypes = 'string'
144
145 def cmd_GAME_ERROR(game, msg):
146     game.tui.log_msg('? game error: ' + msg)
147     game.tui.do_refresh = True
148 cmd_GAME_ERROR.argtypes = 'string'
149
150 def cmd_ARGUMENT_ERROR(game, msg):
151     game.tui.log_msg('? syntax error: ' + msg)
152     game.tui.do_refresh = True
153 cmd_ARGUMENT_ERROR.argtypes = 'string'
154
155 def cmd_ANNOTATION(game, position, msg):
156     game.info_db[position] = msg
157     if game.tui.mode.shows_info:
158         game.tui.do_refresh = True
159 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
160
161 def cmd_TASKS(game, tasks_comma_separated):
162     game.tasks = tasks_comma_separated.split(',')
163 cmd_TASKS.argtypes = 'string'
164
165 def cmd_THING_TYPE(game, thing_type, symbol_hint):
166     game.thing_types[thing_type] = symbol_hint
167 cmd_THING_TYPE.argtypes = 'string char'
168
169 def cmd_TERRAIN(game, terrain_char, terrain_desc):
170     game.terrains[terrain_char] = terrain_desc
171 cmd_TERRAIN.argtypes = 'char string'
172
173 def cmd_PONG(game):
174     pass
175 cmd_PONG.argtypes = ''
176
177 class Game(GameBase):
178     turn_complete = False
179     tasks = {}
180     thing_types = {}
181
182     def __init__(self, *args, **kwargs):
183         super().__init__(*args, **kwargs)
184         self.register_command(cmd_LOGIN_OK)
185         self.register_command(cmd_PONG)
186         self.register_command(cmd_CHAT)
187         self.register_command(cmd_PLAYER_ID)
188         self.register_command(cmd_TURN)
189         self.register_command(cmd_THING)
190         self.register_command(cmd_THING_TYPE)
191         self.register_command(cmd_THING_NAME)
192         self.register_command(cmd_THING_CHAR)
193         self.register_command(cmd_TERRAIN)
194         self.register_command(cmd_MAP)
195         self.register_command(cmd_MAP_CONTROL)
196         self.register_command(cmd_PORTAL)
197         self.register_command(cmd_ANNOTATION)
198         self.register_command(cmd_GAME_STATE_COMPLETE)
199         self.register_command(cmd_ARGUMENT_ERROR)
200         self.register_command(cmd_GAME_ERROR)
201         self.register_command(cmd_PLAY_ERROR)
202         self.register_command(cmd_TASKS)
203         self.register_command(cmd_FOV)
204         self.map_content = ''
205         self.player_id = -1
206         self.info_db = {}
207         self.portals = {}
208         self.terrains = {}
209
210     def get_string_options(self, string_option_type):
211         if string_option_type == 'map_geometry':
212             return ['Hex', 'Square']
213         elif string_option_type == 'thing_type':
214             return self.thing_types.keys()
215         return None
216
217     def get_command(self, command_name):
218         from functools import partial
219         f = partial(self.commands[command_name], self)
220         f.argtypes = self.commands[command_name].argtypes
221         return f
222
223 class TUI:
224
225     class Mode:
226
227         def __init__(self, name, help_intro, has_input_prompt=False,
228                      shows_info=False, is_intro = False):
229             self.name = name
230             self.has_input_prompt = has_input_prompt
231             self.shows_info = shows_info
232             self.is_intro = is_intro
233             self.help_intro = help_intro
234
235     def __init__(self, host):
236         import os
237         import json
238         self.host = host
239         self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
240         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)
241         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.')
242         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)
243         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)
244         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)
245         self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
246         self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
247         self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
248         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)
249         self.game = Game()
250         self.game.tui = self
251         self.parser = Parser(self.game)
252         self.log = []
253         self.do_refresh = True
254         self.queue = queue.Queue()
255         self.login_name = None
256         self.map_mode = 'terrain'
257         self.password = 'foo'
258         self.switch_mode('waiting_for_server')
259         self.keys = {
260             'switch_to_chat': 't',
261             'switch_to_play': 'p',
262             'switch_to_password': 'P',
263             'switch_to_annotate': 'M',
264             'switch_to_portal': 'T',
265             'switch_to_study': '?',
266             'switch_to_edit': 'm',
267             'flatten': 'F',
268             'take_thing': 'z',
269             'drop_thing': 'u',
270             'teleport': 'p',
271             'toggle_map_mode': 'M',
272             'hex_move_upleft': 'w',
273             'hex_move_upright': 'e',
274             'hex_move_right': 'd',
275             'hex_move_downright': 'x',
276             'hex_move_downleft': 'y',
277             'hex_move_left': 'a',
278             'square_move_up': 'w',
279             'square_move_left': 'a',
280             'square_move_down': 's',
281             'square_move_right': 'd',
282         }
283         if os.path.isfile('config.json'):
284             with open('config.json', 'r') as f:
285                 keys_conf = json.loads(f.read())
286             for k in keys_conf:
287                 self.keys[k] = keys_conf[k]
288         self.show_help = False
289         self.disconnected = True
290         self.force_instant_connect = True
291         self.input_lines = []
292         self.fov = ''
293         curses.wrapper(self.loop)
294
295     def flash(self):
296         curses.flash()
297
298     def connect(self):
299
300         def handle_recv(msg):
301             if msg == 'BYE':
302                 self.socket.close()
303             else:
304                 self.queue.put(msg)
305
306         self.log_msg('@ attempting connect')
307         socket_client_class = PlomSocketClient
308         if self.host.startswith('ws://') or self.host.startswith('wss://'):
309             socket_client_class = WebSocketClient
310         try:
311             self.socket = socket_client_class(handle_recv, self.host)
312             self.socket_thread = threading.Thread(target=self.socket.run)
313             self.socket_thread.start()
314             self.disconnected = False
315             self.game.thing_types = {}
316             self.game.terrains = {}
317             self.socket.send('TASKS')
318             self.socket.send('TERRAINS')
319             self.socket.send('THING_TYPES')
320             self.switch_mode('login')
321         except ConnectionRefusedError:
322             self.log_msg('@ server connect failure')
323             self.disconnected = True
324             self.switch_mode('waiting_for_server')
325         self.do_refresh = True
326
327     def reconnect(self):
328         self.log_msg('@ attempting reconnect')
329         self.send('QUIT')
330         time.sleep(0.1)  # FIXME necessitated by some some strange SSL race
331                          # conditions with ws4py, find out what exactly
332         self.switch_mode('waiting_for_server')
333         self.connect()
334
335     def send(self, msg):
336         try:
337             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
338                 raise BrokenSocketConnection
339             self.socket.send(msg)
340         except (BrokenPipeError, BrokenSocketConnection):
341             self.log_msg('@ server disconnected :(')
342             self.disconnected = True
343             self.force_instant_connect = True
344             self.do_refresh = True
345
346     def log_msg(self, msg):
347         self.log += [msg]
348         if len(self.log) > 100:
349             self.log = self.log[-100:]
350
351     def query_info(self):
352         self.send('GET_ANNOTATION ' + str(self.explorer))
353
354     def restore_input_values(self):
355         if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
356             info = self.game.info_db[self.explorer]
357             if info != '(none)':
358                 self.input_ = info
359         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
360             self.input_ = self.game.portals[self.explorer]
361         elif self.mode.name == 'password':
362             self.input_ = self.password
363
364     def switch_mode(self, mode_name):
365         self.map_mode = 'terrain'
366         self.mode = getattr(self, 'mode_' + mode_name)
367         if self.mode.shows_info:
368             player = self.game.get_thing(self.game.player_id)
369             self.explorer = YX(player.position.y, player.position.x)
370         if self.mode.name == 'waiting_for_server':
371             self.log_msg('@ waiting for server …')
372         if self.mode.name == 'edit':
373             self.show_help = True
374         elif self.mode.name == 'login':
375             if self.login_name:
376                 self.send('LOGIN ' + quote(self.login_name))
377             else:
378                 self.log_msg('@ enter username')
379         self.restore_input_values()
380
381     def loop(self, stdscr):
382         import datetime
383
384         def safe_addstr(y, x, line):
385             if y < self.size.y - 1 or x + len(line) < self.size.x:
386                 stdscr.addstr(y, x, line)
387             else:  # workaround to <https://stackoverflow.com/q/7063128>
388                 cut_i = self.size.x - x - 1
389                 cut = line[:cut_i]
390                 last_char = line[cut_i]
391                 stdscr.addstr(y, self.size.x - 2, last_char)
392                 stdscr.insstr(y, self.size.x - 2, ' ')
393                 stdscr.addstr(y, x, cut)
394
395         def handle_input(msg):
396             command, args = self.parser.parse(msg)
397             command(*args)
398
399         def msg_into_lines_of_width(msg, width):
400             chunk = ''
401             lines = []
402             x = 0
403             for i in range(len(msg)):
404                 if x >= width or msg[i] == "\n":
405                     lines += [chunk]
406                     chunk = ''
407                     x = 0
408                 if msg[i] != "\n":
409                     chunk += msg[i]
410                 x += 1
411             lines += [chunk]
412             return lines
413
414         def reset_screen_size():
415             self.size = YX(*stdscr.getmaxyx())
416             self.size = self.size - YX(self.size.y % 4, 0)
417             self.size = self.size - YX(0, self.size.x % 4)
418             self.window_width = int(self.size.x / 2)
419
420         def recalc_input_lines():
421             if not self.mode.has_input_prompt:
422                 self.input_lines = []
423             else:
424                 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
425                                                            self.window_width)
426
427         def move_explorer(direction):
428             target = self.game.map_geometry.move(self.explorer, direction)
429             if target:
430                 self.explorer = target
431                 self.query_info()
432             else:
433                 self.flash()
434
435         def draw_history():
436             lines = []
437             for line in self.log:
438                 lines += msg_into_lines_of_width(line, self.window_width)
439             lines.reverse()
440             height_header = 2
441             max_y = self.size.y - len(self.input_lines)
442             for i in range(len(lines)):
443                 if (i >= max_y - height_header):
444                     break
445                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
446
447         def draw_info():
448             if not self.game.turn_complete:
449                 return
450             pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
451             info = 'outside field of view'
452             if self.game.fov[pos_i] == '.':
453                 terrain_char = self.game.map_content[pos_i]
454                 terrain_desc = '?'
455                 if terrain_char in self.game.terrains:
456                     terrain_desc = self.game.terrains[terrain_char]
457                 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
458                 for t in self.game.things:
459                     if t.position == self.explorer:
460                         info += 'THING: %s / %s' % (t.type_,
461                                                     self.game.thing_types[t.type_])
462                         if hasattr(t, 'player_char'):
463                             info += t.player_char
464                         if hasattr(t, 'name'):
465                             info += ' (%s)' % t.name
466                         info += '\n'
467                 if self.explorer in self.game.portals:
468                     info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
469                 else:
470                     info += 'PORTAL: (none)\n'
471                 if self.explorer in self.game.info_db:
472                     info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
473                 else:
474                     info += 'ANNOTATION: waiting …'
475             lines = msg_into_lines_of_width(info, self.window_width)
476             height_header = 2
477             for i in range(len(lines)):
478                 y = height_header + i
479                 if y >= self.size.y - len(self.input_lines):
480                     break
481                 safe_addstr(y, self.window_width, lines[i])
482
483         def draw_input():
484             y = self.size.y - len(self.input_lines)
485             for i in range(len(self.input_lines)):
486                 safe_addstr(y, self.window_width, self.input_lines[i])
487                 y += 1
488
489         def draw_turn():
490             if not self.game.turn_complete:
491                 return
492             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
493
494         def draw_mode():
495             help = "hit [%s] for help" % self.keys['help']
496             if self.mode.has_input_prompt:
497                 help = "enter /help for help"
498             safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
499
500         def draw_map():
501             if not self.game.turn_complete:
502                 return
503             map_lines_split = []
504             map_content = self.game.map_content
505             if self.map_mode == 'control':
506                 map_content = self.game.map_control_content
507             for y in range(self.game.map_geometry.size.y):
508                 start = self.game.map_geometry.size.x * y
509                 end = start + self.game.map_geometry.size.x
510                 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
511             if self.map_mode == 'terrain':
512                 for p in self.game.portals.keys():
513                     map_lines_split[p.y][p.x] = 'P '
514                 used_positions = []
515                 for t in self.game.things:
516                     symbol = self.game.thing_types[t.type_]
517                     meta_char = ' '
518                     if hasattr(t, 'player_char'):
519                         meta_char = t.player_char
520                     if t.position in used_positions:
521                         meta_char = '+'
522                     map_lines_split[t.position.y][t.position.x] = symbol + meta_char
523                     used_positions += [t.position]
524             if self.mode.shows_info:
525                 map_lines_split[self.explorer.y][self.explorer.x] = '??'
526             map_lines = []
527             if type(self.game.map_geometry) == MapGeometryHex:
528                 indent = 0
529                 for line in map_lines_split:
530                     map_lines += [indent*' ' + ''.join(line)]
531                     indent = 0 if indent else 1
532             else:
533                 for line in map_lines_split:
534                     map_lines += [''.join(line)]
535             window_center = YX(int(self.size.y / 2),
536                                int(self.window_width / 2))
537             player = self.game.get_thing(self.game.player_id)
538             center = player.position
539             if self.mode.shows_info:
540                 center = self.explorer
541             center = YX(center.y, center.x * 2)
542             offset = center - window_center
543             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
544                 offset += YX(0, 1)
545             term_y = max(0, -offset.y)
546             term_x = max(0, -offset.x)
547             map_y = max(0, offset.y)
548             map_x = max(0, offset.x)
549             while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
550                 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
551                 safe_addstr(term_y, term_x, to_draw)
552                 term_y += 1
553                 map_y += 1
554
555         def draw_help():
556             content = "%s mode help\n\n%s\n\n" % (self.mode.name,
557                                                   self.mode.help_intro)
558             if self.mode == self.mode_play:
559                 content += "Available actions:\n"
560                 if 'MOVE' in self.game.tasks:
561                     content += "[%s] – move player\n" % ','.join(self.movement_keys)
562                 if 'PICK_UP' in self.game.tasks:
563                     content += "[%s] – take thing under player\n" % self.keys['take_thing']
564                 if 'DROP' in self.game.tasks:
565                     content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
566                 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
567                     content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
568                 content += '[%s] – teleport to other space\n' % self.keys['teleport']
569                 content += 'Other modes available from here:\n'
570                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
571                 content += '[%s] – study mode\n' % self.keys['switch_to_study']
572                 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
573                 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
574                 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
575                 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
576             elif self.mode == self.mode_study:
577                 content += 'Available actions:\n'
578                 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
579                 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
580                 content += '\n\nOther modes available from here:'
581                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
582                 content += '[%s] – play mode\n' % self.keys['switch_to_play']
583             elif self.mode == self.mode_chat:
584                 content += '/nick NAME – re-name yourself to NAME\n'
585                 #content += '/msg USER TEXT – send TEXT to USER\n'
586                 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
587                 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
588             for i in range(self.size.y):
589                 safe_addstr(i,
590                             self.window_width * (not self.mode.has_input_prompt),
591                             ' '*self.window_width)
592             lines = []
593             for line in content.split('\n'):
594                 lines += msg_into_lines_of_width(line, self.window_width)
595             for i in range(len(lines)):
596                 if i >= self.size.y:
597                     break
598                 safe_addstr(i,
599                             self.window_width * (not self.mode.has_input_prompt),
600                             lines[i])
601
602         def draw_screen():
603             stdscr.clear()
604             if self.mode.has_input_prompt:
605                 recalc_input_lines()
606                 draw_input()
607             if self.mode.shows_info:
608                 draw_info()
609             else:
610                 draw_history()
611             draw_mode()
612             if not self.mode.is_intro:
613                 draw_turn()
614                 draw_map()
615             if self.show_help:
616                 draw_help()
617
618         curses.curs_set(False)  # hide cursor
619         curses.use_default_colors();
620         stdscr.timeout(10)
621         reset_screen_size()
622         self.explorer = YX(0, 0)
623         self.input_ = ''
624         input_prompt = '> '
625         interval = datetime.timedelta(seconds=5)
626         last_ping = datetime.datetime.now() - interval
627         while True:
628             if self.disconnected and self.force_instant_connect:
629                 self.force_instant_connect = False
630                 self.connect()
631             now = datetime.datetime.now()
632             if now - last_ping > interval:
633                 if self.disconnected:
634                     self.connect()
635                 else:
636                     self.send('PING')
637                 last_ping = now
638             if self.do_refresh:
639                 draw_screen()
640                 self.do_refresh = False
641             while True:
642                 try:
643                     msg = self.queue.get(block=False)
644                     handle_input(msg)
645                 except queue.Empty:
646                     break
647             try:
648                 key = stdscr.getkey()
649                 self.do_refresh = True
650             except curses.error:
651                 continue
652             self.show_help = False
653             if key == 'KEY_RESIZE':
654                 reset_screen_size()
655             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
656                 self.input_ = self.input_[:-1]
657             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
658                 self.show_help = True
659                 self.input_ = ""
660                 self.restore_input_values()
661             elif self.mode.has_input_prompt and key != '\n':  # Return key
662                 self.input_ += key
663                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
664                 if len(self.input_) > max_length:
665                     self.input_ = self.input_[:max_length]
666             elif key == self.keys['help'] and self.mode != self.mode_edit:
667                 self.show_help = True
668             elif self.mode == self.mode_login and key == '\n':
669                 self.login_name = self.input_
670                 self.send('LOGIN ' + quote(self.input_))
671                 self.input_ = ""
672             elif self.mode == self.mode_password and key == '\n':
673                 if self.input_ == '':
674                     self.input_ = ' '
675                 self.password = self.input_
676                 self.input_ = ""
677                 self.switch_mode('play')
678             elif self.mode == self.mode_chat and key == '\n':
679                 if self.input_ == '':
680                     continue
681                 if self.input_[0] == '/':  # FIXME fails on empty input
682                     if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
683                         self.switch_mode('play')
684                     elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
685                         self.switch_mode('study')
686                     elif self.input_.startswith('/nick'):
687                         tokens = self.input_.split(maxsplit=1)
688                         if len(tokens) == 2:
689                             self.send('NICK ' + quote(tokens[1]))
690                         else:
691                             self.log_msg('? need login name')
692                     #elif self.input_.startswith('/msg'):
693                     #    tokens = self.input_.split(maxsplit=2)
694                     #    if len(tokens) == 3:
695                     #        self.send('QUERY %s %s' % (quote(tokens[1]),
696                     #                                          quote(tokens[2])))
697                     #    else:
698                     #        self.log_msg('? need message target and message')
699                     else:
700                         self.log_msg('? unknown command')
701                 else:
702                     self.send('ALL ' + quote(self.input_))
703                 self.input_ = ""
704             elif self.mode == self.mode_annotate and key == '\n':
705                 if self.input_ == '':
706                     self.input_ = ' '
707                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
708                                                  quote(self.password)))
709                 self.input_ = ""
710                 self.switch_mode('play')
711             elif self.mode == self.mode_portal and key == '\n':
712                 if self.input_ == '':
713                     self.input_ = ' '
714                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
715                                                quote(self.password)))
716                 self.input_ = ""
717                 self.switch_mode('play')
718             elif self.mode == self.mode_study:
719                 if key == self.keys['switch_to_chat']:
720                     self.switch_mode('chat')
721                 elif key == self.keys['switch_to_play']:
722                     self.switch_mode('play')
723                 elif key == self.keys['toggle_map_mode']:
724                     if self.map_mode == 'terrain':
725                         self.map_mode = 'control'
726                     else:
727                         self.map_mode = 'terrain'
728                 elif key in self.movement_keys:
729                     move_explorer(self.movement_keys[key])
730             elif self.mode == self.mode_play:
731                 if key == self.keys['switch_to_chat']:
732                     self.switch_mode('chat')
733                 elif key == self.keys['switch_to_study']:
734                     self.switch_mode('study')
735                 elif key == self.keys['switch_to_annotate']:
736                     self.switch_mode('annotate')
737                 elif key == self.keys['switch_to_portal']:
738                     self.switch_mode('portal')
739                 elif key == self.keys['switch_to_password']:
740                     self.switch_mode('password')
741                 if key == self.keys['switch_to_edit'] and\
742                    'WRITE' in self.game.tasks:
743                     self.switch_mode('edit')
744                 elif key == self.keys['flatten'] and\
745                      'FLATTEN_SURROUNDINGS' in self.game.tasks:
746                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
747                 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
748                     self.send('TASK:PICK_UP')
749                 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
750                     self.send('TASK:DROP')
751                 elif key == self.keys['teleport']:
752                     player = self.game.get_thing(self.game.player_id)
753                     if player.position in self.game.portals:
754                         self.host = self.game.portals[player.position]
755                         self.reconnect()
756                     else:
757                         self.flash()
758                         self.log_msg('? not standing on portal')
759                 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
760                     self.send('TASK:MOVE ' + self.movement_keys[key])
761             elif self.mode == self.mode_edit:
762                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
763                 self.switch_mode('play')
764
765 #TUI('localhost:5000')
766 TUI('wss://plomlompom.com/rogue_chat/')