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
13 from ws4py.client import WebSocketBaseClient
14 class WebSocketClient(WebSocketBaseClient):
16 def __init__(self, recv_handler, *args, **kwargs):
17 super().__init__(*args, **kwargs)
18 self.recv_handler = recv_handler
21 def received_message(self, message):
23 message = str(message)
24 self.recv_handler(message)
27 def plom_closed(self):
28 return self.client_terminated
30 from plomrogue.io_tcp import PlomSocket
31 class PlomSocketClient(PlomSocket):
33 def __init__(self, recv_handler, url):
35 self.recv_handler = recv_handler
36 host, port = url.split(':')
37 super().__init__(socket.create_connection((host, port)))
45 for msg in self.recv():
47 self.socket = ssl.wrap_socket(self.socket)
49 self.recv_handler(msg)
50 except BrokenSocketConnection:
51 pass # we assume socket will be known as dead by now
53 def cmd_TURN(game, n):
59 game.turn_complete = False
60 cmd_TURN.argtypes = 'int:nonneg'
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 = ''
68 def cmd_CHAT(game, msg):
69 game.tui.log_msg('# ' + msg)
70 game.tui.do_refresh = True
71 cmd_CHAT.argtypes = 'string'
73 def cmd_PLAYER_ID(game, player_id):
74 game.player_id = player_id
75 cmd_PLAYER_ID.argtypes = 'int:nonneg'
77 def cmd_THING(game, yx, thing_type, thing_id):
78 t = game.get_thing(thing_id)
80 t = ThingBase(game, thing_id)
84 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
86 def cmd_THING_NAME(game, thing_id, name):
87 t = game.get_thing(thing_id)
90 cmd_THING_NAME.argtypes = 'int:nonneg string'
92 def cmd_THING_CHAR(game, thing_id, c):
93 t = game.get_thing(thing_id)
96 cmd_THING_CHAR.argtypes = 'int:nonneg char'
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',
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',
118 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
120 def cmd_FOV(game, content):
122 cmd_FOV.argtypes = 'string'
124 def cmd_MAP_CONTROL(game, content):
125 game.map_control_content = content
126 cmd_MAP_CONTROL.argtypes = 'string'
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 = ''
137 def cmd_PORTAL(game, position, msg):
138 game.portals[position] = msg
139 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
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'
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'
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'
157 def cmd_ANNOTATION_HINT(game, position):
158 game.info_hints += [position]
159 cmd_ANNOTATION_HINT.argtypes = 'yx_tuple:nonneg'
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'
168 def cmd_TASKS(game, tasks_comma_separated):
169 game.tasks = tasks_comma_separated.split(',')
170 cmd_TASKS.argtypes = 'string'
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'
176 def cmd_TERRAIN(game, terrain_char, terrain_desc):
177 game.terrains[terrain_char] = terrain_desc
178 cmd_TERRAIN.argtypes = 'char string'
182 cmd_PONG.argtypes = ''
184 class Game(GameBase):
185 turn_complete = False
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 = ''
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()
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
236 def __init__(self, name, help_intro, has_input_prompt=False,
237 shows_info=False, is_intro = False,
238 is_single_char_entry=False):
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
246 def __init__(self, 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.',
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.',
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)
287 self.parser = Parser(self.game)
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')
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',
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',
322 if os.path.isfile('config.json'):
323 with open('config.json', 'r') as f:
324 keys_conf = json.loads(f.read())
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 = []
333 curses.wrapper(self.loop)
337 def handle_recv(msg):
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
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
365 self.log_msg('@ attempting reconnect')
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')
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
383 def log_msg(self, msg):
385 if len(self.log) > 100:
386 self.log = self.log[-100:]
388 def query_info(self):
389 self.send('GET_ANNOTATION ' + str(self.explorer))
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]
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
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)
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':
414 self.send('LOGIN ' + quote(self.login_name))
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()
423 def loop(self, stdscr):
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
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)
437 def handle_input(msg):
438 command, args = self.parser.parse(msg)
441 def msg_into_lines_of_width(msg, width):
445 for i in range(len(msg)):
446 if x >= width or msg[i] == "\n":
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)
462 def recalc_input_lines():
463 if not self.mode.has_input_prompt:
464 self.input_lines = []
466 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
469 def move_explorer(direction):
470 target = self.game.map_geometry.move_yx(self.explorer, direction)
472 self.explorer = target
479 for line in self.log:
480 lines += msg_into_lines_of_width(line, self.window_width)
483 max_y = self.size.y - len(self.input_lines)
484 for i in range(len(lines)):
485 if (i >= max_y - height_header):
487 safe_addstr(max_y - i - 1, self.window_width, lines[i])
490 if not self.game.turn_complete:
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]
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
513 if self.explorer in self.game.portals:
514 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
516 info += 'PORTAL: (none)\n'
517 if self.explorer in self.game.info_db:
518 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
520 info += 'ANNOTATION: waiting …'
521 lines = msg_into_lines_of_width(info, self.window_width)
523 for i in range(len(lines)):
524 y = height_header + i
525 if y >= self.size.y - len(self.input_lines):
527 safe_addstr(y, self.window_width, lines[i])
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])
536 if not self.game.turn_complete:
538 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
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))
547 if not self.game.turn_complete:
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 '
564 for t in self.game.things:
565 symbol = self.game.thing_types[t.type_]
567 if hasattr(t, 'player_char'):
568 meta_char = t.player_char
569 if t.position in used_positions:
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] = '??'
576 if type(self.game.map_geometry) == MapGeometryHex:
578 for line in map_lines_split:
579 map_lines += [indent*' ' + ''.join(line)]
580 indent = 0 if indent else 1
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:
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)
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']
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):
641 self.window_width * (not self.mode.has_input_prompt),
642 ' '*self.window_width)
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)):
650 self.window_width * (not self.mode.has_input_prompt),
655 if self.mode.has_input_prompt:
658 if self.mode.shows_info:
663 if not self.mode.is_intro:
669 curses.curs_set(False) # hide cursor
670 curses.use_default_colors();
673 self.explorer = YX(0, 0)
676 interval = datetime.timedelta(seconds=5)
677 last_ping = datetime.datetime.now() - interval
679 if self.disconnected and self.force_instant_connect:
680 self.force_instant_connect = False
682 now = datetime.datetime.now()
683 if now - last_ping > interval:
684 if self.disconnected:
694 self.do_refresh = False
697 msg = self.queue.get(block=False)
702 key = stdscr.getkey()
703 self.do_refresh = True
706 self.show_help = False
707 if key == 'KEY_RESIZE':
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
714 self.restore_input_values()
715 elif self.mode.has_input_prompt and key != '\n': # Return 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_))
726 elif self.mode == self.mode_control_pw_pw and key == '\n':
727 if self.input_ == '':
728 self.log_msg('@ aborted')
730 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
732 self.switch_mode('play')
733 elif self.mode == self.mode_password and key == '\n':
734 if self.input_ == '':
736 self.password = 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_))
742 self.switch_mode('play')
743 elif self.mode == self.mode_chat and key == '\n':
744 if self.input_ == '':
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)
754 self.send('NICK ' + quote(tokens[1]))
756 self.log_msg('? need login name')
758 self.log_msg('? unknown command')
760 self.send('ALL ' + quote(self.input_))
762 elif self.mode == self.mode_annotate and key == '\n':
763 if self.input_ == '':
765 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
766 quote(self.password)))
768 self.switch_mode('play')
769 elif self.mode == self.mode_portal and key == '\n':
770 if self.input_ == '':
772 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
773 quote(self.password)))
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'
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]
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')
832 #TUI('localhost:5000')
833 TUI('wss://plomlompom.com/rogue_chat/')