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