home · contact · privacy
In curses client, don't flash outside of refresh loop.
[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     if game.tui.mode.shows_info:
159         game.tui.do_refresh = True
160 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
161
162 def cmd_TASKS(game, tasks_comma_separated):
163     game.tasks = tasks_comma_separated.split(',')
164 cmd_TASKS.argtypes = 'string'
165
166 def cmd_THING_TYPE(game, thing_type, symbol_hint):
167     game.thing_types[thing_type] = symbol_hint
168 cmd_THING_TYPE.argtypes = 'string char'
169
170 def cmd_TERRAIN(game, terrain_char, terrain_desc):
171     game.terrains[terrain_char] = terrain_desc
172 cmd_TERRAIN.argtypes = 'char string'
173
174 def cmd_PONG(game):
175     pass
176 cmd_PONG.argtypes = ''
177
178 class Game(GameBase):
179     turn_complete = False
180     tasks = {}
181     thing_types = {}
182
183     def __init__(self, *args, **kwargs):
184         super().__init__(*args, **kwargs)
185         self.register_command(cmd_LOGIN_OK)
186         self.register_command(cmd_PONG)
187         self.register_command(cmd_CHAT)
188         self.register_command(cmd_PLAYER_ID)
189         self.register_command(cmd_TURN)
190         self.register_command(cmd_THING)
191         self.register_command(cmd_THING_TYPE)
192         self.register_command(cmd_THING_NAME)
193         self.register_command(cmd_THING_CHAR)
194         self.register_command(cmd_TERRAIN)
195         self.register_command(cmd_MAP)
196         self.register_command(cmd_MAP_CONTROL)
197         self.register_command(cmd_PORTAL)
198         self.register_command(cmd_ANNOTATION)
199         self.register_command(cmd_GAME_STATE_COMPLETE)
200         self.register_command(cmd_ARGUMENT_ERROR)
201         self.register_command(cmd_GAME_ERROR)
202         self.register_command(cmd_PLAY_ERROR)
203         self.register_command(cmd_TASKS)
204         self.register_command(cmd_FOV)
205         self.map_content = ''
206         self.player_id = -1
207         self.info_db = {}
208         self.portals = {}
209         self.terrains = {}
210
211     def get_string_options(self, string_option_type):
212         if string_option_type == 'map_geometry':
213             return ['Hex', 'Square']
214         elif string_option_type == 'thing_type':
215             return self.thing_types.keys()
216         return None
217
218     def get_command(self, command_name):
219         from functools import partial
220         f = partial(self.commands[command_name], self)
221         f.argtypes = self.commands[command_name].argtypes
222         return f
223
224 class TUI:
225
226     class Mode:
227
228         def __init__(self, name, help_intro, has_input_prompt=False,
229                      shows_info=False, is_intro = False):
230             self.name = name
231             self.has_input_prompt = has_input_prompt
232             self.shows_info = shows_info
233             self.is_intro = is_intro
234             self.help_intro = help_intro
235
236     def __init__(self, host):
237         import os
238         import json
239         self.host = host
240         self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
241         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)
242         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.')
243         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)
244         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)
245         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)
246         self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
247         self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
248         self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
249         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)
250         self.game = Game()
251         self.game.tui = self
252         self.parser = Parser(self.game)
253         self.log = []
254         self.do_refresh = True
255         self.queue = queue.Queue()
256         self.login_name = None
257         self.map_mode = 'terrain'
258         self.password = 'foo'
259         self.switch_mode('waiting_for_server')
260         self.keys = {
261             'switch_to_chat': 't',
262             'switch_to_play': 'p',
263             'switch_to_password': 'P',
264             'switch_to_annotate': 'M',
265             'switch_to_portal': 'T',
266             'switch_to_study': '?',
267             'switch_to_edit': 'm',
268             'flatten': 'F',
269             'take_thing': 'z',
270             'drop_thing': 'u',
271             'teleport': 'p',
272             'toggle_map_mode': 'M',
273             'hex_move_upleft': 'w',
274             'hex_move_upright': 'e',
275             'hex_move_right': 'd',
276             'hex_move_downright': 'x',
277             'hex_move_downleft': 'y',
278             'hex_move_left': 'a',
279             'square_move_up': 'w',
280             'square_move_left': 'a',
281             'square_move_down': 's',
282             'square_move_right': 'd',
283         }
284         if os.path.isfile('config.json'):
285             with open('config.json', 'r') as f:
286                 keys_conf = json.loads(f.read())
287             for k in keys_conf:
288                 self.keys[k] = keys_conf[k]
289         self.show_help = False
290         self.disconnected = True
291         self.force_instant_connect = True
292         self.input_lines = []
293         self.fov = ''
294         self.flash = False
295         curses.wrapper(self.loop)
296
297     def connect(self):
298
299         def handle_recv(msg):
300             if msg == 'BYE':
301                 self.socket.close()
302             else:
303                 self.queue.put(msg)
304
305         self.log_msg('@ attempting connect')
306         socket_client_class = PlomSocketClient
307         if self.host.startswith('ws://') or self.host.startswith('wss://'):
308             socket_client_class = WebSocketClient
309         try:
310             self.socket = socket_client_class(handle_recv, self.host)
311             self.socket_thread = threading.Thread(target=self.socket.run)
312             self.socket_thread.start()
313             self.disconnected = False
314             self.game.thing_types = {}
315             self.game.terrains = {}
316             self.socket.send('TASKS')
317             self.socket.send('TERRAINS')
318             self.socket.send('THING_TYPES')
319             self.switch_mode('login')
320         except ConnectionRefusedError:
321             self.log_msg('@ server connect failure')
322             self.disconnected = True
323             self.switch_mode('waiting_for_server')
324         self.do_refresh = True
325
326     def reconnect(self):
327         self.log_msg('@ attempting reconnect')
328         self.send('QUIT')
329         time.sleep(0.1)  # FIXME necessitated by some some strange SSL race
330                          # conditions with ws4py, find out what exactly
331         self.switch_mode('waiting_for_server')
332         self.connect()
333
334     def send(self, msg):
335         try:
336             if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
337                 raise BrokenSocketConnection
338             self.socket.send(msg)
339         except (BrokenPipeError, BrokenSocketConnection):
340             self.log_msg('@ server disconnected :(')
341             self.disconnected = True
342             self.force_instant_connect = True
343             self.do_refresh = True
344
345     def log_msg(self, msg):
346         self.log += [msg]
347         if len(self.log) > 100:
348             self.log = self.log[-100:]
349
350     def query_info(self):
351         self.send('GET_ANNOTATION ' + str(self.explorer))
352
353     def restore_input_values(self):
354         if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
355             info = self.game.info_db[self.explorer]
356             if info != '(none)':
357                 self.input_ = info
358         elif self.mode.name == 'portal' and self.explorer in self.game.portals:
359             self.input_ = self.game.portals[self.explorer]
360         elif self.mode.name == 'password':
361             self.input_ = self.password
362
363     def switch_mode(self, mode_name):
364         self.map_mode = 'terrain'
365         self.mode = getattr(self, 'mode_' + mode_name)
366         if self.mode.shows_info:
367             player = self.game.get_thing(self.game.player_id)
368             self.explorer = YX(player.position.y, player.position.x)
369         if self.mode.name == 'waiting_for_server':
370             self.log_msg('@ waiting for server …')
371         if self.mode.name == 'edit':
372             self.show_help = True
373         elif self.mode.name == 'login':
374             if self.login_name:
375                 self.send('LOGIN ' + quote(self.login_name))
376             else:
377                 self.log_msg('@ enter username')
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 = True
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                 terrain_char = self.game.map_content[pos_i]
453                 terrain_desc = '?'
454                 if terrain_char in self.game.terrains:
455                     terrain_desc = self.game.terrains[terrain_char]
456                 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
457                 for t in self.game.things:
458                     if t.position == self.explorer:
459                         info += 'THING: %s / %s' % (t.type_,
460                                                     self.game.thing_types[t.type_])
461                         if hasattr(t, 'player_char'):
462                             info += t.player_char
463                         if hasattr(t, 'name'):
464                             info += ' (%s)' % t.name
465                         info += '\n'
466                 if self.explorer in self.game.portals:
467                     info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
468                 else:
469                     info += 'PORTAL: (none)\n'
470                 if self.explorer in self.game.info_db:
471                     info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
472                 else:
473                     info += 'ANNOTATION: waiting …'
474             lines = msg_into_lines_of_width(info, self.window_width)
475             height_header = 2
476             for i in range(len(lines)):
477                 y = height_header + i
478                 if y >= self.size.y - len(self.input_lines):
479                     break
480                 safe_addstr(y, self.window_width, lines[i])
481
482         def draw_input():
483             y = self.size.y - len(self.input_lines)
484             for i in range(len(self.input_lines)):
485                 safe_addstr(y, self.window_width, self.input_lines[i])
486                 y += 1
487
488         def draw_turn():
489             if not self.game.turn_complete:
490                 return
491             safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
492
493         def draw_mode():
494             help = "hit [%s] for help" % self.keys['help']
495             if self.mode.has_input_prompt:
496                 help = "enter /help for help"
497             safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
498
499         def draw_map():
500             if not self.game.turn_complete:
501                 return
502             map_lines_split = []
503             map_content = self.game.map_content
504             if self.map_mode == 'control':
505                 map_content = self.game.map_control_content
506             for y in range(self.game.map_geometry.size.y):
507                 start = self.game.map_geometry.size.x * y
508                 end = start + self.game.map_geometry.size.x
509                 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
510             if self.map_mode == 'terrain':
511                 for p in self.game.portals.keys():
512                     map_lines_split[p.y][p.x] = 'P '
513                 used_positions = []
514                 for t in self.game.things:
515                     symbol = self.game.thing_types[t.type_]
516                     meta_char = ' '
517                     if hasattr(t, 'player_char'):
518                         meta_char = t.player_char
519                     if t.position in used_positions:
520                         meta_char = '+'
521                     map_lines_split[t.position.y][t.position.x] = symbol + meta_char
522                     used_positions += [t.position]
523             if self.mode.shows_info:
524                 map_lines_split[self.explorer.y][self.explorer.x] = '??'
525             map_lines = []
526             if type(self.game.map_geometry) == MapGeometryHex:
527                 indent = 0
528                 for line in map_lines_split:
529                     map_lines += [indent*' ' + ''.join(line)]
530                     indent = 0 if indent else 1
531             else:
532                 for line in map_lines_split:
533                     map_lines += [''.join(line)]
534             window_center = YX(int(self.size.y / 2),
535                                int(self.window_width / 2))
536             player = self.game.get_thing(self.game.player_id)
537             center = player.position
538             if self.mode.shows_info:
539                 center = self.explorer
540             center = YX(center.y, center.x * 2)
541             offset = center - window_center
542             if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
543                 offset += YX(0, 1)
544             term_y = max(0, -offset.y)
545             term_x = max(0, -offset.x)
546             map_y = max(0, offset.y)
547             map_x = max(0, offset.x)
548             while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
549                 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
550                 safe_addstr(term_y, term_x, to_draw)
551                 term_y += 1
552                 map_y += 1
553
554         def draw_help():
555             content = "%s mode help\n\n%s\n\n" % (self.mode.name,
556                                                   self.mode.help_intro)
557             if self.mode == self.mode_play:
558                 content += "Available actions:\n"
559                 if 'MOVE' in self.game.tasks:
560                     content += "[%s] – move player\n" % ','.join(self.movement_keys)
561                 if 'PICK_UP' in self.game.tasks:
562                     content += "[%s] – take thing under player\n" % self.keys['take_thing']
563                 if 'DROP' in self.game.tasks:
564                     content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
565                 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
566                     content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
567                 content += '[%s] – teleport to other space\n' % self.keys['teleport']
568                 content += 'Other modes available from here:\n'
569                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
570                 content += '[%s] – study mode\n' % self.keys['switch_to_study']
571                 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
572                 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
573                 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
574                 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
575             elif self.mode == self.mode_study:
576                 content += 'Available actions:\n'
577                 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
578                 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
579                 content += '\n\nOther modes available from here:'
580                 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
581                 content += '[%s] – play mode\n' % self.keys['switch_to_play']
582             elif self.mode == self.mode_chat:
583                 content += '/nick NAME – re-name yourself to NAME\n'
584                 #content += '/msg USER TEXT – send TEXT to USER\n'
585                 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
586                 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
587             for i in range(self.size.y):
588                 safe_addstr(i,
589                             self.window_width * (not self.mode.has_input_prompt),
590                             ' '*self.window_width)
591             lines = []
592             for line in content.split('\n'):
593                 lines += msg_into_lines_of_width(line, self.window_width)
594             for i in range(len(lines)):
595                 if i >= self.size.y:
596                     break
597                 safe_addstr(i,
598                             self.window_width * (not self.mode.has_input_prompt),
599                             lines[i])
600
601         def draw_screen():
602             stdscr.clear()
603             if self.mode.has_input_prompt:
604                 recalc_input_lines()
605                 draw_input()
606             if self.mode.shows_info:
607                 draw_info()
608             else:
609                 draw_history()
610             draw_mode()
611             if not self.mode.is_intro:
612                 draw_turn()
613                 draw_map()
614             if self.show_help:
615                 draw_help()
616
617         curses.curs_set(False)  # hide cursor
618         curses.use_default_colors();
619         stdscr.timeout(10)
620         reset_screen_size()
621         self.explorer = YX(0, 0)
622         self.input_ = ''
623         input_prompt = '> '
624         interval = datetime.timedelta(seconds=5)
625         last_ping = datetime.datetime.now() - interval
626         while True:
627             if self.disconnected and self.force_instant_connect:
628                 self.force_instant_connect = False
629                 self.connect()
630             now = datetime.datetime.now()
631             if now - last_ping > interval:
632                 if self.disconnected:
633                     self.connect()
634                 else:
635                     self.send('PING')
636                 last_ping = now
637             if self.flash:
638                 curses.flash()
639                 self.flash = False
640             if self.do_refresh:
641                 draw_screen()
642                 self.do_refresh = False
643             while True:
644                 try:
645                     msg = self.queue.get(block=False)
646                     handle_input(msg)
647                 except queue.Empty:
648                     break
649             try:
650                 key = stdscr.getkey()
651                 self.do_refresh = True
652             except curses.error:
653                 continue
654             self.show_help = False
655             if key == 'KEY_RESIZE':
656                 reset_screen_size()
657             elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
658                 self.input_ = self.input_[:-1]
659             elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
660                 self.show_help = True
661                 self.input_ = ""
662                 self.restore_input_values()
663             elif self.mode.has_input_prompt and key != '\n':  # Return key
664                 self.input_ += key
665                 max_length = self.window_width * self.size.y - len(input_prompt) - 1
666                 if len(self.input_) > max_length:
667                     self.input_ = self.input_[:max_length]
668             elif key == self.keys['help'] and self.mode != self.mode_edit:
669                 self.show_help = True
670             elif self.mode == self.mode_login and key == '\n':
671                 self.login_name = self.input_
672                 self.send('LOGIN ' + quote(self.input_))
673                 self.input_ = ""
674             elif self.mode == self.mode_password and key == '\n':
675                 if self.input_ == '':
676                     self.input_ = ' '
677                 self.password = self.input_
678                 self.input_ = ""
679                 self.switch_mode('play')
680             elif self.mode == self.mode_chat and key == '\n':
681                 if self.input_ == '':
682                     continue
683                 if self.input_[0] == '/':  # FIXME fails on empty input
684                     if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
685                         self.switch_mode('play')
686                     elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
687                         self.switch_mode('study')
688                     elif self.input_.startswith('/nick'):
689                         tokens = self.input_.split(maxsplit=1)
690                         if len(tokens) == 2:
691                             self.send('NICK ' + quote(tokens[1]))
692                         else:
693                             self.log_msg('? need login name')
694                     else:
695                         self.log_msg('? unknown command')
696                 else:
697                     self.send('ALL ' + quote(self.input_))
698                 self.input_ = ""
699             elif self.mode == self.mode_annotate and key == '\n':
700                 if self.input_ == '':
701                     self.input_ = ' '
702                 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
703                                                  quote(self.password)))
704                 self.input_ = ""
705                 self.switch_mode('play')
706             elif self.mode == self.mode_portal and key == '\n':
707                 if self.input_ == '':
708                     self.input_ = ' '
709                 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
710                                                quote(self.password)))
711                 self.input_ = ""
712                 self.switch_mode('play')
713             elif self.mode == self.mode_study:
714                 if key == self.keys['switch_to_chat']:
715                     self.switch_mode('chat')
716                 elif key == self.keys['switch_to_play']:
717                     self.switch_mode('play')
718                 elif key == self.keys['toggle_map_mode']:
719                     if self.map_mode == 'terrain':
720                         self.map_mode = 'control'
721                     else:
722                         self.map_mode = 'terrain'
723                 elif key in self.movement_keys:
724                     move_explorer(self.movement_keys[key])
725             elif self.mode == self.mode_play:
726                 if key == self.keys['switch_to_chat']:
727                     self.switch_mode('chat')
728                 elif key == self.keys['switch_to_study']:
729                     self.switch_mode('study')
730                 elif key == self.keys['switch_to_annotate']:
731                     self.switch_mode('annotate')
732                 elif key == self.keys['switch_to_portal']:
733                     self.switch_mode('portal')
734                 elif key == self.keys['switch_to_password']:
735                     self.switch_mode('password')
736                 if key == self.keys['switch_to_edit'] and\
737                    'WRITE' in self.game.tasks:
738                     self.switch_mode('edit')
739                 elif key == self.keys['flatten'] and\
740                      'FLATTEN_SURROUNDINGS' in self.game.tasks:
741                     self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
742                 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
743                     self.send('TASK:PICK_UP')
744                 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
745                     self.send('TASK:DROP')
746                 elif key == self.keys['teleport']:
747                     player = self.game.get_thing(self.game.player_id)
748                     if player.position in self.game.portals:
749                         self.host = self.game.portals[player.position]
750                         self.reconnect()
751                     else:
752                         self.flash = True
753                         self.log_msg('? not standing on portal')
754                 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
755                     self.send('TASK:MOVE ' + self.movement_keys[key])
756             elif self.mode == self.mode_edit:
757                 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
758                 self.switch_mode('play')
759
760 #TUI('localhost:5000')
761 TUI('wss://plomlompom.com/rogue_chat/')