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 game.turn_complete = True
133 game.tui.do_refresh = True
134 cmd_GAME_STATE_COMPLETE.argtypes = ''
136 def cmd_PORTAL(game, position, msg):
137 game.portals[position] = msg
138 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
140 def cmd_PLAY_ERROR(game, msg):
142 game.tui.do_refresh = True
143 cmd_PLAY_ERROR.argtypes = 'string'
145 def cmd_GAME_ERROR(game, msg):
146 game.tui.log_msg('? game error: ' + msg)
147 game.tui.do_refresh = True
148 cmd_GAME_ERROR.argtypes = 'string'
150 def cmd_ARGUMENT_ERROR(game, msg):
151 game.tui.log_msg('? syntax error: ' + msg)
152 game.tui.do_refresh = True
153 cmd_ARGUMENT_ERROR.argtypes = 'string'
155 def cmd_ANNOTATION(game, position, msg):
156 game.info_db[position] = msg
157 if game.tui.mode.shows_info:
158 game.tui.do_refresh = True
159 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
161 def cmd_TASKS(game, tasks_comma_separated):
162 game.tasks = tasks_comma_separated.split(',')
163 cmd_TASKS.argtypes = 'string'
165 def cmd_THING_TYPE(game, thing_type, symbol_hint):
166 game.thing_types[thing_type] = symbol_hint
167 cmd_THING_TYPE.argtypes = 'string char'
169 def cmd_TERRAIN(game, terrain_char, terrain_desc):
170 game.terrains[terrain_char] = terrain_desc
171 cmd_TERRAIN.argtypes = 'char string'
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_TERRAIN)
194 self.register_command(cmd_MAP)
195 self.register_command(cmd_MAP_CONTROL)
196 self.register_command(cmd_PORTAL)
197 self.register_command(cmd_ANNOTATION)
198 self.register_command(cmd_GAME_STATE_COMPLETE)
199 self.register_command(cmd_ARGUMENT_ERROR)
200 self.register_command(cmd_GAME_ERROR)
201 self.register_command(cmd_PLAY_ERROR)
202 self.register_command(cmd_TASKS)
203 self.register_command(cmd_FOV)
204 self.map_content = ''
210 def get_string_options(self, string_option_type):
211 if string_option_type == 'map_geometry':
212 return ['Hex', 'Square']
213 elif string_option_type == 'thing_type':
214 return self.thing_types.keys()
217 def get_command(self, command_name):
218 from functools import partial
219 f = partial(self.commands[command_name], self)
220 f.argtypes = self.commands[command_name].argtypes
227 def __init__(self, name, help_intro, has_input_prompt=False,
228 shows_info=False, is_intro = False):
230 self.has_input_prompt = has_input_prompt
231 self.shows_info = shows_info
232 self.is_intro = is_intro
233 self.help_intro = help_intro
235 def __init__(self, host):
239 self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
240 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)
241 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.')
242 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)
243 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)
244 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)
245 self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
246 self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
247 self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
248 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)
251 self.parser = Parser(self.game)
253 self.do_refresh = True
254 self.queue = queue.Queue()
255 self.login_name = None
256 self.map_mode = 'terrain'
257 self.password = 'foo'
258 self.switch_mode('waiting_for_server')
260 'switch_to_chat': 't',
261 'switch_to_play': 'p',
262 'switch_to_password': 'P',
263 'switch_to_annotate': 'M',
264 'switch_to_portal': 'T',
265 'switch_to_study': '?',
266 'switch_to_edit': 'm',
271 'toggle_map_mode': 'M',
272 'hex_move_upleft': 'w',
273 'hex_move_upright': 'e',
274 'hex_move_right': 'd',
275 'hex_move_downright': 'x',
276 'hex_move_downleft': 'y',
277 'hex_move_left': 'a',
278 'square_move_up': 'w',
279 'square_move_left': 'a',
280 'square_move_down': 's',
281 'square_move_right': 'd',
283 if os.path.isfile('config.json'):
284 with open('config.json', 'r') as f:
285 keys_conf = json.loads(f.read())
287 self.keys[k] = keys_conf[k]
288 self.show_help = False
289 self.disconnected = True
290 self.force_instant_connect = True
291 self.input_lines = []
293 curses.wrapper(self.loop)
300 def handle_recv(msg):
306 self.log_msg('@ attempting connect')
307 socket_client_class = PlomSocketClient
308 if self.host.startswith('ws://') or self.host.startswith('wss://'):
309 socket_client_class = WebSocketClient
311 self.socket = socket_client_class(handle_recv, self.host)
312 self.socket_thread = threading.Thread(target=self.socket.run)
313 self.socket_thread.start()
314 self.disconnected = False
315 self.game.thing_types = {}
316 self.game.terrains = {}
317 self.socket.send('TASKS')
318 self.socket.send('TERRAINS')
319 self.socket.send('THING_TYPES')
320 self.switch_mode('login')
321 except ConnectionRefusedError:
322 self.log_msg('@ server connect failure')
323 self.disconnected = True
324 self.switch_mode('waiting_for_server')
325 self.do_refresh = True
328 self.log_msg('@ attempting reconnect')
330 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
331 # conditions with ws4py, find out what exactly
332 self.switch_mode('waiting_for_server')
337 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
338 raise BrokenSocketConnection
339 self.socket.send(msg)
340 except (BrokenPipeError, BrokenSocketConnection):
341 self.log_msg('@ server disconnected :(')
342 self.disconnected = True
343 self.force_instant_connect = True
344 self.do_refresh = True
346 def log_msg(self, msg):
348 if len(self.log) > 100:
349 self.log = self.log[-100:]
351 def query_info(self):
352 self.send('GET_ANNOTATION ' + str(self.explorer))
354 def restore_input_values(self):
355 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
356 info = self.game.info_db[self.explorer]
359 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
360 self.input_ = self.game.portals[self.explorer]
361 elif self.mode.name == 'password':
362 self.input_ = self.password
364 def switch_mode(self, mode_name):
365 self.map_mode = 'terrain'
366 self.mode = getattr(self, 'mode_' + mode_name)
367 if self.mode.shows_info:
368 player = self.game.get_thing(self.game.player_id)
369 self.explorer = YX(player.position.y, player.position.x)
370 if self.mode.name == 'waiting_for_server':
371 self.log_msg('@ waiting for server …')
372 if self.mode.name == 'edit':
373 self.show_help = True
374 elif self.mode.name == 'login':
376 self.send('LOGIN ' + quote(self.login_name))
378 self.log_msg('@ enter username')
379 self.restore_input_values()
381 def loop(self, stdscr):
384 def safe_addstr(y, x, line):
385 if y < self.size.y - 1 or x + len(line) < self.size.x:
386 stdscr.addstr(y, x, line)
387 else: # workaround to <https://stackoverflow.com/q/7063128>
388 cut_i = self.size.x - x - 1
390 last_char = line[cut_i]
391 stdscr.addstr(y, self.size.x - 2, last_char)
392 stdscr.insstr(y, self.size.x - 2, ' ')
393 stdscr.addstr(y, x, cut)
395 def handle_input(msg):
396 command, args = self.parser.parse(msg)
399 def msg_into_lines_of_width(msg, width):
403 for i in range(len(msg)):
404 if x >= width or msg[i] == "\n":
414 def reset_screen_size():
415 self.size = YX(*stdscr.getmaxyx())
416 self.size = self.size - YX(self.size.y % 4, 0)
417 self.size = self.size - YX(0, self.size.x % 4)
418 self.window_width = int(self.size.x / 2)
420 def recalc_input_lines():
421 if not self.mode.has_input_prompt:
422 self.input_lines = []
424 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
427 def move_explorer(direction):
428 target = self.game.map_geometry.move(self.explorer, direction)
430 self.explorer = target
437 for line in self.log:
438 lines += msg_into_lines_of_width(line, self.window_width)
441 max_y = self.size.y - len(self.input_lines)
442 for i in range(len(lines)):
443 if (i >= max_y - height_header):
445 safe_addstr(max_y - i - 1, self.window_width, lines[i])
448 if not self.game.turn_complete:
450 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
451 info = 'outside field of view'
452 if self.game.fov[pos_i] == '.':
453 terrain_char = self.game.map_content[pos_i]
455 if terrain_char in self.game.terrains:
456 terrain_desc = self.game.terrains[terrain_char]
457 info = 'TERRAIN: "%s" / %s\n' % (terrain_char, terrain_desc)
458 for t in self.game.things:
459 if t.position == self.explorer:
460 info += 'THING: %s / %s' % (t.type_,
461 self.game.thing_types[t.type_])
462 if hasattr(t, 'player_char'):
463 info += t.player_char
464 if hasattr(t, 'name'):
465 info += ' (%s)' % t.name
467 if self.explorer in self.game.portals:
468 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
470 info += 'PORTAL: (none)\n'
471 if self.explorer in self.game.info_db:
472 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
474 info += 'ANNOTATION: waiting …'
475 lines = msg_into_lines_of_width(info, self.window_width)
477 for i in range(len(lines)):
478 y = height_header + i
479 if y >= self.size.y - len(self.input_lines):
481 safe_addstr(y, self.window_width, lines[i])
484 y = self.size.y - len(self.input_lines)
485 for i in range(len(self.input_lines)):
486 safe_addstr(y, self.window_width, self.input_lines[i])
490 if not self.game.turn_complete:
492 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
495 help = "hit [%s] for help" % self.keys['help']
496 if self.mode.has_input_prompt:
497 help = "enter /help for help"
498 safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
501 if not self.game.turn_complete:
504 map_content = self.game.map_content
505 if self.map_mode == 'control':
506 map_content = self.game.map_control_content
507 for y in range(self.game.map_geometry.size.y):
508 start = self.game.map_geometry.size.x * y
509 end = start + self.game.map_geometry.size.x
510 map_lines_split += [[c + ' ' for c in map_content[start:end]]]
511 if self.map_mode == 'terrain':
513 for t in self.game.things:
514 symbol = self.game.thing_types[t.type_]
516 if hasattr(t, 'player_char'):
517 meta_char = t.player_char
518 if t.position in used_positions:
520 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
521 used_positions += [t.position]
522 if self.mode.shows_info:
523 map_lines_split[self.explorer.y][self.explorer.x] = '??'
525 if type(self.game.map_geometry) == MapGeometryHex:
527 for line in map_lines_split:
528 map_lines += [indent*' ' + ''.join(line)]
529 indent = 0 if indent else 1
531 for line in map_lines_split:
532 map_lines += [''.join(line)]
533 window_center = YX(int(self.size.y / 2),
534 int(self.window_width / 2))
535 player = self.game.get_thing(self.game.player_id)
536 center = player.position
537 if self.mode.shows_info:
538 center = self.explorer
539 center = YX(center.y, center.x * 2)
540 offset = center - window_center
541 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
543 term_y = max(0, -offset.y)
544 term_x = max(0, -offset.x)
545 map_y = max(0, offset.y)
546 map_x = max(0, offset.x)
547 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
548 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
549 safe_addstr(term_y, term_x, to_draw)
554 content = "%s mode help\n\n%s\n\n" % (self.mode.name,
555 self.mode.help_intro)
556 if self.mode == self.mode_play:
557 content += "Available actions:\n"
558 if 'MOVE' in self.game.tasks:
559 content += "[%s] – move player\n" % ','.join(self.movement_keys)
560 if 'PICK_UP' in self.game.tasks:
561 content += "[%s] – take thing under player\n" % self.keys['take_thing']
562 if 'DROP' in self.game.tasks:
563 content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
564 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
565 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
566 content += '[%s] – teleport to other space\n' % self.keys['teleport']
567 content += 'Other modes available from here:\n'
568 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
569 content += '[%s] – study mode\n' % self.keys['switch_to_study']
570 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
571 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
572 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
573 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
574 elif self.mode == self.mode_study:
575 content += 'Available actions:\n'
576 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
577 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
578 content += '\n\nOther modes available from here:'
579 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
580 content += '[%s] – play mode\n' % self.keys['switch_to_play']
581 elif self.mode == self.mode_chat:
582 content += '/nick NAME – re-name yourself to NAME\n'
583 #content += '/msg USER TEXT – send TEXT to USER\n'
584 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
585 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
586 for i in range(self.size.y):
588 self.window_width * (not self.mode.has_input_prompt),
589 ' '*self.window_width)
591 for line in content.split('\n'):
592 lines += msg_into_lines_of_width(line, self.window_width)
593 for i in range(len(lines)):
597 self.window_width * (not self.mode.has_input_prompt),
602 if self.mode.has_input_prompt:
605 if self.mode.shows_info:
610 if not self.mode.is_intro:
616 curses.curs_set(False) # hide cursor
617 curses.use_default_colors();
620 self.explorer = YX(0, 0)
623 interval = datetime.timedelta(seconds=5)
624 last_ping = datetime.datetime.now() - interval
626 if self.disconnected and self.force_instant_connect:
627 self.force_instant_connect = False
629 now = datetime.datetime.now()
630 if now - last_ping > interval:
631 if self.disconnected:
638 self.do_refresh = False
641 msg = self.queue.get(block=False)
646 key = stdscr.getkey()
647 self.do_refresh = True
650 self.show_help = False
651 if key == 'KEY_RESIZE':
653 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
654 self.input_ = self.input_[:-1]
655 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
656 self.show_help = True
658 self.restore_input_values()
659 elif self.mode.has_input_prompt and key != '\n': # Return key
661 max_length = self.window_width * self.size.y - len(input_prompt) - 1
662 if len(self.input_) > max_length:
663 self.input_ = self.input_[:max_length]
664 elif key == self.keys['help'] and self.mode != self.mode_edit:
665 self.show_help = True
666 elif self.mode == self.mode_login and key == '\n':
667 self.login_name = self.input_
668 self.send('LOGIN ' + quote(self.input_))
670 elif self.mode == self.mode_password and key == '\n':
671 if self.input_ == '':
673 self.password = self.input_
675 self.switch_mode('play')
676 elif self.mode == self.mode_chat and key == '\n':
677 if self.input_ == '':
679 if self.input_[0] == '/': # FIXME fails on empty input
680 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
681 self.switch_mode('play')
682 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
683 self.switch_mode('study')
684 elif self.input_.startswith('/nick'):
685 tokens = self.input_.split(maxsplit=1)
687 self.send('NICK ' + quote(tokens[1]))
689 self.log_msg('? need login name')
690 #elif self.input_.startswith('/msg'):
691 # tokens = self.input_.split(maxsplit=2)
692 # if len(tokens) == 3:
693 # self.send('QUERY %s %s' % (quote(tokens[1]),
696 # self.log_msg('? need message target and message')
698 self.log_msg('? unknown command')
700 self.send('ALL ' + quote(self.input_))
702 elif self.mode == self.mode_annotate and key == '\n':
703 if self.input_ == '':
705 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
706 quote(self.password)))
708 self.switch_mode('play')
709 elif self.mode == self.mode_portal and key == '\n':
710 if self.input_ == '':
712 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
713 quote(self.password)))
715 self.switch_mode('play')
716 elif self.mode == self.mode_study:
717 if key == self.keys['switch_to_chat']:
718 self.switch_mode('chat')
719 elif key == self.keys['switch_to_play']:
720 self.switch_mode('play')
721 elif key == self.keys['toggle_map_mode']:
722 if self.map_mode == 'terrain':
723 self.map_mode = 'control'
725 self.map_mode = 'terrain'
726 elif key in self.movement_keys:
727 move_explorer(self.movement_keys[key])
728 elif self.mode == self.mode_play:
729 if key == self.keys['switch_to_chat']:
730 self.switch_mode('chat')
731 elif key == self.keys['switch_to_study']:
732 self.switch_mode('study')
733 elif key == self.keys['switch_to_annotate']:
734 self.switch_mode('annotate')
735 elif key == self.keys['switch_to_portal']:
736 self.switch_mode('portal')
737 elif key == self.keys['switch_to_password']:
738 self.switch_mode('password')
739 if key == self.keys['switch_to_edit'] and\
740 'WRITE' in self.game.tasks:
741 self.switch_mode('edit')
742 elif key == self.keys['flatten'] and\
743 'FLATTEN_SURROUNDINGS' in self.game.tasks:
744 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
745 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
746 self.send('TASK:PICK_UP')
747 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
748 self.send('TASK:DROP')
749 elif key == self.keys['teleport']:
750 player = self.game.get_thing(self.game.player_id)
751 if player.position in self.game.portals:
752 self.host = self.game.portals[player.position]
756 self.log_msg('? not standing on portal')
757 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
758 self.send('TASK:MOVE ' + self.movement_keys[key])
759 elif self.mode == self.mode_edit:
760 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
761 self.switch_mode('play')
763 TUI('localhost:5000')