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