home · contact · privacy
Add item picking and dropping.
[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             'take_thing': 'z',
261             'drop_thing': 'u',
262             'toggle_map_mode': 'M',
263             'hex_move_upleft': 'w',
264             'hex_move_upright': 'e',
265             'hex_move_right': 'd',
266             'hex_move_downright': 'x',
267             'hex_move_downleft': 'y',
268             'hex_move_left': 'a',
269             'square_move_up': 'w',
270             'square_move_left': 'a',
271             'square_move_down': 's',
272             'square_move_right': 'd',
273         }
274         if os.path.isfile('config.json'):
275             with open('config.json', 'r') as f:
276                 keys_conf = json.loads(f.read())
277             for k in keys_conf:
278                 self.keys[k] = keys_conf[k]
279         self.show_help = False
280         self.disconnected = True
281         self.force_instant_connect = True
282         self.input_lines = []
283         self.fov = ''
284         curses.wrapper(self.loop)
285
286     def flash(self):
287         curses.flash()
288
289     def connect(self):
290
291         def handle_recv(msg):
292             if msg == 'BYE':
293                 self.socket.close()
294             else:
295                 self.queue.put(msg)
296
297         self.log_msg('@ attempting connect')
298         socket_client_class = PlomSocketClient
299         if self.host.startswith('ws://') or self.host.startswith('wss://'):
300             socket_client_class = WebSocketClient
301         try:
302             self.socket = socket_client_class(handle_recv, self.host)
303             self.socket_thread = threading.Thread(target=self.socket.run)
304             self.socket_thread.start()
305             self.disconnected = False
306             self.game.thing_types = {}
307             self.socket.send('TASKS')
308             self.socket.send('THING_TYPES')
309             self.switch_mode('login')
310         except ConnectionRefusedError:
311             self.log_msg('@ server connect failure')
312             self.disconnected = True
313             self.switch_mode('waiting_for_server')
314         self.do_refresh = True
315
316     def reconnect(self):
317         self.log_msg('@ attempting reconnect')
318         self.send('QUIT')
319         time.sleep(0.1)  # FIXME necessitated by some some strange SSL race
320                          # conditions with ws4py, find out what exactly
321         self.switch_mode('waiting_for_server')
322         self.connect()
323
324     def send(self, msg):
325         try:
326             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
327                 raise BrokenSocketConnection
328             self.socket.send(msg)
329         except (BrokenPipeError, BrokenSocketConnection):
330             self.log_msg('@ server disconnected :(')
331             self.disconnected = True
332             self.force_instant_connect = True
333             self.do_refresh = True
334
335     def log_msg(self, msg):
336         self.log += [msg]
337         if len(self.log) > 100:
338             self.log = self.log[-100:]
339
340     def query_info(self):
341         self.send('GET_ANNOTATION ' + str(self.explorer))
342
343     def restore_input_values(self):
344         if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
345             info = self.game.info_db[self.explorer]
346             if info != '(none)':
347                 self.input_ = info
348         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
349             self.input_ = self.game.portals[self.explorer]
350         elif self.mode.name == 'password':
351             self.input_ = self.password
352
353     def switch_mode(self, mode_name):
354         self.map_mode = 'terrain'
355         self.mode = getattr(self, 'mode_' + mode_name)
356         if self.mode.shows_info:
357             player = self.game.get_thing(self.game.player_id)
358             self.explorer = YX(player.position.y, player.position.x)
359         if self.mode.name == 'waiting_for_server':
360             self.log_msg('@ waiting for server …')
361         if self.mode.name == 'edit':
362             self.show_help = True
363         elif self.mode.name == 'login':
364             if self.login_name:
365                 self.send('LOGIN ' + quote(self.login_name))
366             else:
367                 self.log_msg('@ enter username')
368         elif self.mode.name == 'teleport':
369             self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
370             self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
371         self.restore_input_values()
372
373     def loop(self, stdscr):
374         import datetime
375
376         def safe_addstr(y, x, line):
377             if y < self.size.y - 1 or x + len(line) < self.size.x:
378                 stdscr.addstr(y, x, line)
379             else:  # workaround to <https://stackoverflow.com/q/7063128>
380                 cut_i = self.size.x - x - 1
381                 cut = line[:cut_i]
382                 last_char = line[cut_i]
383                 stdscr.addstr(y, self.size.x - 2, last_char)
384                 stdscr.insstr(y, self.size.x - 2, ' ')
385                 stdscr.addstr(y, x, cut)
386
387         def handle_input(msg):
388             command, args = self.parser.parse(msg)
389             command(*args)
390
391         def msg_into_lines_of_width(msg, width):
392             chunk = ''
393             lines = []
394             x = 0
395             for i in range(len(msg)):
396                 if x >= width or msg[i] == "\n":
397                     lines += [chunk]
398                     chunk = ''
399                     x = 0
400                 if msg[i] != "\n":
401                     chunk += msg[i]
402                 x += 1
403             lines += [chunk]
404             return lines
405
406         def reset_screen_size():
407             self.size = YX(*stdscr.getmaxyx())
408             self.size = self.size - YX(self.size.y % 4, 0)
409             self.size = self.size - YX(0, self.size.x % 4)
410             self.window_width = int(self.size.x / 2)
411
412         def recalc_input_lines():
413             if not self.mode.has_input_prompt:
414                 self.input_lines = []
415             else:
416                 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
417                                                            self.window_width)
418
419         def move_explorer(direction):
420             target = self.game.map_geometry.move(self.explorer, direction)
421             if target:
422                 self.explorer = target
423                 self.query_info()
424             else:
425                 self.flash()
426
427         def draw_history():
428             lines = []
429             for line in self.log:
430                 lines += msg_into_lines_of_width(line, self.window_width)
431             lines.reverse()
432             height_header = 2
433             max_y = self.size.y - len(self.input_lines)
434             for i in range(len(lines)):
435                 if (i >= max_y - height_header):
436                     break
437                 safe_addstr(max_y - i - 1, self.window_width, lines[i])
438
439         def draw_info():
440             if not self.game.turn_complete:
441                 return
442             pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
443             info = 'outside field of view'
444             if self.game.fov[pos_i] == '.':
445                 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
446                 for t in self.game.things:
447                     if t.position == self.explorer:
448                         info += 'THING: %s' % t.type_
449                         if hasattr(t, 'name'):
450                             info += ' (name: %s)' % t.name
451                         info += '\n'
452                 if self.explorer in self.game.portals:
453                     info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
454                 else:
455                     info += 'PORTAL: (none)\n'
456                 if self.explorer in self.game.info_db:
457                     info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
458                 else:
459                     info += 'ANNOTATION: waiting …'
460             lines = msg_into_lines_of_width(info, self.window_width)
461             height_header = 2
462             for i in range(len(lines)):
463                 y = height_header + i
464                 if y >= self.size.y - len(self.input_lines):
465                     break
466                 safe_addstr(y, self.window_width, lines[i])
467
468         def draw_input():
469             y = self.size.y - len(self.input_lines)
470             for i in range(len(self.input_lines)):
471                 safe_addstr(y, self.window_width, self.input_lines[i])
472                 y += 1
473
474         def draw_turn():
475             if not self.game.turn_complete:
476                 return
477             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
478
479         def draw_mode():
480             help = "hit [%s] for help" % self.keys['help']
481             if self.mode.has_input_prompt:
482                 help = "enter /help for help"
483             safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
484
485         def draw_map():
486             if not self.game.turn_complete:
487                 return
488             map_lines_split = []
489             map_content = self.game.map_content
490             if self.map_mode == 'control':
491                 map_content = self.game.map_control_content
492             for y in range(self.game.map_geometry.size.y):
493                 start = self.game.map_geometry.size.x * y
494                 end = start + self.game.map_geometry.size.x
495                 map_lines_split += [list(map_content[start:end])]
496             if self.map_mode == 'terrain':
497                 for t in self.game.things:
498                     symbol = self.game.thing_types[t.type_]
499                     map_lines_split[t.position.y][t.position.x] = symbol
500             if self.mode.shows_info:
501                 map_lines_split[self.explorer.y][self.explorer.x] = '?'
502             map_lines = []
503             if type(self.game.map_geometry) == MapGeometryHex:
504                 indent = 0
505                 for line in map_lines_split:
506                     map_lines += [indent*' ' + ' '.join(line)]
507                     indent = 0 if indent else 1
508             else:
509                 for line in map_lines_split:
510                     map_lines += [' '.join(line)]
511             window_center = YX(int(self.size.y / 2),
512                                int(self.window_width / 2))
513             player = self.game.get_thing(self.game.player_id)
514             center = player.position
515             if self.mode.shows_info:
516                 center = self.explorer
517             center = YX(center.y, center.x * 2)
518             offset = center - window_center
519             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
520                 offset += YX(0, 1)
521             term_y = max(0, -offset.y)
522             term_x = max(0, -offset.x)
523             map_y = max(0, offset.y)
524             map_x = max(0, offset.x)
525             while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
526                 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
527                 safe_addstr(term_y, term_x, to_draw)
528                 term_y += 1
529                 map_y += 1
530
531         def draw_help():
532             content = "%s mode help\n\n%s\n\n" % (self.mode.name,
533                                                   self.mode.help_intro)
534             if self.mode == self.mode_play:
535                 content += "Available actions:\n"
536                 if 'MOVE' in self.game.tasks:
537                     content += "[%s] – move player\n" % ','.join(self.movement_keys)
538                 if 'PICK_UP' in self.game.tasks:
539                     content += "[%s] – take thing under player\n" % self.keys['take_thing']
540                 if 'DROP' in self.game.tasks:
541                     content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
542                 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
543                     content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
544                 content += 'Other modes available from here:\n'
545                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
546                 content += '[%s] – study mode\n' % self.keys['switch_to_study']
547                 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
548                 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
549                 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
550                 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
551             elif self.mode == self.mode_study:
552                 content += 'Available actions:\n'
553                 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
554                 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
555                 content += '\n\nOther modes available from here:'
556                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
557                 content += '[%s] – play mode\n' % self.keys['switch_to_play']
558             elif self.mode == self.mode_chat:
559                 content += '/nick NAME – re-name yourself to NAME\n'
560                 #content += '/msg USER TEXT – send TEXT to USER\n'
561                 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
562                 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
563             for i in range(self.size.y):
564                 safe_addstr(i,
565                             self.window_width * (not self.mode.has_input_prompt),
566                             ' '*self.window_width)
567             lines = []
568             for line in content.split('\n'):
569                 lines += msg_into_lines_of_width(line, self.window_width)
570             for i in range(len(lines)):
571                 if i >= self.size.y:
572                     break
573                 safe_addstr(i,
574                             self.window_width * (not self.mode.has_input_prompt),
575                             lines[i])
576
577         def draw_screen():
578             stdscr.clear()
579             if self.mode.has_input_prompt:
580                 recalc_input_lines()
581                 draw_input()
582             if self.mode.shows_info:
583                 draw_info()
584             else:
585                 draw_history()
586             draw_mode()
587             if not self.mode.is_intro:
588                 draw_turn()
589                 draw_map()
590             if self.show_help:
591                 draw_help()
592
593         curses.curs_set(False)  # hide cursor
594         curses.use_default_colors();
595         stdscr.timeout(10)
596         reset_screen_size()
597         self.explorer = YX(0, 0)
598         self.input_ = ''
599         input_prompt = '> '
600         interval = datetime.timedelta(seconds=5)
601         last_ping = datetime.datetime.now() - interval
602         while True:
603             if self.disconnected and self.force_instant_connect:
604                 self.force_instant_connect = False
605                 self.connect()
606             now = datetime.datetime.now()
607             if now - last_ping > interval:
608                 if self.disconnected:
609                     self.connect()
610                 else:
611                     self.send('PING')
612                 last_ping = now
613             if self.do_refresh:
614                 draw_screen()
615                 self.do_refresh = False
616             while True:
617                 try:
618                     msg = self.queue.get(block=False)
619                     handle_input(msg)
620                 except queue.Empty:
621                     break
622             try:
623                 key = stdscr.getkey()
624                 self.do_refresh = True
625             except curses.error:
626                 continue
627             self.show_help = False
628             if key == 'KEY_RESIZE':
629                 reset_screen_size()
630             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
631                 self.input_ = self.input_[:-1]
632             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
633                 self.show_help = True
634                 self.input_ = ""
635                 self.restore_input_values()
636             elif self.mode.has_input_prompt and key != '\n':  # Return key
637                 self.input_ += key
638                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
639                 if len(self.input_) > max_length:
640                     self.input_ = self.input_[:max_length]
641             elif key == self.keys['help'] and self.mode != self.mode_edit:
642                 self.show_help = True
643             elif self.mode == self.mode_login and key == '\n':
644                 self.login_name = self.input_
645                 self.send('LOGIN ' + quote(self.input_))
646                 self.input_ = ""
647             elif self.mode == self.mode_password and key == '\n':
648                 if self.input_ == '':
649                     self.input_ = ' '
650                 self.password = self.input_
651                 self.input_ = ""
652                 self.switch_mode('play')
653             elif self.mode == self.mode_chat and key == '\n':
654                 if self.input_ == '':
655                     continue
656                 if self.input_[0] == '/':  # FIXME fails on empty input
657                     if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
658                         self.switch_mode('play')
659                     elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
660                         self.switch_mode('study')
661                     elif self.input_.startswith('/nick'):
662                         tokens = self.input_.split(maxsplit=1)
663                         if len(tokens) == 2:
664                             self.send('NICK ' + quote(tokens[1]))
665                         else:
666                             self.log_msg('? need login name')
667                     #elif self.input_.startswith('/msg'):
668                     #    tokens = self.input_.split(maxsplit=2)
669                     #    if len(tokens) == 3:
670                     #        self.send('QUERY %s %s' % (quote(tokens[1]),
671                     #                                          quote(tokens[2])))
672                     #    else:
673                     #        self.log_msg('? need message target and message')
674                     else:
675                         self.log_msg('? unknown command')
676                 else:
677                     self.send('ALL ' + quote(self.input_))
678                 self.input_ = ""
679             elif self.mode == self.mode_annotate and key == '\n':
680                 if self.input_ == '':
681                     self.input_ = ' '
682                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
683                                                  quote(self.password)))
684                 self.input_ = ""
685                 self.switch_mode('play')
686             elif self.mode == self.mode_portal and key == '\n':
687                 if self.input_ == '':
688                     self.input_ = ' '
689                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
690                                                quote(self.password)))
691                 self.input_ = ""
692                 self.switch_mode('play')
693             elif self.mode == self.mode_teleport and key == '\n':
694                 if self.input_ == 'YES!':
695                     self.host = self.teleport_target_host
696                     self.reconnect()
697                 else:
698                     self.log_msg('@ teleport aborted')
699                     self.switch_mode('play')
700                 self.input_ = ''
701             elif self.mode == self.mode_study:
702                 if key == self.keys['switch_to_chat']:
703                     self.switch_mode('chat')
704                 elif key == self.keys['switch_to_play']:
705                     self.switch_mode('play')
706                 elif key == self.keys['toggle_map_mode']:
707                     if self.map_mode == 'terrain':
708                         self.map_mode = 'control'
709                     else:
710                         self.map_mode = 'terrain'
711                 elif key in self.movement_keys:
712                     move_explorer(self.movement_keys[key])
713             elif self.mode == self.mode_play:
714                 if key == self.keys['switch_to_chat']:
715                     self.switch_mode('chat')
716                 elif key == self.keys['switch_to_study']:
717                     self.switch_mode('study')
718                 elif key == self.keys['switch_to_annotate']:
719                     self.switch_mode('annotate')
720                 elif key == self.keys['switch_to_portal']:
721                     self.switch_mode('portal')
722                 elif key == self.keys['switch_to_password']:
723                     self.switch_mode('password')
724                 if key == self.keys['switch_to_edit'] and\
725                    'WRITE' in self.game.tasks:
726                     self.switch_mode('edit')
727                 elif key == self.keys['flatten'] and\
728                      'FLATTEN_SURROUNDINGS' in self.game.tasks:
729                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
730                 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
731                     self.send('TASK:PICK_UP')
732                 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
733                     self.send('TASK:DROP')
734                 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
735                     self.send('TASK:MOVE ' + self.movement_keys[key])
736             elif self.mode == self.mode_edit:
737                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
738                 self.switch_mode('play')
739
740 TUI('localhost:5000')