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):
57 game.turn_complete = False
58 cmd_TURN.argtypes = 'int:nonneg'
60 def cmd_LOGIN_OK(game):
61 game.tui.switch_mode('post_login_wait')
62 game.tui.send('GET_GAMESTATE')
63 game.tui.log_msg('@ welcome')
64 cmd_LOGIN_OK.argtypes = ''
66 def cmd_CHAT(game, msg):
67 game.tui.log_msg('# ' + msg)
68 game.tui.do_refresh = True
69 cmd_CHAT.argtypes = 'string'
71 def cmd_PLAYER_ID(game, player_id):
72 game.player_id = player_id
73 cmd_PLAYER_ID.argtypes = 'int:nonneg'
75 def cmd_THING(game, yx, thing_type, thing_id):
76 t = game.get_thing(thing_id)
78 t = ThingBase(game, thing_id)
82 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type int:nonneg'
84 def cmd_THING_NAME(game, thing_id, name):
85 t = game.get_thing(thing_id)
88 cmd_THING_NAME.argtypes = 'int:nonneg string'
90 def cmd_THING_CHAR(game, thing_id, c):
91 t = game.get_thing(thing_id)
94 cmd_THING_CHAR.argtypes = 'int:nonneg char'
96 def cmd_MAP(game, geometry, size, content):
97 map_geometry_class = globals()['MapGeometry' + geometry]
98 game.map_geometry = map_geometry_class(size)
99 game.map_content = content
100 if type(game.map_geometry) == MapGeometrySquare:
101 game.tui.movement_keys = {
102 game.tui.keys['square_move_up']: 'UP',
103 game.tui.keys['square_move_left']: 'LEFT',
104 game.tui.keys['square_move_down']: 'DOWN',
105 game.tui.keys['square_move_right']: 'RIGHT',
107 elif type(game.map_geometry) == MapGeometryHex:
108 game.tui.movement_keys = {
109 game.tui.keys['hex_move_upleft']: 'UPLEFT',
110 game.tui.keys['hex_move_upright']: 'UPRIGHT',
111 game.tui.keys['hex_move_right']: 'RIGHT',
112 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
113 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
114 game.tui.keys['hex_move_left']: 'LEFT',
116 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
118 def cmd_FOV(game, content):
120 cmd_FOV.argtypes = 'string'
122 def cmd_MAP_CONTROL(game, content):
123 game.map_control_content = content
124 cmd_MAP_CONTROL.argtypes = 'string'
126 def cmd_GAME_STATE_COMPLETE(game):
128 if game.tui.mode.name == 'post_login_wait':
129 game.tui.switch_mode('play')
130 if game.tui.mode.shows_info:
131 game.tui.query_info()
132 player = game.get_thing(game.player_id)
133 if player.position in game.portals:
134 game.tui.teleport_target_host = game.portals[player.position]
135 game.tui.switch_mode('teleport')
136 game.turn_complete = True
137 game.tui.do_refresh = True
138 cmd_GAME_STATE_COMPLETE.argtypes = ''
140 def cmd_PORTAL(game, position, msg):
141 game.portals[position] = msg
142 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
144 def cmd_PLAY_ERROR(game, msg):
146 game.tui.do_refresh = True
147 cmd_PLAY_ERROR.argtypes = 'string'
149 def cmd_GAME_ERROR(game, msg):
150 game.tui.log_msg('? game error: ' + msg)
151 game.tui.do_refresh = True
152 cmd_GAME_ERROR.argtypes = 'string'
154 def cmd_ARGUMENT_ERROR(game, msg):
155 game.tui.log_msg('? syntax error: ' + msg)
156 game.tui.do_refresh = True
157 cmd_ARGUMENT_ERROR.argtypes = 'string'
159 def cmd_ANNOTATION(game, position, msg):
160 game.info_db[position] = msg
161 if game.tui.mode.shows_info:
162 game.tui.do_refresh = True
163 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
165 def cmd_TASKS(game, tasks_comma_separated):
166 game.tasks = tasks_comma_separated.split(',')
167 cmd_TASKS.argtypes = 'string'
169 def cmd_THING_TYPE(game, thing_type, symbol_hint):
170 game.thing_types[thing_type] = symbol_hint
171 cmd_THING_TYPE.argtypes = 'string char'
175 cmd_PONG.argtypes = ''
177 class Game(GameBase):
178 turn_complete = False
182 def __init__(self, *args, **kwargs):
183 super().__init__(*args, **kwargs)
184 self.register_command(cmd_LOGIN_OK)
185 self.register_command(cmd_PONG)
186 self.register_command(cmd_CHAT)
187 self.register_command(cmd_PLAYER_ID)
188 self.register_command(cmd_TURN)
189 self.register_command(cmd_THING)
190 self.register_command(cmd_THING_TYPE)
191 self.register_command(cmd_THING_NAME)
192 self.register_command(cmd_THING_CHAR)
193 self.register_command(cmd_MAP)
194 self.register_command(cmd_MAP_CONTROL)
195 self.register_command(cmd_PORTAL)
196 self.register_command(cmd_ANNOTATION)
197 self.register_command(cmd_GAME_STATE_COMPLETE)
198 self.register_command(cmd_ARGUMENT_ERROR)
199 self.register_command(cmd_GAME_ERROR)
200 self.register_command(cmd_PLAY_ERROR)
201 self.register_command(cmd_TASKS)
202 self.register_command(cmd_FOV)
203 self.map_content = ''
208 def get_string_options(self, string_option_type):
209 if string_option_type == 'map_geometry':
210 return ['Hex', 'Square']
211 elif string_option_type == 'thing_type':
212 return self.thing_types.keys()
215 def get_command(self, command_name):
216 from functools import partial
217 f = partial(self.commands[command_name], self)
218 f.argtypes = self.commands[command_name].argtypes
225 def __init__(self, name, help_intro, has_input_prompt=False,
226 shows_info=False, is_intro = False):
228 self.has_input_prompt = has_input_prompt
229 self.shows_info = shows_info
230 self.is_intro = is_intro
231 self.help_intro = help_intro
233 def __init__(self, host):
237 self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
238 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)
239 self.mode_edit = self.Mode('edit', 'This mode allows you to change the map tile you currently stand on (if your map editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.')
240 self.mode_annotate = self.Mode('annotate', 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your map editing password authorizes you so). Hit Return to leave.', has_input_prompt=True, shows_info=True)
241 self.mode_portal = self.Mode('portal', 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your map editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.', has_input_prompt=True, shows_info=True)
242 self.mode_chat = self.Mode('chat', 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:', has_input_prompt=True)
243 self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
244 self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
245 self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
246 self.mode_teleport = self.Mode('teleport', 'Follow the instructions to re-connect and log-in to another server, or enter anything else to abort.', has_input_prompt=True)
247 self.mode_password = self.Mode('password', 'This mode allows you to change the password that you send to authorize yourself for editing password-protected map tiles. Hit return to confirm and leave.', has_input_prompt=True)
250 self.parser = Parser(self.game)
252 self.do_refresh = True
253 self.queue = queue.Queue()
254 self.login_name = None
255 self.map_mode = 'terrain'
256 self.password = 'foo'
257 self.switch_mode('waiting_for_server')
259 'switch_to_chat': 't',
260 'switch_to_play': 'p',
261 'switch_to_password': 'P',
262 'switch_to_annotate': 'M',
263 'switch_to_portal': 'T',
264 'switch_to_study': '?',
265 'switch_to_edit': 'm',
269 'toggle_map_mode': 'M',
270 'hex_move_upleft': 'w',
271 'hex_move_upright': 'e',
272 'hex_move_right': 'd',
273 'hex_move_downright': 'x',
274 'hex_move_downleft': 'y',
275 'hex_move_left': 'a',
276 'square_move_up': 'w',
277 'square_move_left': 'a',
278 'square_move_down': 's',
279 'square_move_right': 'd',
281 if os.path.isfile('config.json'):
282 with open('config.json', 'r') as f:
283 keys_conf = json.loads(f.read())
285 self.keys[k] = keys_conf[k]
286 self.show_help = False
287 self.disconnected = True
288 self.force_instant_connect = True
289 self.input_lines = []
291 curses.wrapper(self.loop)
298 def handle_recv(msg):
304 self.log_msg('@ attempting connect')
305 socket_client_class = PlomSocketClient
306 if self.host.startswith('ws://') or self.host.startswith('wss://'):
307 socket_client_class = WebSocketClient
309 self.socket = socket_client_class(handle_recv, self.host)
310 self.socket_thread = threading.Thread(target=self.socket.run)
311 self.socket_thread.start()
312 self.disconnected = False
313 self.game.thing_types = {}
314 self.socket.send('TASKS')
315 self.socket.send('THING_TYPES')
316 self.switch_mode('login')
317 except ConnectionRefusedError:
318 self.log_msg('@ server connect failure')
319 self.disconnected = True
320 self.switch_mode('waiting_for_server')
321 self.do_refresh = True
324 self.log_msg('@ attempting reconnect')
326 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
327 # conditions with ws4py, find out what exactly
328 self.switch_mode('waiting_for_server')
333 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
334 raise BrokenSocketConnection
335 self.socket.send(msg)
336 except (BrokenPipeError, BrokenSocketConnection):
337 self.log_msg('@ server disconnected :(')
338 self.disconnected = True
339 self.force_instant_connect = True
340 self.do_refresh = True
342 def log_msg(self, msg):
344 if len(self.log) > 100:
345 self.log = self.log[-100:]
347 def query_info(self):
348 self.send('GET_ANNOTATION ' + str(self.explorer))
350 def restore_input_values(self):
351 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
352 info = self.game.info_db[self.explorer]
355 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
356 self.input_ = self.game.portals[self.explorer]
357 elif self.mode.name == 'password':
358 self.input_ = self.password
360 def switch_mode(self, mode_name):
361 self.map_mode = 'terrain'
362 self.mode = getattr(self, 'mode_' + mode_name)
363 if self.mode.shows_info:
364 player = self.game.get_thing(self.game.player_id)
365 self.explorer = YX(player.position.y, player.position.x)
366 if self.mode.name == 'waiting_for_server':
367 self.log_msg('@ waiting for server …')
368 if self.mode.name == 'edit':
369 self.show_help = True
370 elif self.mode.name == 'login':
372 self.send('LOGIN ' + quote(self.login_name))
374 self.log_msg('@ enter username')
375 elif self.mode.name == 'teleport':
376 self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
377 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
378 self.restore_input_values()
380 def loop(self, stdscr):
383 def safe_addstr(y, x, line):
384 if y < self.size.y - 1 or x + len(line) < self.size.x:
385 stdscr.addstr(y, x, line)
386 else: # workaround to <https://stackoverflow.com/q/7063128>
387 cut_i = self.size.x - x - 1
389 last_char = line[cut_i]
390 stdscr.addstr(y, self.size.x - 2, last_char)
391 stdscr.insstr(y, self.size.x - 2, ' ')
392 stdscr.addstr(y, x, cut)
394 def handle_input(msg):
395 command, args = self.parser.parse(msg)
398 def msg_into_lines_of_width(msg, width):
402 for i in range(len(msg)):
403 if x >= width or msg[i] == "\n":
413 def reset_screen_size():
414 self.size = YX(*stdscr.getmaxyx())
415 self.size = self.size - YX(self.size.y % 4, 0)
416 self.size = self.size - YX(0, self.size.x % 4)
417 self.window_width = int(self.size.x / 2)
419 def recalc_input_lines():
420 if not self.mode.has_input_prompt:
421 self.input_lines = []
423 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
426 def move_explorer(direction):
427 target = self.game.map_geometry.move(self.explorer, direction)
429 self.explorer = target
436 for line in self.log:
437 lines += msg_into_lines_of_width(line, self.window_width)
440 max_y = self.size.y - len(self.input_lines)
441 for i in range(len(lines)):
442 if (i >= max_y - height_header):
444 safe_addstr(max_y - i - 1, self.window_width, lines[i])
447 if not self.game.turn_complete:
449 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
450 info = 'outside field of view'
451 if self.game.fov[pos_i] == '.':
452 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
453 for t in self.game.things:
454 if t.position == self.explorer:
455 info += 'THING: %s / %s' % (t.type_,
456 self.game.thing_types[t.type_])
457 if hasattr(t, 'player_char'):
458 info += t.player_char
459 if hasattr(t, 'name'):
460 info += ' (name: %s)' % t.name
462 if self.explorer in self.game.portals:
463 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
465 info += 'PORTAL: (none)\n'
466 if self.explorer in self.game.info_db:
467 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
469 info += 'ANNOTATION: waiting …'
470 lines = msg_into_lines_of_width(info, self.window_width)
472 for i in range(len(lines)):
473 y = height_header + i
474 if y >= self.size.y - len(self.input_lines):
476 safe_addstr(y, self.window_width, lines[i])
479 y = self.size.y - len(self.input_lines)
480 for i in range(len(self.input_lines)):
481 safe_addstr(y, self.window_width, self.input_lines[i])
485 if not self.game.turn_complete:
487 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
490 help = "hit [%s] for help" % self.keys['help']
491 if self.mode.has_input_prompt:
492 help = "enter /help for help"
493 safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
496 if not self.game.turn_complete:
499 map_content = self.game.map_content
500 if self.map_mode == 'control':
501 map_content = self.game.map_control_content
502 for y in range(self.game.map_geometry.size.y):
503 start = self.game.map_geometry.size.x * y
504 end = start + self.game.map_geometry.size.x
505 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
506 if self.map_mode == 'terrain':
508 for t in self.game.things:
509 symbol = self.game.thing_types[t.type_]
511 if hasattr(t, 'player_char'):
512 meta_char = t.player_char
513 if t.position in used_positions:
515 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
516 used_positions += [t.position]
517 if self.mode.shows_info:
518 map_lines_split[self.explorer.y][self.explorer.x] = '??'
520 if type(self.game.map_geometry) == MapGeometryHex:
522 for line in map_lines_split:
523 map_lines += [indent*' ' + ''.join(line)]
524 indent = 0 if indent else 1
526 for line in map_lines_split:
527 map_lines += [''.join(line)]
528 window_center = YX(int(self.size.y / 2),
529 int(self.window_width / 2))
530 player = self.game.get_thing(self.game.player_id)
531 center = player.position
532 if self.mode.shows_info:
533 center = self.explorer
534 center = YX(center.y, center.x * 2)
535 offset = center - window_center
536 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
538 term_y = max(0, -offset.y)
539 term_x = max(0, -offset.x)
540 map_y = max(0, offset.y)
541 map_x = max(0, offset.x)
542 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
543 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
544 safe_addstr(term_y, term_x, to_draw)
549 content = "%s mode help\n\n%s\n\n" % (self.mode.name,
550 self.mode.help_intro)
551 if self.mode == self.mode_play:
552 content += "Available actions:\n"
553 if 'MOVE' in self.game.tasks:
554 content += "[%s] – move player\n" % ','.join(self.movement_keys)
555 if 'PICK_UP' in self.game.tasks:
556 content += "[%s] – take thing under player\n" % self.keys['take_thing']
557 if 'DROP' in self.game.tasks:
558 content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
559 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
560 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
561 content += 'Other modes available from here:\n'
562 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
563 content += '[%s] – study mode\n' % self.keys['switch_to_study']
564 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
565 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
566 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
567 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
568 elif self.mode == self.mode_study:
569 content += 'Available actions:\n'
570 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
571 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
572 content += '\n\nOther modes available from here:'
573 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
574 content += '[%s] – play mode\n' % self.keys['switch_to_play']
575 elif self.mode == self.mode_chat:
576 content += '/nick NAME – re-name yourself to NAME\n'
577 #content += '/msg USER TEXT – send TEXT to USER\n'
578 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
579 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
580 for i in range(self.size.y):
582 self.window_width * (not self.mode.has_input_prompt),
583 ' '*self.window_width)
585 for line in content.split('\n'):
586 lines += msg_into_lines_of_width(line, self.window_width)
587 for i in range(len(lines)):
591 self.window_width * (not self.mode.has_input_prompt),
596 if self.mode.has_input_prompt:
599 if self.mode.shows_info:
604 if not self.mode.is_intro:
610 curses.curs_set(False) # hide cursor
611 curses.use_default_colors();
614 self.explorer = YX(0, 0)
617 interval = datetime.timedelta(seconds=5)
618 last_ping = datetime.datetime.now() - interval
620 if self.disconnected and self.force_instant_connect:
621 self.force_instant_connect = False
623 now = datetime.datetime.now()
624 if now - last_ping > interval:
625 if self.disconnected:
632 self.do_refresh = False
635 msg = self.queue.get(block=False)
640 key = stdscr.getkey()
641 self.do_refresh = True
644 self.show_help = False
645 if key == 'KEY_RESIZE':
647 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
648 self.input_ = self.input_[:-1]
649 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
650 self.show_help = True
652 self.restore_input_values()
653 elif self.mode.has_input_prompt and key != '\n': # Return key
655 max_length = self.window_width * self.size.y - len(input_prompt) - 1
656 if len(self.input_) > max_length:
657 self.input_ = self.input_[:max_length]
658 elif key == self.keys['help'] and self.mode != self.mode_edit:
659 self.show_help = True
660 elif self.mode == self.mode_login and key == '\n':
661 self.login_name = self.input_
662 self.send('LOGIN ' + quote(self.input_))
664 elif self.mode == self.mode_password and key == '\n':
665 if self.input_ == '':
667 self.password = self.input_
669 self.switch_mode('play')
670 elif self.mode == self.mode_chat and key == '\n':
671 if self.input_ == '':
673 if self.input_[0] == '/': # FIXME fails on empty input
674 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
675 self.switch_mode('play')
676 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
677 self.switch_mode('study')
678 elif self.input_.startswith('/nick'):
679 tokens = self.input_.split(maxsplit=1)
681 self.send('NICK ' + quote(tokens[1]))
683 self.log_msg('? need login name')
684 #elif self.input_.startswith('/msg'):
685 # tokens = self.input_.split(maxsplit=2)
686 # if len(tokens) == 3:
687 # self.send('QUERY %s %s' % (quote(tokens[1]),
690 # self.log_msg('? need message target and message')
692 self.log_msg('? unknown command')
694 self.send('ALL ' + quote(self.input_))
696 elif self.mode == self.mode_annotate and key == '\n':
697 if self.input_ == '':
699 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
700 quote(self.password)))
702 self.switch_mode('play')
703 elif self.mode == self.mode_portal and key == '\n':
704 if self.input_ == '':
706 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
707 quote(self.password)))
709 self.switch_mode('play')
710 elif self.mode == self.mode_teleport and key == '\n':
711 if self.input_ == 'YES!':
712 self.host = self.teleport_target_host
715 self.log_msg('@ teleport aborted')
716 self.switch_mode('play')
718 elif self.mode == self.mode_study:
719 if key == self.keys['switch_to_chat']:
720 self.switch_mode('chat')
721 elif key == self.keys['switch_to_play']:
722 self.switch_mode('play')
723 elif key == self.keys['toggle_map_mode']:
724 if self.map_mode == 'terrain':
725 self.map_mode = 'control'
727 self.map_mode = 'terrain'
728 elif key in self.movement_keys:
729 move_explorer(self.movement_keys[key])
730 elif self.mode == self.mode_play:
731 if key == self.keys['switch_to_chat']:
732 self.switch_mode('chat')
733 elif key == self.keys['switch_to_study']:
734 self.switch_mode('study')
735 elif key == self.keys['switch_to_annotate']:
736 self.switch_mode('annotate')
737 elif key == self.keys['switch_to_portal']:
738 self.switch_mode('portal')
739 elif key == self.keys['switch_to_password']:
740 self.switch_mode('password')
741 if key == self.keys['switch_to_edit'] and\
742 'WRITE' in self.game.tasks:
743 self.switch_mode('edit')
744 elif key == self.keys['flatten'] and\
745 'FLATTEN_SURROUNDINGS' in self.game.tasks:
746 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
747 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
748 self.send('TASK:PICK_UP')
749 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
750 self.send('TASK:DROP')
751 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
752 self.send('TASK:MOVE ' + self.movement_keys[key])
753 elif self.mode == self.mode_edit:
754 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
755 self.switch_mode('play')
757 TUI('localhost:5000')