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