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_MAP(game, geometry, size, content):
91 map_geometry_class = globals()['MapGeometry' + geometry]
92 game.map_geometry = map_geometry_class(size)
93 game.map_content = content
94 if type(game.map_geometry) == MapGeometrySquare:
95 game.tui.movement_keys = {
96 game.tui.keys['square_move_up']: 'UP',
97 game.tui.keys['square_move_left']: 'LEFT',
98 game.tui.keys['square_move_down']: 'DOWN',
99 game.tui.keys['square_move_right']: 'RIGHT',
101 elif type(game.map_geometry) == MapGeometryHex:
102 game.tui.movement_keys = {
103 game.tui.keys['hex_move_upleft']: 'UPLEFT',
104 game.tui.keys['hex_move_upright']: 'UPRIGHT',
105 game.tui.keys['hex_move_right']: 'RIGHT',
106 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
107 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
108 game.tui.keys['hex_move_left']: 'LEFT',
110 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
112 def cmd_FOV(game, content):
114 cmd_FOV.argtypes = 'string'
116 def cmd_MAP_CONTROL(game, content):
117 game.map_control_content = content
118 cmd_MAP_CONTROL.argtypes = 'string'
120 def cmd_GAME_STATE_COMPLETE(game):
122 if game.tui.mode.name == 'post_login_wait':
123 game.tui.switch_mode('play')
124 if game.tui.mode.shows_info:
125 game.tui.query_info()
126 player = game.get_thing(game.player_id)
127 if player.position in game.portals:
128 game.tui.teleport_target_host = game.portals[player.position]
129 game.tui.switch_mode('teleport')
130 game.turn_complete = True
131 game.tui.do_refresh = True
132 cmd_GAME_STATE_COMPLETE.argtypes = ''
134 def cmd_PORTAL(game, position, msg):
135 game.portals[position] = msg
136 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
138 def cmd_PLAY_ERROR(game, msg):
140 game.tui.do_refresh = True
141 cmd_PLAY_ERROR.argtypes = 'string'
143 def cmd_GAME_ERROR(game, msg):
144 game.tui.log_msg('? game error: ' + msg)
145 game.tui.do_refresh = True
146 cmd_GAME_ERROR.argtypes = 'string'
148 def cmd_ARGUMENT_ERROR(game, msg):
149 game.tui.log_msg('? syntax error: ' + msg)
150 game.tui.do_refresh = True
151 cmd_ARGUMENT_ERROR.argtypes = 'string'
153 def cmd_ANNOTATION(game, position, msg):
154 game.info_db[position] = msg
155 if game.tui.mode.shows_info:
156 game.tui.do_refresh = True
157 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
159 def cmd_TASKS(game, tasks_comma_separated):
160 game.tasks = tasks_comma_separated.split(',')
161 cmd_TASKS.argtypes = 'string'
163 def cmd_THING_TYPE(game, thing_type, symbol_hint):
164 game.thing_types[thing_type] = symbol_hint
165 cmd_THING_TYPE.argtypes = 'string char'
169 cmd_PONG.argtypes = ''
171 class Game(GameBase):
172 turn_complete = False
176 def __init__(self, *args, **kwargs):
177 super().__init__(*args, **kwargs)
178 self.register_command(cmd_LOGIN_OK)
179 self.register_command(cmd_PONG)
180 self.register_command(cmd_CHAT)
181 self.register_command(cmd_PLAYER_ID)
182 self.register_command(cmd_TURN)
183 self.register_command(cmd_THING)
184 self.register_command(cmd_THING_TYPE)
185 self.register_command(cmd_THING_NAME)
186 self.register_command(cmd_MAP)
187 self.register_command(cmd_MAP_CONTROL)
188 self.register_command(cmd_PORTAL)
189 self.register_command(cmd_ANNOTATION)
190 self.register_command(cmd_GAME_STATE_COMPLETE)
191 self.register_command(cmd_ARGUMENT_ERROR)
192 self.register_command(cmd_GAME_ERROR)
193 self.register_command(cmd_PLAY_ERROR)
194 self.register_command(cmd_TASKS)
195 self.register_command(cmd_FOV)
196 self.map_content = ''
201 def get_string_options(self, string_option_type):
202 if string_option_type == 'map_geometry':
203 return ['Hex', 'Square']
204 elif string_option_type == 'thing_type':
205 return self.thing_types.keys()
208 def get_command(self, command_name):
209 from functools import partial
210 f = partial(self.commands[command_name], self)
211 f.argtypes = self.commands[command_name].argtypes
218 def __init__(self, name, help_intro, has_input_prompt=False,
219 shows_info=False, is_intro = False):
221 self.has_input_prompt = has_input_prompt
222 self.shows_info = shows_info
223 self.is_intro = is_intro
224 self.help_intro = help_intro
226 def __init__(self, host):
230 self.mode_play = self.Mode('play', 'This mode allows you to interact with the map.')
231 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)
232 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.')
233 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)
234 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)
235 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)
236 self.mode_waiting_for_server = self.Mode('waiting_for_server', 'Waiting for a server response.', is_intro=True)
237 self.mode_login = self.Mode('login', 'Pick your player name.', has_input_prompt=True, is_intro=True)
238 self.mode_post_login_wait = self.Mode('post_login_wait', 'Waiting for a server response.', is_intro=True)
239 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)
240 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)
243 self.parser = Parser(self.game)
245 self.do_refresh = True
246 self.queue = queue.Queue()
247 self.login_name = None
248 self.map_mode = 'terrain'
249 self.password = 'foo'
250 self.switch_mode('waiting_for_server')
252 'switch_to_chat': 't',
253 'switch_to_play': 'p',
254 'switch_to_password': 'P',
255 'switch_to_annotate': 'M',
256 'switch_to_portal': 'T',
257 'switch_to_study': '?',
258 'switch_to_edit': 'm',
262 'toggle_map_mode': 'M',
263 'hex_move_upleft': 'w',
264 'hex_move_upright': 'e',
265 'hex_move_right': 'd',
266 'hex_move_downright': 'x',
267 'hex_move_downleft': 'y',
268 'hex_move_left': 'a',
269 'square_move_up': 'w',
270 'square_move_left': 'a',
271 'square_move_down': 's',
272 'square_move_right': 'd',
274 if os.path.isfile('config.json'):
275 with open('config.json', 'r') as f:
276 keys_conf = json.loads(f.read())
278 self.keys[k] = keys_conf[k]
279 self.show_help = False
280 self.disconnected = True
281 self.force_instant_connect = True
282 self.input_lines = []
284 curses.wrapper(self.loop)
291 def handle_recv(msg):
297 self.log_msg('@ attempting connect')
298 socket_client_class = PlomSocketClient
299 if self.host.startswith('ws://') or self.host.startswith('wss://'):
300 socket_client_class = WebSocketClient
302 self.socket = socket_client_class(handle_recv, self.host)
303 self.socket_thread = threading.Thread(target=self.socket.run)
304 self.socket_thread.start()
305 self.disconnected = False
306 self.game.thing_types = {}
307 self.socket.send('TASKS')
308 self.socket.send('THING_TYPES')
309 self.switch_mode('login')
310 except ConnectionRefusedError:
311 self.log_msg('@ server connect failure')
312 self.disconnected = True
313 self.switch_mode('waiting_for_server')
314 self.do_refresh = True
317 self.log_msg('@ attempting reconnect')
319 time.sleep(0.1) # FIXME necessitated by some some strange SSL race
320 # conditions with ws4py, find out what exactly
321 self.switch_mode('waiting_for_server')
326 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
327 raise BrokenSocketConnection
328 self.socket.send(msg)
329 except (BrokenPipeError, BrokenSocketConnection):
330 self.log_msg('@ server disconnected :(')
331 self.disconnected = True
332 self.force_instant_connect = True
333 self.do_refresh = True
335 def log_msg(self, msg):
337 if len(self.log) > 100:
338 self.log = self.log[-100:]
340 def query_info(self):
341 self.send('GET_ANNOTATION ' + str(self.explorer))
343 def restore_input_values(self):
344 if self.mode.name == 'annotate' and self.explorer in self.game.info_db:
345 info = self.game.info_db[self.explorer]
348 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
349 self.input_ = self.game.portals[self.explorer]
350 elif self.mode.name == 'password':
351 self.input_ = self.password
353 def switch_mode(self, mode_name):
354 self.map_mode = 'terrain'
355 self.mode = getattr(self, 'mode_' + mode_name)
356 if self.mode.shows_info:
357 player = self.game.get_thing(self.game.player_id)
358 self.explorer = YX(player.position.y, player.position.x)
359 if self.mode.name == 'waiting_for_server':
360 self.log_msg('@ waiting for server …')
361 if self.mode.name == 'edit':
362 self.show_help = True
363 elif self.mode.name == 'login':
365 self.send('LOGIN ' + quote(self.login_name))
367 self.log_msg('@ enter username')
368 elif self.mode.name == 'teleport':
369 self.log_msg("@ May teleport to %s" % (self.teleport_target_host)),
370 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
371 self.restore_input_values()
373 def loop(self, stdscr):
376 def safe_addstr(y, x, line):
377 if y < self.size.y - 1 or x + len(line) < self.size.x:
378 stdscr.addstr(y, x, line)
379 else: # workaround to <https://stackoverflow.com/q/7063128>
380 cut_i = self.size.x - x - 1
382 last_char = line[cut_i]
383 stdscr.addstr(y, self.size.x - 2, last_char)
384 stdscr.insstr(y, self.size.x - 2, ' ')
385 stdscr.addstr(y, x, cut)
387 def handle_input(msg):
388 command, args = self.parser.parse(msg)
391 def msg_into_lines_of_width(msg, width):
395 for i in range(len(msg)):
396 if x >= width or msg[i] == "\n":
406 def reset_screen_size():
407 self.size = YX(*stdscr.getmaxyx())
408 self.size = self.size - YX(self.size.y % 4, 0)
409 self.size = self.size - YX(0, self.size.x % 4)
410 self.window_width = int(self.size.x / 2)
412 def recalc_input_lines():
413 if not self.mode.has_input_prompt:
414 self.input_lines = []
416 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
419 def move_explorer(direction):
420 target = self.game.map_geometry.move(self.explorer, direction)
422 self.explorer = target
429 for line in self.log:
430 lines += msg_into_lines_of_width(line, self.window_width)
433 max_y = self.size.y - len(self.input_lines)
434 for i in range(len(lines)):
435 if (i >= max_y - height_header):
437 safe_addstr(max_y - i - 1, self.window_width, lines[i])
440 if not self.game.turn_complete:
442 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
443 info = 'outside field of view'
444 if self.game.fov[pos_i] == '.':
445 info = 'TERRAIN: %s\n' % self.game.map_content[pos_i]
446 for t in self.game.things:
447 if t.position == self.explorer:
448 info += 'THING: %s' % t.type_
449 if hasattr(t, 'name'):
450 info += ' (name: %s)' % t.name
452 if self.explorer in self.game.portals:
453 info += 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
455 info += 'PORTAL: (none)\n'
456 if self.explorer in self.game.info_db:
457 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
459 info += 'ANNOTATION: waiting …'
460 lines = msg_into_lines_of_width(info, self.window_width)
462 for i in range(len(lines)):
463 y = height_header + i
464 if y >= self.size.y - len(self.input_lines):
466 safe_addstr(y, self.window_width, lines[i])
469 y = self.size.y - len(self.input_lines)
470 for i in range(len(self.input_lines)):
471 safe_addstr(y, self.window_width, self.input_lines[i])
475 if not self.game.turn_complete:
477 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
480 help = "hit [%s] for help" % self.keys['help']
481 if self.mode.has_input_prompt:
482 help = "enter /help for help"
483 safe_addstr(1, self.window_width, 'MODE: %s – %s' % (self.mode.name, help))
486 if not self.game.turn_complete:
489 map_content = self.game.map_content
490 if self.map_mode == 'control':
491 map_content = self.game.map_control_content
492 for y in range(self.game.map_geometry.size.y):
493 start = self.game.map_geometry.size.x * y
494 end = start + self.game.map_geometry.size.x
495 map_lines_split += [list(map_content[start:end])]
496 if self.map_mode == 'terrain':
497 for t in self.game.things:
498 symbol = self.game.thing_types[t.type_]
499 map_lines_split[t.position.y][t.position.x] = symbol
500 if self.mode.shows_info:
501 map_lines_split[self.explorer.y][self.explorer.x] = '?'
503 if type(self.game.map_geometry) == MapGeometryHex:
505 for line in map_lines_split:
506 map_lines += [indent*' ' + ' '.join(line)]
507 indent = 0 if indent else 1
509 for line in map_lines_split:
510 map_lines += [' '.join(line)]
511 window_center = YX(int(self.size.y / 2),
512 int(self.window_width / 2))
513 player = self.game.get_thing(self.game.player_id)
514 center = player.position
515 if self.mode.shows_info:
516 center = self.explorer
517 center = YX(center.y, center.x * 2)
518 offset = center - window_center
519 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
521 term_y = max(0, -offset.y)
522 term_x = max(0, -offset.x)
523 map_y = max(0, offset.y)
524 map_x = max(0, offset.x)
525 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
526 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
527 safe_addstr(term_y, term_x, to_draw)
532 content = "%s mode help\n\n%s\n\n" % (self.mode.name,
533 self.mode.help_intro)
534 if self.mode == self.mode_play:
535 content += "Available actions:\n"
536 if 'MOVE' in self.game.tasks:
537 content += "[%s] – move player\n" % ','.join(self.movement_keys)
538 if 'PICK_UP' in self.game.tasks:
539 content += "[%s] – take thing under player\n" % self.keys['take_thing']
540 if 'DROP' in self.game.tasks:
541 content += "[%s] – drop carried thing\n" % self.keys['drop_thing']
542 if 'FLATTEN_SURROUNDINGS' in self.game.tasks:
543 content += "[%s] – flatten player's surroundings\n" % self.keys['flatten']
544 content += 'Other modes available from here:\n'
545 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
546 content += '[%s] – study mode\n' % self.keys['switch_to_study']
547 content += '[%s] – terrain edit mode\n' % self.keys['switch_to_edit']
548 content += '[%s] – portal edit mode\n' % self.keys['switch_to_portal']
549 content += '[%s] – annotation mode\n' % self.keys['switch_to_annotate']
550 content += '[%s] – password input mode\n' % self.keys['switch_to_password']
551 elif self.mode == self.mode_study:
552 content += 'Available actions:\n'
553 content += '[%s] – move question mark\n' % ','.join(self.movement_keys)
554 content += '[%s] – toggle view between terrain, and password protection areas\n' % self.keys['toggle_map_mode']
555 content += '\n\nOther modes available from here:'
556 content += '[%s] – chat mode\n' % self.keys['switch_to_chat']
557 content += '[%s] – play mode\n' % self.keys['switch_to_play']
558 elif self.mode == self.mode_chat:
559 content += '/nick NAME – re-name yourself to NAME\n'
560 #content += '/msg USER TEXT – send TEXT to USER\n'
561 content += '/%s or /play – switch to play mode\n' % self.keys['switch_to_play']
562 content += '/%s or /study – switch to study mode\n' % self.keys['switch_to_study']
563 for i in range(self.size.y):
565 self.window_width * (not self.mode.has_input_prompt),
566 ' '*self.window_width)
568 for line in content.split('\n'):
569 lines += msg_into_lines_of_width(line, self.window_width)
570 for i in range(len(lines)):
574 self.window_width * (not self.mode.has_input_prompt),
579 if self.mode.has_input_prompt:
582 if self.mode.shows_info:
587 if not self.mode.is_intro:
593 curses.curs_set(False) # hide cursor
594 curses.use_default_colors();
597 self.explorer = YX(0, 0)
600 interval = datetime.timedelta(seconds=5)
601 last_ping = datetime.datetime.now() - interval
603 if self.disconnected and self.force_instant_connect:
604 self.force_instant_connect = False
606 now = datetime.datetime.now()
607 if now - last_ping > interval:
608 if self.disconnected:
615 self.do_refresh = False
618 msg = self.queue.get(block=False)
623 key = stdscr.getkey()
624 self.do_refresh = True
627 self.show_help = False
628 if key == 'KEY_RESIZE':
630 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
631 self.input_ = self.input_[:-1]
632 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
633 self.show_help = True
635 self.restore_input_values()
636 elif self.mode.has_input_prompt and key != '\n': # Return key
638 max_length = self.window_width * self.size.y - len(input_prompt) - 1
639 if len(self.input_) > max_length:
640 self.input_ = self.input_[:max_length]
641 elif key == self.keys['help'] and self.mode != self.mode_edit:
642 self.show_help = True
643 elif self.mode == self.mode_login and key == '\n':
644 self.login_name = self.input_
645 self.send('LOGIN ' + quote(self.input_))
647 elif self.mode == self.mode_password and key == '\n':
648 if self.input_ == '':
650 self.password = self.input_
652 self.switch_mode('play')
653 elif self.mode == self.mode_chat and key == '\n':
654 if self.input_ == '':
656 if self.input_[0] == '/': # FIXME fails on empty input
657 if self.input_ in {'/' + self.keys['switch_to_play'], '/play'}:
658 self.switch_mode('play')
659 elif self.input_ in {'/' + self.keys['switch_to_study'], '/study'}:
660 self.switch_mode('study')
661 elif self.input_.startswith('/nick'):
662 tokens = self.input_.split(maxsplit=1)
664 self.send('NICK ' + quote(tokens[1]))
666 self.log_msg('? need login name')
667 #elif self.input_.startswith('/msg'):
668 # tokens = self.input_.split(maxsplit=2)
669 # if len(tokens) == 3:
670 # self.send('QUERY %s %s' % (quote(tokens[1]),
673 # self.log_msg('? need message target and message')
675 self.log_msg('? unknown command')
677 self.send('ALL ' + quote(self.input_))
679 elif self.mode == self.mode_annotate and key == '\n':
680 if self.input_ == '':
682 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
683 quote(self.password)))
685 self.switch_mode('play')
686 elif self.mode == self.mode_portal and key == '\n':
687 if self.input_ == '':
689 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
690 quote(self.password)))
692 self.switch_mode('play')
693 elif self.mode == self.mode_teleport and key == '\n':
694 if self.input_ == 'YES!':
695 self.host = self.teleport_target_host
698 self.log_msg('@ teleport aborted')
699 self.switch_mode('play')
701 elif self.mode == self.mode_study:
702 if key == self.keys['switch_to_chat']:
703 self.switch_mode('chat')
704 elif key == self.keys['switch_to_play']:
705 self.switch_mode('play')
706 elif key == self.keys['toggle_map_mode']:
707 if self.map_mode == 'terrain':
708 self.map_mode = 'control'
710 self.map_mode = 'terrain'
711 elif key in self.movement_keys:
712 move_explorer(self.movement_keys[key])
713 elif self.mode == self.mode_play:
714 if key == self.keys['switch_to_chat']:
715 self.switch_mode('chat')
716 elif key == self.keys['switch_to_study']:
717 self.switch_mode('study')
718 elif key == self.keys['switch_to_annotate']:
719 self.switch_mode('annotate')
720 elif key == self.keys['switch_to_portal']:
721 self.switch_mode('portal')
722 elif key == self.keys['switch_to_password']:
723 self.switch_mode('password')
724 if key == self.keys['switch_to_edit'] and\
725 'WRITE' in self.game.tasks:
726 self.switch_mode('edit')
727 elif key == self.keys['flatten'] and\
728 'FLATTEN_SURROUNDINGS' in self.game.tasks:
729 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
730 elif key == self.keys['take_thing'] and 'PICK_UP' in self.game.tasks:
731 self.send('TASK:PICK_UP')
732 elif key == self.keys['drop_thing'] and 'DROP' in self.game.tasks:
733 self.send('TASK:DROP')
734 elif key in self.movement_keys and 'MOVE' in self.game.tasks:
735 self.send('TASK:MOVE ' + self.movement_keys[key])
736 elif self.mode == self.mode_edit:
737 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
738 self.switch_mode('play')
740 TUI('localhost:5000')