7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
18 'long': 'This mode allows you to interact with the map in various ways.'
23 'long': '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. Toggle the map view to show or hide different information layers.'},
25 'short': 'world edit',
27 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
30 'short': 'name thing',
32 'long': 'Give name to/change name of carried thing.'
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
41 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
46 'intro': 'Enter number of direction to which you want to drop thing.',
47 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
49 'admin_thing_protect': {
50 'short': 'change thing protection',
51 'intro': '@ enter thing protection character:',
52 'long': 'Change protection character for carried thing.'
56 'intro': '@ enter face line:',
57 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
60 'short': 'edit design',
61 'intro': '@ enter design:',
62 'long': 'Enter design for carried thing as ASCII art.'
66 'intro': '@ enter hat line:',
67 'long': 'Draw your hat as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom. Eat cookies to extend the ASCII characters available for drawing.'
72 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
75 'short': 'change protection character password',
76 'intro': '@ enter protection character for which you want to change the password:',
77 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
80 'short': 'change protection character password',
82 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
84 'control_tile_type': {
85 'short': 'change tiles protection',
86 'intro': '@ enter protection character which you want to draw:',
87 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
89 'control_tile_draw': {
90 'short': 'change tiles protection',
92 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
95 'short': 'annotate tile',
97 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
100 'short': 'edit portal',
102 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world 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.'
107 'long': '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:\n\n/nick NAME – re-name yourself to NAME'
112 'long': 'Enter your player name.'
114 'waiting_for_server': {
115 'short': 'waiting for server response',
116 'intro': '@ waiting for server …',
117 'long': 'Waiting for a server response.'
120 'short': 'waiting for server response',
122 'long': 'Waiting for a server response.'
125 'short': 'set world edit password',
127 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
130 'short': 'become admin',
131 'intro': '@ enter admin password:',
132 'long': 'This mode allows you to become admin if you know an admin password.'
137 'long': 'This mode allows you access to actions limited to administrators.'
141 from ws4py.client import WebSocketBaseClient
142 class WebSocketClient(WebSocketBaseClient):
144 def __init__(self, recv_handler, *args, **kwargs):
145 super().__init__(*args, **kwargs)
146 self.recv_handler = recv_handler
149 def received_message(self, message):
151 message = str(message)
152 self.recv_handler(message)
155 def plom_closed(self):
156 return self.client_terminated
158 from plomrogue.io_tcp import PlomSocket
159 class PlomSocketClient(PlomSocket):
161 def __init__(self, recv_handler, url):
163 self.recv_handler = recv_handler
164 host, port = url.split(':')
165 super().__init__(socket.create_connection((host, port)))
173 for msg in self.recv():
174 if msg == 'NEED_SSL':
175 self.socket = ssl.wrap_socket(self.socket)
177 self.recv_handler(msg)
178 except BrokenSocketConnection:
179 pass # we assume socket will be known as dead by now
181 def cmd_TURN(game, n):
182 game.turn_complete = False
183 cmd_TURN.argtypes = 'int:nonneg'
185 def cmd_OTHER_WIPE(game):
186 game.portals_new = {}
187 game.annotations_new = {}
189 cmd_OTHER_WIPE.argtypes = ''
191 def cmd_LOGIN_OK(game):
192 game.tui.switch_mode('post_login_wait')
193 game.tui.send('GET_GAMESTATE')
194 game.tui.log_msg('@ welcome!')
195 game.tui.log_msg('@ hint: see top of terminal for how to get help.')
196 game.tui.log_msg('@ hint: enter study mode to understand your environment.')
197 cmd_LOGIN_OK.argtypes = ''
199 def cmd_ADMIN_OK(game):
200 game.tui.is_admin = True
201 game.tui.log_msg('@ you now have admin rights')
202 game.tui.switch_mode('admin')
203 game.tui.do_refresh = True
204 cmd_ADMIN_OK.argtypes = ''
206 def cmd_REPLY(game, msg):
207 game.tui.log_msg('#MUSICPLAYER: ' + msg)
208 game.tui.do_refresh = True
209 cmd_REPLY.argtypes = 'string'
211 def cmd_CHAT(game, msg):
212 game.tui.log_msg('# ' + msg)
213 game.tui.do_refresh = True
214 cmd_CHAT.argtypes = 'string'
216 def cmd_CHATFACE(game, thing_id):
217 game.tui.draw_face = thing_id
218 game.tui.do_refresh = True
219 cmd_CHATFACE.argtypes = 'int:pos'
221 def cmd_PLAYER_ID(game, player_id):
222 game.player_id = player_id
223 cmd_PLAYER_ID.argtypes = 'int:nonneg'
225 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
226 game.players_hat_chars_new = hat_chars
227 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
229 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
230 t = game.get_thing_temp(thing_id)
232 t = ThingBase(game, thing_id)
233 game.things_new += [t]
236 t.protection = protection
237 t.portable = portable
238 t.commandable = commandable
239 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
241 def cmd_THING_NAME(game, thing_id, name):
242 t = game.get_thing_temp(thing_id)
244 cmd_THING_NAME.argtypes = 'int:pos string'
246 def cmd_THING_FACE(game, thing_id, face):
247 t = game.get_thing_temp(thing_id)
249 cmd_THING_FACE.argtypes = 'int:pos string'
251 def cmd_THING_HAT(game, thing_id, hat):
252 t = game.get_thing_temp(thing_id)
254 cmd_THING_HAT.argtypes = 'int:pos string'
256 def cmd_THING_DESIGN(game, thing_id, size, design):
257 t = game.get_thing_temp(thing_id)
258 t.design = [size, design]
259 cmd_THING_DESIGN.argtypes = 'int:pos yx_tuple string'
261 def cmd_THING_CHAR(game, thing_id, c):
262 t = game.get_thing_temp(thing_id)
264 cmd_THING_CHAR.argtypes = 'int:pos char'
266 def cmd_MAP(game, geometry, size, content):
267 map_geometry_class = globals()['MapGeometry' + geometry]
268 game.map_geometry_new = map_geometry_class(size)
269 game.map_content_new = content
270 if type(game.map_geometry_new) == MapGeometrySquare:
271 game.tui.movement_keys = {
272 game.tui.keys['square_move_up']: 'UP',
273 game.tui.keys['square_move_left']: 'LEFT',
274 game.tui.keys['square_move_down']: 'DOWN',
275 game.tui.keys['square_move_right']: 'RIGHT',
277 elif type(game.map_geometry_new) == MapGeometryHex:
278 game.tui.movement_keys = {
279 game.tui.keys['hex_move_upleft']: 'UPLEFT',
280 game.tui.keys['hex_move_upright']: 'UPRIGHT',
281 game.tui.keys['hex_move_right']: 'RIGHT',
282 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
283 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
284 game.tui.keys['hex_move_left']: 'LEFT',
286 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
288 def cmd_FOV(game, content):
289 game.fov_new = content
290 cmd_FOV.argtypes = 'string'
292 def cmd_MAP_CONTROL(game, content):
293 game.map_control_content_new = content
294 cmd_MAP_CONTROL.argtypes = 'string'
296 def cmd_GAME_STATE_COMPLETE(game):
297 game.tui.do_refresh = True
298 game.tui.info_cached = None
299 game.things = game.things_new
300 game.portals = game.portals_new
301 game.annotations = game.annotations_new
302 game.fov = game.fov_new
303 game.map_geometry = game.map_geometry_new
304 game.map_content = game.map_content_new
305 game.map_control_content = game.map_control_content_new
306 game.player = game.get_thing(game.player_id)
307 game.players_hat_chars = game.players_hat_chars_new
308 game.bladder_pressure = game.bladder_pressure_new
309 game.energy = game.energy_new
310 game.turn_complete = True
311 if game.tui.mode.name == 'post_login_wait':
312 game.tui.switch_mode('play')
313 cmd_GAME_STATE_COMPLETE.argtypes = ''
315 def cmd_PORTAL(game, position, msg):
316 game.portals_new[position] = msg
317 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
319 def cmd_PLAY_ERROR(game, msg):
320 game.tui.log_msg('? ' + msg)
321 game.tui.flash = True
322 game.tui.do_refresh = True
323 cmd_PLAY_ERROR.argtypes = 'string'
325 def cmd_GAME_ERROR(game, msg):
326 game.tui.log_msg('? game error: ' + msg)
327 game.tui.do_refresh = True
328 cmd_GAME_ERROR.argtypes = 'string'
330 def cmd_ARGUMENT_ERROR(game, msg):
331 game.tui.log_msg('? syntax error: ' + msg)
332 game.tui.do_refresh = True
333 cmd_ARGUMENT_ERROR.argtypes = 'string'
335 def cmd_ANNOTATION(game, position, msg):
336 game.annotations_new[position] = msg
337 if game.tui.mode.shows_info:
338 game.tui.do_refresh = True
339 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
341 def cmd_TASKS(game, tasks_comma_separated):
342 game.tasks = tasks_comma_separated.split(',')
343 game.tui.mode_write.legal = 'WRITE' in game.tasks
344 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
345 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
346 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
347 cmd_TASKS.argtypes = 'string'
349 def cmd_THING_TYPE(game, thing_type, symbol_hint):
350 game.thing_types[thing_type] = symbol_hint
351 cmd_THING_TYPE.argtypes = 'string char'
353 def cmd_THING_INSTALLED(game, thing_id):
354 game.get_thing_temp(thing_id).installed = True
355 cmd_THING_INSTALLED.argtypes = 'int:pos'
357 def cmd_THING_CARRYING(game, thing_id, carried_id):
358 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
359 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
361 def cmd_TERRAIN(game, terrain_char, terrain_desc):
362 game.terrains[terrain_char] = terrain_desc
363 cmd_TERRAIN.argtypes = 'char string'
367 cmd_PONG.argtypes = ''
369 def cmd_DEFAULT_COLORS(game):
370 game.tui.set_default_colors()
371 cmd_DEFAULT_COLORS.argtypes = ''
373 def cmd_RANDOM_COLORS(game):
374 game.tui.set_random_colors()
375 cmd_RANDOM_COLORS.argtypes = ''
377 def cmd_STATS(game, bladder_pressure, energy):
378 game.bladder_pressure_new = bladder_pressure
379 game.energy_new = energy
380 cmd_STATS.argtypes = 'int:nonneg int'
382 class Game(GameBase):
383 turn_complete = False
388 def __init__(self, *args, **kwargs):
389 super().__init__(*args, **kwargs)
390 self.register_command(cmd_LOGIN_OK)
391 self.register_command(cmd_ADMIN_OK)
392 self.register_command(cmd_PONG)
393 self.register_command(cmd_CHAT)
394 self.register_command(cmd_CHATFACE)
395 self.register_command(cmd_REPLY)
396 self.register_command(cmd_PLAYER_ID)
397 self.register_command(cmd_TURN)
398 self.register_command(cmd_OTHER_WIPE)
399 self.register_command(cmd_THING)
400 self.register_command(cmd_THING_TYPE)
401 self.register_command(cmd_THING_NAME)
402 self.register_command(cmd_THING_CHAR)
403 self.register_command(cmd_THING_FACE)
404 self.register_command(cmd_THING_HAT)
405 self.register_command(cmd_THING_DESIGN)
406 self.register_command(cmd_THING_CARRYING)
407 self.register_command(cmd_THING_INSTALLED)
408 self.register_command(cmd_TERRAIN)
409 self.register_command(cmd_MAP)
410 self.register_command(cmd_MAP_CONTROL)
411 self.register_command(cmd_PORTAL)
412 self.register_command(cmd_ANNOTATION)
413 self.register_command(cmd_GAME_STATE_COMPLETE)
414 self.register_command(cmd_PLAYERS_HAT_CHARS)
415 self.register_command(cmd_ARGUMENT_ERROR)
416 self.register_command(cmd_GAME_ERROR)
417 self.register_command(cmd_PLAY_ERROR)
418 self.register_command(cmd_TASKS)
419 self.register_command(cmd_FOV)
420 self.register_command(cmd_DEFAULT_COLORS)
421 self.register_command(cmd_RANDOM_COLORS)
422 self.register_command(cmd_STATS)
423 self.map_content = ''
424 self.players_hat_chars = ''
426 self.annotations = {}
427 self.annotations_new = {}
429 self.portals_new = {}
433 def get_string_options(self, string_option_type):
434 if string_option_type == 'map_geometry':
435 return ['Hex', 'Square']
436 elif string_option_type == 'thing_type':
437 return self.thing_types.keys()
440 def get_command(self, command_name):
441 from functools import partial
442 f = partial(self.commands[command_name], self)
443 f.argtypes = self.commands[command_name].argtypes
446 def get_thing_temp(self, id_):
447 for thing in self.things_new:
454 def __init__(self, name, has_input_prompt=False, shows_info=False,
455 is_intro=False, is_single_char_entry=False):
457 self.short_desc = mode_helps[name]['short']
458 self.available_modes = []
459 self.available_actions = []
460 self.has_input_prompt = has_input_prompt
461 self.shows_info = shows_info
462 self.is_intro = is_intro
463 self.help_intro = mode_helps[name]['long']
464 self.intro_msg = mode_helps[name]['intro']
465 self.is_single_char_entry = is_single_char_entry
468 def iter_available_modes(self, tui):
469 for mode_name in self.available_modes:
470 mode = getattr(tui, 'mode_' + mode_name)
473 key = tui.keys['switch_to_' + mode.name]
476 def list_available_modes(self, tui):
478 if len(self.available_modes) > 0:
479 msg = 'Other modes available from here:\n'
480 for mode, key in self.iter_available_modes(tui):
481 msg += '[%s] – %s\n' % (key, mode.short_desc)
484 def mode_switch_on_key(self, tui, key_pressed):
485 for mode, key in self.iter_available_modes(tui):
486 if key_pressed == key:
487 tui.switch_mode(mode.name)
492 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
493 mode_admin = Mode('admin')
494 mode_play = Mode('play')
495 mode_study = Mode('study', shows_info=True)
496 mode_write = Mode('write', is_single_char_entry=True)
497 mode_edit = Mode('edit')
498 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
499 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
500 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
501 mode_control_tile_draw = Mode('control_tile_draw')
502 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
503 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
504 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
505 mode_chat = Mode('chat', has_input_prompt=True)
506 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
507 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
508 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
509 mode_password = Mode('password', has_input_prompt=True)
510 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
511 mode_command_thing = Mode('command_thing', has_input_prompt=True)
512 mode_take_thing = Mode('take_thing', has_input_prompt=True)
513 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
514 mode_enter_face = Mode('enter_face', has_input_prompt=True)
515 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
516 mode_enter_design = Mode('enter_design', has_input_prompt=True)
520 def __init__(self, host):
523 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
524 "command_thing", "take_thing",
526 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
527 "install", "wear", "spin", "dance"]
528 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
529 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
530 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
531 "control_tile_type", "chat",
532 "study", "play", "edit"]
533 self.mode_admin.available_actions = ["move", "toggle_map_mode"]
534 self.mode_control_tile_draw.available_modes = ["admin_enter"]
535 self.mode_control_tile_draw.available_actions = ["move_explorer",
537 self.mode_edit.available_modes = ["write", "annotate", "portal",
538 "name_thing", "enter_face", "enter_hat",
539 "enter_design", "password",
540 "chat", "study", "play", "admin_enter"]
541 self.mode_edit.available_actions = ["move", "flatten", "install",
547 self.parser = Parser(self.game)
549 self.do_refresh = True
550 self.queue = queue.Queue()
551 self.login_name = None
552 self.map_mode = 'terrain + things'
553 self.password = 'foo'
554 self.switch_mode('waiting_for_server')
556 'switch_to_chat': 't',
557 'switch_to_play': 'p',
558 'switch_to_password': 'P',
559 'switch_to_annotate': 'M',
560 'switch_to_portal': 'T',
561 'switch_to_study': '?',
562 'switch_to_edit': 'E',
563 'switch_to_write': 'm',
564 'switch_to_name_thing': 'N',
565 'switch_to_command_thing': 'O',
566 'switch_to_admin_enter': 'A',
567 'switch_to_control_pw_type': 'C',
568 'switch_to_control_tile_type': 'Q',
569 'switch_to_admin_thing_protect': 'T',
571 'switch_to_enter_face': 'f',
572 'switch_to_enter_hat': 'H',
573 'switch_to_enter_design': 'D',
574 'switch_to_take_thing': 'z',
575 'switch_to_drop_thing': 'u',
584 'toggle_map_mode': 'L',
585 'toggle_tile_draw': 'm',
586 'hex_move_upleft': 'w',
587 'hex_move_upright': 'e',
588 'hex_move_right': 'd',
589 'hex_move_downright': 'x',
590 'hex_move_downleft': 'y',
591 'hex_move_left': 'a',
592 'square_move_up': 'w',
593 'square_move_left': 'a',
594 'square_move_down': 's',
595 'square_move_right': 'd',
597 if os.path.isfile('config.json'):
598 with open('config.json', 'r') as f:
599 keys_conf = json.loads(f.read())
601 self.keys[k] = keys_conf[k]
602 self.show_help = False
603 self.disconnected = True
604 self.force_instant_connect = True
605 self.input_lines = []
609 self.ascii_draw_stage = 0
610 self.full_ascii_draw = ''
611 self.offset = YX(0,0)
612 curses.wrapper(self.loop)
616 def handle_recv(msg):
622 self.log_msg('@ attempting connect')
623 socket_client_class = PlomSocketClient
624 if self.host.startswith('ws://') or self.host.startswith('wss://'):
625 socket_client_class = WebSocketClient
627 self.socket = socket_client_class(handle_recv, self.host)
628 self.socket_thread = threading.Thread(target=self.socket.run)
629 self.socket_thread.start()
630 self.disconnected = False
631 self.game.thing_types = {}
632 self.game.terrains = {}
633 time.sleep(0.1) # give potential SSL negotation some time …
634 self.socket.send('TASKS')
635 self.socket.send('TERRAINS')
636 self.socket.send('THING_TYPES')
637 self.switch_mode('login')
638 except ConnectionRefusedError:
639 self.log_msg('@ server connect failure')
640 self.disconnected = True
641 self.switch_mode('waiting_for_server')
642 self.do_refresh = True
645 self.log_msg('@ attempting reconnect')
647 # necessitated by some strange SSL race conditions with ws4py
648 time.sleep(0.1) # FIXME find out why exactly necessary
649 self.switch_mode('waiting_for_server')
654 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
655 raise BrokenSocketConnection
656 self.socket.send(msg)
657 except (BrokenPipeError, BrokenSocketConnection):
658 self.log_msg('@ server disconnected :(')
659 self.disconnected = True
660 self.force_instant_connect = True
661 self.do_refresh = True
663 def log_msg(self, msg):
665 if len(self.log) > 100:
666 self.log = self.log[-100:]
668 def restore_input_values(self):
669 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
670 self.input_ = self.game.annotations[self.explorer]
671 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
672 self.input_ = self.game.portals[self.explorer]
673 elif self.mode.name == 'password':
674 self.input_ = self.password
675 elif self.mode.name == 'name_thing':
676 if hasattr(self.game.player.carrying, 'name'):
677 self.input_ = self.game.player.carrying.name
678 elif self.mode.name == 'admin_thing_protect':
679 if hasattr(self.game.player.carrying, 'protection'):
680 self.input_ = self.game.player.carrying.protection
681 elif self.mode.name in {'enter_face', 'enter_hat'}:
682 start = self.ascii_draw_stage * 6
683 end = (self.ascii_draw_stage + 1) * 6
684 if self.mode.name == 'enter_face':
685 self.input_ = self.game.player.face[start:end]
686 elif self.mode.name == 'enter_hat':
687 self.input_ = self.game.player.hat[start:end]
688 elif self.mode.name == 'enter_design':
689 width = self.game.player.carrying.design[0].x
690 start = self.ascii_draw_stage * width
691 end = (self.ascii_draw_stage + 1) * width
692 self.input_ = self.game.player.carrying.design[1][start:end]
694 def send_tile_control_command(self):
695 self.send('SET_TILE_CONTROL %s %s' %
696 (self.explorer, quote(self.tile_control_char)))
698 def toggle_map_mode(self):
699 if self.map_mode == 'terrain only':
700 self.map_mode = 'terrain + annotations'
701 elif self.map_mode == 'terrain + annotations':
702 self.map_mode = 'terrain + things'
703 elif self.map_mode == 'terrain + things':
704 self.map_mode = 'protections'
705 elif self.map_mode == 'protections':
706 self.map_mode = 'terrain only'
708 def switch_mode(self, mode_name):
710 def fail(msg, return_mode='play'):
711 self.log_msg('? ' + msg)
713 self.switch_mode(return_mode)
715 if self.mode and self.mode.name == 'control_tile_draw':
716 self.log_msg('@ finished tile protection drawing.')
717 self.draw_face = False
718 self.tile_draw = False
719 if mode_name == 'command_thing' and\
720 (not self.game.player.carrying or
721 not self.game.player.carrying.commandable):
722 return fail('not carrying anything commandable')
723 if mode_name == 'name_thing' and not self.game.player.carrying:
724 return fail('not carrying anything to re-name', 'edit')
725 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
726 return fail('not carrying anything to protect')
727 if mode_name == 'take_thing' and self.game.player.carrying:
728 return fail('already carrying something')
729 if mode_name == 'drop_thing' and not self.game.player.carrying:
730 return fail('not carrying anything droppable')
731 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
732 return fail('not wearing hat to edit', 'edit')
733 if mode_name == 'enter_design' and\
734 (not self.game.player.carrying or
735 not hasattr(self.game.player.carrying, 'design')):
736 return fail('not carrying designable to edit', 'edit')
737 if mode_name == 'admin_enter' and self.is_admin:
739 self.mode = getattr(self, 'mode_' + mode_name)
740 if self.mode.name in {'control_tile_draw', 'control_tile_type',
742 self.map_mode = 'protections'
743 elif self.mode.name != 'edit':
744 self.map_mode = 'terrain + things'
745 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
746 self.explorer = YX(self.game.player.position.y,
747 self.game.player.position.x)
748 if self.mode.is_single_char_entry:
749 self.show_help = True
750 if len(self.mode.intro_msg) > 0:
751 self.log_msg(self.mode.intro_msg)
752 if self.mode.name == 'login':
754 self.send('LOGIN ' + quote(self.login_name))
756 self.log_msg('@ enter username')
757 elif self.mode.name == 'take_thing':
758 self.log_msg('Portable things in reach for pick-up:')
760 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
762 if type(self.game.map_geometry) == MapGeometrySquare:
763 directed_moves['UP'] = YX(-1, 0)
764 directed_moves['DOWN'] = YX(1, 0)
765 elif type(self.game.map_geometry) == MapGeometryHex:
766 if self.game.player.position.y % 2:
767 directed_moves['UPLEFT'] = YX(-1, 0)
768 directed_moves['UPRIGHT'] = YX(-1, 1)
769 directed_moves['DOWNLEFT'] = YX(1, 0)
770 directed_moves['DOWNRIGHT'] = YX(1, 1)
772 directed_moves['UPLEFT'] = YX(-1, -1)
773 directed_moves['UPRIGHT'] = YX(-1, 0)
774 directed_moves['DOWNLEFT'] = YX(1, -1)
775 directed_moves['DOWNRIGHT'] = YX(1, 0)
777 for direction in directed_moves:
778 move = directed_moves[direction]
779 select_range[direction] = self.game.player.position + move
780 self.selectables = []
782 for direction in select_range:
783 for t in [t for t in self.game.things
784 if t.portable and t.position == select_range[direction]]:
785 self.selectables += [t.id_]
786 directions += [direction]
787 if len(self.selectables) == 0:
788 return fail('nothing to pick-up')
790 for i in range(len(self.selectables)):
791 t = self.game.get_thing(self.selectables[i])
792 self.log_msg('%s %s: %s' % (i, directions[i],
793 self.get_thing_info(t)))
794 elif self.mode.name == 'drop_thing':
795 self.log_msg('Direction to drop thing to:')
797 ['HERE'] + list(self.game.tui.movement_keys.values())
798 for i in range(len(self.selectables)):
799 self.log_msg(str(i) + ': ' + self.selectables[i])
800 elif self.mode.name == 'enter_hat':
801 self.log_msg('legal characters: ' + self.game.players_hat_chars)
802 elif self.mode.name == 'command_thing':
803 self.send('TASK:COMMAND ' + quote('HELP'))
804 elif self.mode.name == 'control_pw_pw':
805 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
806 elif self.mode.name == 'control_tile_draw':
807 self.log_msg('@ can draw protection character "%s", turn drawing on/off with [%s], finish with [%s].' % (self.tile_control_char, self.keys['toggle_tile_draw'], self.keys['switch_to_admin_enter']))
809 self.restore_input_values()
811 def set_default_colors(self):
812 curses.init_color(1, 1000, 1000, 1000)
813 curses.init_color(2, 0, 0, 0)
814 self.do_refresh = True
816 def set_random_colors(self):
820 return int(offset + random.random()*375)
822 curses.init_color(1, rand(625), rand(625), rand(625))
823 curses.init_color(2, rand(0), rand(0), rand(0))
824 self.do_refresh = True
828 return self.info_cached
829 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
831 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
832 info_to_cache += 'outside field of view'
834 for t in self.game.things:
835 if t.position == self.explorer:
836 info_to_cache += 'THING: %s' % self.get_thing_info(t)
837 protection = t.protection
838 if protection == '.':
840 info_to_cache += ' / protection: %s\n' % protection
841 if hasattr(t, 'hat'):
842 info_to_cache += t.hat[0:6] + '\n'
843 info_to_cache += t.hat[6:12] + '\n'
844 info_to_cache += t.hat[12:18] + '\n'
845 if hasattr(t, 'face'):
846 info_to_cache += t.face[0:6] + '\n'
847 info_to_cache += t.face[6:12] + '\n'
848 info_to_cache += t.face[12:18] + '\n'
849 if hasattr(t, 'design'):
851 line_length = t.design[0].x
852 wrapper = textwrap.TextWrapper(drop_whitespace=False,
854 lines = wrapper.wrap(t.design[1])
855 if t.type_ == 'Sign':
856 info_to_cache += '-' * (line_length + 4) + '\n'
858 info_to_cache += '| %s |\n' % line
859 if t.type_ == 'Sign':
860 info_to_cache += '-' * (line_length + 4) + '\n'
861 terrain_char = self.game.map_content[pos_i]
863 if terrain_char in self.game.terrains:
864 terrain_desc = self.game.terrains[terrain_char]
865 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
867 protection = self.game.map_control_content[pos_i]
868 if protection == '.':
869 protection = 'unprotected'
870 info_to_cache += 'PROTECTION: %s\n' % protection
871 if self.explorer in self.game.portals:
872 info_to_cache += 'PORTAL: ' +\
873 self.game.portals[self.explorer] + '\n'
875 info_to_cache += 'PORTAL: (none)\n'
876 if self.explorer in self.game.annotations:
877 info_to_cache += 'ANNOTATION: ' +\
878 self.game.annotations[self.explorer]
879 self.info_cached = info_to_cache
880 return self.info_cached
882 def get_thing_info(self, t):
884 (t.type_, self.game.thing_types[t.type_])
885 if hasattr(t, 'thing_char'):
887 if hasattr(t, 'name'):
888 info += ' (%s)' % t.name
889 if hasattr(t, 'installed'):
890 info += ' / installed'
893 def loop(self, stdscr):
896 def safe_addstr(y, x, line):
897 if y < self.size.y - 1 or x + len(line) < self.size.x:
898 stdscr.addstr(y, x, line, curses.color_pair(1))
899 else: # workaround to <https://stackoverflow.com/q/7063128>
900 cut_i = self.size.x - x - 1
902 last_char = line[cut_i]
903 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
904 stdscr.insstr(y, self.size.x - 2, ' ')
905 stdscr.addstr(y, x, cut, curses.color_pair(1))
907 def handle_input(msg):
908 command, args = self.parser.parse(msg)
911 def task_action_on(action):
912 return action_tasks[action] in self.game.tasks
914 def msg_into_lines_of_width(msg, width):
918 for i in range(len(msg)):
919 if x >= width or msg[i] == "\n":
931 def reset_screen_size():
932 self.size = YX(*stdscr.getmaxyx())
933 self.size = self.size - YX(self.size.y % 4, 0)
934 self.size = self.size - YX(0, self.size.x % 4)
935 self.window_width = int(self.size.x / 2)
937 def recalc_input_lines():
938 if not self.mode.has_input_prompt:
939 self.input_lines = []
941 self.input_lines = msg_into_lines_of_width(input_prompt
945 def move_explorer(direction):
946 target = self.game.map_geometry.move_yx(self.explorer, direction)
948 self.info_cached = None
949 self.explorer = target
951 self.send_tile_control_command()
957 for line in self.log:
958 lines += msg_into_lines_of_width(line, self.window_width)
961 max_y = self.size.y - len(self.input_lines)
962 for i in range(len(lines)):
963 if (i >= max_y - height_header):
965 safe_addstr(max_y - i - 1, self.window_width, lines[i])
968 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
969 lines = msg_into_lines_of_width(info, self.window_width)
971 for i in range(len(lines)):
972 y = height_header + i
973 if y >= self.size.y - len(self.input_lines):
975 safe_addstr(y, self.window_width, lines[i])
978 y = self.size.y - len(self.input_lines)
979 for i in range(len(self.input_lines)):
980 safe_addstr(y, self.window_width, self.input_lines[i])
984 stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
985 self.game.bladder_pressure)
986 safe_addstr(0, self.window_width, stats)
989 help = "hit [%s] for help" % self.keys['help']
990 if self.mode.has_input_prompt:
991 help = "enter /help for help"
992 safe_addstr(1, self.window_width,
993 'MODE: %s – %s' % (self.mode.short_desc, help))
996 if (not self.game.turn_complete) and len(self.map_lines) == 0:
998 if self.game.turn_complete:
1000 for y in range(self.game.map_geometry.size.y):
1001 start = self.game.map_geometry.size.x * y
1002 end = start + self.game.map_geometry.size.x
1003 if self.map_mode == 'protections':
1004 map_lines_split += [[c + ' ' for c
1005 in self.game.map_control_content[start:end]]]
1007 map_lines_split += [[c + ' ' for c
1008 in self.game.map_content[start:end]]]
1009 if self.map_mode == 'terrain + annotations':
1010 for p in self.game.annotations:
1011 map_lines_split[p.y][p.x] = 'A '
1012 elif self.map_mode == 'terrain + things':
1013 for p in self.game.portals.keys():
1014 original = map_lines_split[p.y][p.x]
1015 map_lines_split[p.y][p.x] = original[0] + 'P'
1018 def draw_thing(t, used_positions):
1019 symbol = self.game.thing_types[t.type_]
1021 if hasattr(t, 'thing_char'):
1022 meta_char = t.thing_char
1023 if t.position in used_positions:
1025 if hasattr(t, 'carrying') and t.carrying:
1027 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
1028 used_positions += [t.position]
1030 for t in [t for t in self.game.things if t.type_ != 'Player']:
1031 draw_thing(t, used_positions)
1032 for t in [t for t in self.game.things if t.type_ == 'Player']:
1033 draw_thing(t, used_positions)
1034 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1035 map_lines_split[self.explorer.y][self.explorer.x] = '??'
1036 elif self.map_mode != 'terrain + things':
1037 map_lines_split[self.game.player.position.y]\
1038 [self.game.player.position.x] = '??'
1040 if type(self.game.map_geometry) == MapGeometryHex:
1042 for line in map_lines_split:
1043 self.map_lines += [indent * ' ' + ''.join(line)]
1044 indent = 0 if indent else 1
1046 for line in map_lines_split:
1047 self.map_lines += [''.join(line)]
1048 window_center = YX(int(self.size.y / 2),
1049 int(self.window_width / 2))
1050 center = self.game.player.position
1051 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1052 center = self.explorer
1053 center = YX(center.y, center.x * 2)
1054 self.offset = center - window_center
1055 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1056 self.offset += YX(0, 1)
1057 term_y = max(0, -self.offset.y)
1058 term_x = max(0, -self.offset.x)
1059 map_y = max(0, self.offset.y)
1060 map_x = max(0, self.offset.x)
1061 while term_y < self.size.y and map_y < len(self.map_lines):
1062 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1063 safe_addstr(term_y, term_x, to_draw)
1067 def draw_face_popup():
1068 t = self.game.get_thing(self.draw_face)
1069 if not t or not hasattr(t, 'face'):
1070 self.draw_face = False
1073 start_x = self.window_width - 10
1075 if hasattr(t, 'thing_char'):
1076 t_char = t.thing_char
1077 def draw_body_part(body_part, end_y):
1078 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1079 safe_addstr(end_y - 3, start_x, '| |')
1080 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1081 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1082 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1084 if hasattr(t, 'face'):
1085 draw_body_part(t.face, self.size.y - 2)
1086 if hasattr(t, 'hat'):
1087 draw_body_part(t.hat, self.size.y - 5)
1088 safe_addstr(self.size.y - 1, start_x, '| |')
1091 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1092 self.mode.help_intro)
1093 if len(self.mode.available_actions) > 0:
1094 content += "Available actions:\n"
1095 for action in self.mode.available_actions:
1096 if action in action_tasks:
1097 if action_tasks[action] not in self.game.tasks:
1099 if action == 'move_explorer':
1101 if action == 'move':
1102 key = ','.join(self.movement_keys)
1104 key = self.keys[action]
1105 content += '[%s] – %s\n' % (key, action_descriptions[action])
1107 content += self.mode.list_available_modes(self)
1108 for i in range(self.size.y):
1110 self.window_width * (not self.mode.has_input_prompt),
1111 ' ' * self.window_width)
1113 for line in content.split('\n'):
1114 lines += msg_into_lines_of_width(line, self.window_width)
1115 for i in range(len(lines)):
1116 if i >= self.size.y:
1119 self.window_width * (not self.mode.has_input_prompt),
1124 stdscr.bkgd(' ', curses.color_pair(1))
1125 recalc_input_lines()
1126 if self.mode.has_input_prompt:
1128 if self.mode.shows_info:
1133 if not self.mode.is_intro:
1138 if self.draw_face and self.mode.name in {'chat', 'play'}:
1141 def pick_selectable(task_name):
1143 i = int(self.input_)
1144 if i < 0 or i >= len(self.selectables):
1145 self.log_msg('? invalid index, aborted')
1147 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1149 self.log_msg('? invalid index, aborted')
1151 self.switch_mode('play')
1153 def enter_ascii_art(command, height, width, with_pw=False):
1154 if len(self.input_) > width:
1155 self.log_msg('? wrong input length, '
1156 'must be max %s; try again' % width)
1158 if len(self.input_) < width:
1159 self.input_ += ' ' * (width - len(self.input_))
1160 self.log_msg(' ' + self.input_)
1161 self.full_ascii_draw += self.input_
1162 self.ascii_draw_stage += 1
1163 if self.ascii_draw_stage < height:
1164 self.restore_input_values()
1167 self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1168 quote(self.password)))
1170 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1171 self.full_ascii_draw = ""
1172 self.ascii_draw_stage = 0
1174 self.switch_mode('edit')
1176 action_descriptions = {
1178 'flatten': 'flatten surroundings',
1179 'teleport': 'teleport',
1180 'take_thing': 'pick up thing',
1181 'drop_thing': 'drop thing',
1182 'toggle_map_mode': 'toggle map view',
1183 'toggle_tile_draw': 'toggle protection character drawing',
1184 'install': '(un-)install',
1185 'wear': '(un-)wear',
1186 'door': 'open/close',
1187 'consume': 'consume',
1193 'flatten': 'FLATTEN_SURROUNDINGS',
1194 'take_thing': 'PICK_UP',
1195 'drop_thing': 'DROP',
1197 'install': 'INSTALL',
1200 'command': 'COMMAND',
1201 'consume': 'INTOXICATE',
1206 curses.curs_set(False) # hide cursor
1207 curses.start_color()
1208 self.set_default_colors()
1209 curses.init_pair(1, 1, 2)
1212 self.explorer = YX(0, 0)
1214 store_widechar = False
1216 interval = datetime.timedelta(seconds=5)
1217 last_ping = datetime.datetime.now() - interval
1219 if self.disconnected and self.force_instant_connect:
1220 self.force_instant_connect = False
1222 now = datetime.datetime.now()
1223 if now - last_ping > interval:
1224 if self.disconnected:
1234 self.do_refresh = False
1237 msg = self.queue.get(block=False)
1242 key = stdscr.getkey()
1243 self.do_refresh = True
1244 except curses.error:
1249 # workaround for <https://stackoverflow.com/a/56390915>
1251 store_widechar = False
1252 key = bytes([195, keycode]).decode()
1254 store_widechar = True
1256 self.show_help = False
1257 self.draw_face = False
1258 if key == 'KEY_RESIZE':
1260 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1261 self.input_ = self.input_[:-1]
1262 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1263 or (self.mode.has_input_prompt and key == '\n'
1264 and self.input_ == ''\
1265 and self.mode.name in {'chat', 'command_thing',
1266 'take_thing', 'drop_thing',
1268 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1269 self.log_msg('@ aborted')
1270 self.switch_mode('play')
1271 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1272 self.show_help = True
1274 self.restore_input_values()
1275 elif self.mode.has_input_prompt and key != '\n': # Return key
1277 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1278 if len(self.input_) > max_length:
1279 self.input_ = self.input_[:max_length]
1280 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1281 self.show_help = True
1282 elif self.mode.name == 'login' and key == '\n':
1283 self.login_name = self.input_
1284 self.send('LOGIN ' + quote(self.input_))
1286 elif self.mode.name == 'enter_face' and key == '\n':
1287 enter_ascii_art('PLAYER_FACE', 3, 6)
1288 elif self.mode.name == 'enter_hat' and key == '\n':
1289 enter_ascii_art('PLAYER_HAT', 3, 6)
1290 elif self.mode.name == 'enter_design' and key == '\n':
1291 enter_ascii_art('THING_DESIGN',
1292 self.game.player.carrying.design[0].y,
1293 self.game.player.carrying.design[0].x, True)
1294 elif self.mode.name == 'take_thing' and key == '\n':
1295 pick_selectable('PICK_UP')
1296 elif self.mode.name == 'drop_thing' and key == '\n':
1297 pick_selectable('DROP')
1298 elif self.mode.name == 'command_thing' and key == '\n':
1299 self.send('TASK:COMMAND ' + quote(self.input_))
1301 elif self.mode.name == 'control_pw_pw' and key == '\n':
1302 if self.input_ == '':
1303 self.log_msg('@ aborted')
1305 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1306 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1307 self.switch_mode('admin')
1308 elif self.mode.name == 'password' and key == '\n':
1309 if self.input_ == '':
1311 self.password = self.input_
1312 self.switch_mode('edit')
1313 elif self.mode.name == 'admin_enter' and key == '\n':
1314 self.send('BECOME_ADMIN ' + quote(self.input_))
1315 self.switch_mode('play')
1316 elif self.mode.name == 'control_pw_type' and key == '\n':
1317 if len(self.input_) != 1:
1318 self.log_msg('@ entered non-single-char, therefore aborted')
1319 self.switch_mode('admin')
1321 self.tile_control_char = self.input_
1322 self.switch_mode('control_pw_pw')
1323 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1324 if len(self.input_) != 1:
1325 self.log_msg('@ entered non-single-char, therefore aborted')
1327 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1328 self.log_msg('@ sent new protection character for thing')
1329 self.switch_mode('admin')
1330 elif self.mode.name == 'control_tile_type' and key == '\n':
1331 if len(self.input_) != 1:
1332 self.log_msg('@ entered non-single-char, therefore aborted')
1333 self.switch_mode('admin')
1335 self.tile_control_char = self.input_
1336 self.switch_mode('control_tile_draw')
1337 elif self.mode.name == 'chat' and key == '\n':
1338 if self.input_ == '':
1340 if self.input_[0] == '/':
1341 if self.input_.startswith('/nick'):
1342 tokens = self.input_.split(maxsplit=1)
1343 if len(tokens) == 2:
1344 self.send('NICK ' + quote(tokens[1]))
1346 self.log_msg('? need login name')
1348 self.log_msg('? unknown command')
1350 self.send('ALL ' + quote(self.input_))
1352 elif self.mode.name == 'name_thing' and key == '\n':
1353 if self.input_ == '':
1355 self.send('THING_NAME %s %s' % (quote(self.input_),
1356 quote(self.password)))
1357 self.switch_mode('edit')
1358 elif self.mode.name == 'annotate' and key == '\n':
1359 if self.input_ == '':
1361 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1362 quote(self.password)))
1363 self.switch_mode('edit')
1364 elif self.mode.name == 'portal' and key == '\n':
1365 if self.input_ == '':
1367 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1368 quote(self.password)))
1369 self.switch_mode('edit')
1370 elif self.mode.name == 'study':
1371 if self.mode.mode_switch_on_key(self, key):
1373 elif key == self.keys['toggle_map_mode']:
1374 self.toggle_map_mode()
1375 elif key in self.movement_keys:
1376 move_explorer(self.movement_keys[key])
1377 elif self.mode.name == 'play':
1378 if self.mode.mode_switch_on_key(self, key):
1380 elif key == self.keys['door'] and task_action_on('door'):
1381 self.send('TASK:DOOR')
1382 elif key == self.keys['consume'] and task_action_on('consume'):
1383 self.send('TASK:INTOXICATE')
1384 elif key == self.keys['wear'] and task_action_on('wear'):
1385 self.send('TASK:WEAR')
1386 elif key == self.keys['spin'] and task_action_on('spin'):
1387 self.send('TASK:SPIN')
1388 elif key == self.keys['dance'] and task_action_on('dance'):
1389 self.send('TASK:DANCE')
1390 elif key == self.keys['teleport']:
1391 if self.game.player.position in self.game.portals:
1392 self.host = self.game.portals[self.game.player.position]
1396 self.log_msg('? not standing on portal')
1397 elif key in self.movement_keys and task_action_on('move'):
1398 self.send('TASK:MOVE ' + self.movement_keys[key])
1399 elif self.mode.name == 'write':
1400 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1401 self.switch_mode('edit')
1402 elif self.mode.name == 'control_tile_draw':
1403 if self.mode.mode_switch_on_key(self, key):
1405 elif key in self.movement_keys:
1406 move_explorer(self.movement_keys[key])
1407 elif key == self.keys['toggle_tile_draw']:
1408 self.tile_draw = False if self.tile_draw else True
1409 elif self.mode.name == 'admin':
1410 if self.mode.mode_switch_on_key(self, key):
1412 elif key == self.keys['toggle_map_mode']:
1413 self.toggle_map_mode()
1414 elif key in self.movement_keys and task_action_on('move'):
1415 self.send('TASK:MOVE ' + self.movement_keys[key])
1416 elif self.mode.name == 'edit':
1417 if self.mode.mode_switch_on_key(self, key):
1419 elif key == self.keys['flatten'] and task_action_on('flatten'):
1420 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1421 elif key == self.keys['install'] and task_action_on('install'):
1422 self.send('TASK:INSTALL %s' % quote(self.password))
1423 elif key == self.keys['toggle_map_mode']:
1424 self.toggle_map_mode()
1425 elif key in self.movement_keys and task_action_on('move'):
1426 self.send('TASK:MOVE ' + self.movement_keys[key])
1428 if len(sys.argv) != 2:
1429 raise ArgError('wrong number of arguments, need game host')