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