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