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