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