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