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.'
67 '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.'
70 'short': 'change protection character password',
71 'intro': '@ enter protection character for which you want to change the password:',
72 '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.'
75 'short': 'change protection character password',
77 '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.'
79 'control_tile_type': {
80 'short': 'change tiles protection',
81 'intro': '@ enter protection character which you want to draw:',
82 '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.'
84 'control_tile_draw': {
85 'short': 'change tiles protection',
87 '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.'
90 'short': 'annotate tile',
92 '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.'
95 'short': 'edit portal',
97 '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.'
102 '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'
107 'long': 'Enter your player name.'
109 'waiting_for_server': {
110 'short': 'waiting for server response',
111 'intro': '@ waiting for server …',
112 'long': 'Waiting for a server response.'
115 'short': 'waiting for server response',
117 'long': 'Waiting for a server response.'
120 'short': 'set world edit password',
122 '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.'
125 'short': 'become admin',
126 'intro': '@ enter admin password:',
127 'long': 'This mode allows you to become admin if you know an admin password.'
132 'long': 'This mode allows you access to actions limited to administrators.'
136 from ws4py.client import WebSocketBaseClient
137 class WebSocketClient(WebSocketBaseClient):
139 def __init__(self, recv_handler, *args, **kwargs):
140 super().__init__(*args, **kwargs)
141 self.recv_handler = recv_handler
144 def received_message(self, message):
146 message = str(message)
147 self.recv_handler(message)
150 def plom_closed(self):
151 return self.client_terminated
153 from plomrogue.io_tcp import PlomSocket
154 class PlomSocketClient(PlomSocket):
156 def __init__(self, recv_handler, url):
158 self.recv_handler = recv_handler
159 host, port = url.split(':')
160 super().__init__(socket.create_connection((host, port)))
168 for msg in self.recv():
169 if msg == 'NEED_SSL':
170 self.socket = ssl.wrap_socket(self.socket)
172 self.recv_handler(msg)
173 except BrokenSocketConnection:
174 pass # we assume socket will be known as dead by now
176 def cmd_TURN(game, n):
177 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_OTHER_WIPE(game):
181 game.portals_new = {}
182 game.annotations_new = {}
184 cmd_OTHER_WIPE.argtypes = ''
186 def cmd_LOGIN_OK(game):
187 game.tui.switch_mode('post_login_wait')
188 game.tui.send('GET_GAMESTATE')
189 game.tui.log_msg('@ welcome!')
190 game.tui.log_msg('@ hint: see top of terminal for how to get help.')
191 game.tui.log_msg('@ hint: enter study mode to understand your environment.')
192 cmd_LOGIN_OK.argtypes = ''
194 def cmd_ADMIN_OK(game):
195 game.tui.is_admin = True
196 game.tui.log_msg('@ you now have admin rights')
197 game.tui.switch_mode('admin')
198 game.tui.do_refresh = True
199 cmd_ADMIN_OK.argtypes = ''
201 def cmd_REPLY(game, msg):
202 game.tui.log_msg('#MUSICPLAYER: ' + msg)
203 game.tui.do_refresh = True
204 cmd_REPLY.argtypes = 'string'
206 def cmd_CHAT(game, msg):
207 game.tui.log_msg('# ' + msg)
208 game.tui.do_refresh = True
209 cmd_CHAT.argtypes = 'string'
211 def cmd_CHATFACE(game, thing_id):
212 game.tui.draw_face = thing_id
213 game.tui.do_refresh = True
214 cmd_CHATFACE.argtypes = 'int:pos'
216 def cmd_PLAYER_ID(game, player_id):
217 game.player_id = player_id
218 cmd_PLAYER_ID.argtypes = 'int:nonneg'
220 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
221 game.players_hat_chars_new = hat_chars
222 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
224 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
225 t = game.get_thing_temp(thing_id)
227 t = ThingBase(game, thing_id)
228 game.things_new += [t]
231 t.protection = protection
232 t.portable = portable
233 t.commandable = commandable
234 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
236 def cmd_THING_NAME(game, thing_id, name):
237 t = game.get_thing_temp(thing_id)
239 cmd_THING_NAME.argtypes = 'int:pos string'
241 def cmd_THING_FACE(game, thing_id, face):
242 t = game.get_thing_temp(thing_id)
244 cmd_THING_FACE.argtypes = 'int:pos string'
246 def cmd_THING_HAT(game, thing_id, hat):
247 t = game.get_thing_temp(thing_id)
249 cmd_THING_HAT.argtypes = 'int:pos string'
251 def cmd_THING_DESIGN(game, thing_id, size, design):
252 t = game.get_thing_temp(thing_id)
253 t.design = [size, design]
254 cmd_THING_DESIGN.argtypes = 'int:pos yx_tuple string'
256 def cmd_THING_CHAR(game, thing_id, c):
257 t = game.get_thing_temp(thing_id)
259 cmd_THING_CHAR.argtypes = 'int:pos char'
261 def cmd_MAP(game, geometry, size, content):
262 map_geometry_class = globals()['MapGeometry' + geometry]
263 game.map_geometry_new = map_geometry_class(size)
264 game.map_content_new = content
265 if type(game.map_geometry_new) == MapGeometrySquare:
266 game.tui.movement_keys = {
267 game.tui.keys['square_move_up']: 'UP',
268 game.tui.keys['square_move_left']: 'LEFT',
269 game.tui.keys['square_move_down']: 'DOWN',
270 game.tui.keys['square_move_right']: 'RIGHT',
272 elif type(game.map_geometry_new) == MapGeometryHex:
273 game.tui.movement_keys = {
274 game.tui.keys['hex_move_upleft']: 'UPLEFT',
275 game.tui.keys['hex_move_upright']: 'UPRIGHT',
276 game.tui.keys['hex_move_right']: 'RIGHT',
277 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
278 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
279 game.tui.keys['hex_move_left']: 'LEFT',
281 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
283 def cmd_FOV(game, content):
284 game.fov_new = content
285 cmd_FOV.argtypes = 'string'
287 def cmd_MAP_CONTROL(game, content):
288 game.map_control_content_new = content
289 cmd_MAP_CONTROL.argtypes = 'string'
291 def cmd_GAME_STATE_COMPLETE(game):
292 game.tui.do_refresh = True
293 game.tui.info_cached = None
294 game.things = game.things_new
295 game.portals = game.portals_new
296 game.annotations = game.annotations_new
297 game.fov = game.fov_new
298 game.map_geometry = game.map_geometry_new
299 game.map_content = game.map_content_new
300 game.map_control_content = game.map_control_content_new
301 game.player = game.get_thing(game.player_id)
302 game.players_hat_chars = game.players_hat_chars_new
303 game.bladder_pressure = game.bladder_pressure_new
304 game.energy = game.energy_new
305 game.turn_complete = True
306 if game.tui.mode.name == 'post_login_wait':
307 game.tui.switch_mode('play')
308 cmd_GAME_STATE_COMPLETE.argtypes = ''
310 def cmd_PORTAL(game, position, msg):
311 game.portals_new[position] = msg
312 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
314 def cmd_PLAY_ERROR(game, msg):
315 game.tui.log_msg('? ' + msg)
316 game.tui.flash = True
317 game.tui.do_refresh = True
318 cmd_PLAY_ERROR.argtypes = 'string'
320 def cmd_GAME_ERROR(game, msg):
321 game.tui.log_msg('? game error: ' + msg)
322 game.tui.do_refresh = True
323 cmd_GAME_ERROR.argtypes = 'string'
325 def cmd_ARGUMENT_ERROR(game, msg):
326 game.tui.log_msg('? syntax error: ' + msg)
327 game.tui.do_refresh = True
328 cmd_ARGUMENT_ERROR.argtypes = 'string'
330 def cmd_ANNOTATION(game, position, msg):
331 game.annotations_new[position] = msg
332 if game.tui.mode.shows_info:
333 game.tui.do_refresh = True
334 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
336 def cmd_TASKS(game, tasks_comma_separated):
337 game.tasks = tasks_comma_separated.split(',')
338 game.tui.mode_write.legal = 'WRITE' in game.tasks
339 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
340 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
341 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
342 cmd_TASKS.argtypes = 'string'
344 def cmd_THING_TYPE(game, thing_type, symbol_hint):
345 game.thing_types[thing_type] = symbol_hint
346 cmd_THING_TYPE.argtypes = 'string char'
348 def cmd_THING_INSTALLED(game, thing_id):
349 game.get_thing_temp(thing_id).installed = True
350 cmd_THING_INSTALLED.argtypes = 'int:pos'
352 def cmd_THING_CARRYING(game, thing_id, carried_id):
353 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
354 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
356 def cmd_TERRAIN(game, terrain_char, terrain_desc):
357 game.terrains[terrain_char] = terrain_desc
358 cmd_TERRAIN.argtypes = 'char string'
362 cmd_PONG.argtypes = ''
364 def cmd_DEFAULT_COLORS(game):
365 game.tui.set_default_colors()
366 cmd_DEFAULT_COLORS.argtypes = ''
368 def cmd_RANDOM_COLORS(game):
369 game.tui.set_random_colors()
370 cmd_RANDOM_COLORS.argtypes = ''
372 def cmd_STATS(game, bladder_pressure, energy):
373 game.bladder_pressure_new = bladder_pressure
374 game.energy_new = energy
375 cmd_STATS.argtypes = 'int:nonneg int'
377 class Game(GameBase):
378 turn_complete = False
383 def __init__(self, *args, **kwargs):
384 super().__init__(*args, **kwargs)
385 self.register_command(cmd_LOGIN_OK)
386 self.register_command(cmd_ADMIN_OK)
387 self.register_command(cmd_PONG)
388 self.register_command(cmd_CHAT)
389 self.register_command(cmd_CHATFACE)
390 self.register_command(cmd_REPLY)
391 self.register_command(cmd_PLAYER_ID)
392 self.register_command(cmd_TURN)
393 self.register_command(cmd_OTHER_WIPE)
394 self.register_command(cmd_THING)
395 self.register_command(cmd_THING_TYPE)
396 self.register_command(cmd_THING_NAME)
397 self.register_command(cmd_THING_CHAR)
398 self.register_command(cmd_THING_FACE)
399 self.register_command(cmd_THING_HAT)
400 self.register_command(cmd_THING_DESIGN)
401 self.register_command(cmd_THING_CARRYING)
402 self.register_command(cmd_THING_INSTALLED)
403 self.register_command(cmd_TERRAIN)
404 self.register_command(cmd_MAP)
405 self.register_command(cmd_MAP_CONTROL)
406 self.register_command(cmd_PORTAL)
407 self.register_command(cmd_ANNOTATION)
408 self.register_command(cmd_GAME_STATE_COMPLETE)
409 self.register_command(cmd_PLAYERS_HAT_CHARS)
410 self.register_command(cmd_ARGUMENT_ERROR)
411 self.register_command(cmd_GAME_ERROR)
412 self.register_command(cmd_PLAY_ERROR)
413 self.register_command(cmd_TASKS)
414 self.register_command(cmd_FOV)
415 self.register_command(cmd_DEFAULT_COLORS)
416 self.register_command(cmd_RANDOM_COLORS)
417 self.register_command(cmd_STATS)
418 self.map_content = ''
419 self.players_hat_chars = ''
421 self.annotations = {}
422 self.annotations_new = {}
424 self.portals_new = {}
428 def get_string_options(self, string_option_type):
429 if string_option_type == 'map_geometry':
430 return ['Hex', 'Square']
431 elif string_option_type == 'thing_type':
432 return self.thing_types.keys()
435 def get_command(self, command_name):
436 from functools import partial
437 f = partial(self.commands[command_name], self)
438 f.argtypes = self.commands[command_name].argtypes
441 def get_thing_temp(self, id_):
442 for thing in self.things_new:
449 def __init__(self, name, has_input_prompt=False, shows_info=False,
450 is_intro=False, is_single_char_entry=False):
452 self.short_desc = mode_helps[name]['short']
453 self.available_modes = []
454 self.available_actions = []
455 self.has_input_prompt = has_input_prompt
456 self.shows_info = shows_info
457 self.is_intro = is_intro
458 self.help_intro = mode_helps[name]['long']
459 self.intro_msg = mode_helps[name]['intro']
460 self.is_single_char_entry = is_single_char_entry
463 def iter_available_modes(self, tui):
464 for mode_name in self.available_modes:
465 mode = getattr(tui, 'mode_' + mode_name)
468 key = tui.keys['switch_to_' + mode.name]
471 def list_available_modes(self, tui):
473 if len(self.available_modes) > 0:
474 msg = 'Other modes available from here:\n'
475 for mode, key in self.iter_available_modes(tui):
476 msg += '[%s] – %s\n' % (key, mode.short_desc)
479 def mode_switch_on_key(self, tui, key_pressed):
480 for mode, key in self.iter_available_modes(tui):
481 if key_pressed == key:
482 tui.switch_mode(mode.name)
487 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
488 mode_admin = Mode('admin')
489 mode_play = Mode('play')
490 mode_study = Mode('study', shows_info=True)
491 mode_write = Mode('write', is_single_char_entry=True)
492 mode_edit = Mode('edit')
493 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
494 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
495 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
496 mode_control_tile_draw = Mode('control_tile_draw')
497 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
498 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
499 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
500 mode_chat = Mode('chat', has_input_prompt=True)
501 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
502 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
503 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
504 mode_password = Mode('password', has_input_prompt=True)
505 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
506 mode_command_thing = Mode('command_thing', has_input_prompt=True)
507 mode_take_thing = Mode('take_thing', has_input_prompt=True)
508 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
509 mode_enter_face = Mode('enter_face', has_input_prompt=True)
510 mode_enter_design = Mode('enter_design', has_input_prompt=True)
514 def __init__(self, host):
517 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
518 "command_thing", "take_thing",
520 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
521 "install", "wear", "spin", "dance"]
522 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
523 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
524 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
525 "control_tile_type", "chat",
526 "study", "play", "edit"]
527 self.mode_admin.available_actions = ["move", "toggle_map_mode"]
528 self.mode_control_tile_draw.available_modes = ["admin_enter"]
529 self.mode_control_tile_draw.available_actions = ["move_explorer",
531 self.mode_edit.available_modes = ["write", "annotate", "portal",
532 "name_thing", "enter_face", "enter_design",
534 "chat", "study", "play", "admin_enter"]
535 self.mode_edit.available_actions = ["move", "flatten", "install",
541 self.parser = Parser(self.game)
543 self.do_refresh = True
544 self.queue = queue.Queue()
545 self.login_name = None
546 self.map_mode = 'terrain + things'
547 self.password = 'foo'
548 self.switch_mode('waiting_for_server')
550 'switch_to_chat': 't',
551 'switch_to_play': 'p',
552 'switch_to_password': 'P',
553 'switch_to_annotate': 'M',
554 'switch_to_portal': 'T',
555 'switch_to_study': '?',
556 'switch_to_edit': 'E',
557 'switch_to_write': 'm',
558 'switch_to_name_thing': 'N',
559 'switch_to_command_thing': 'O',
560 'switch_to_admin_enter': 'A',
561 'switch_to_control_pw_type': 'C',
562 'switch_to_control_tile_type': 'Q',
563 'switch_to_admin_thing_protect': 'T',
565 'switch_to_enter_face': 'f',
566 'switch_to_enter_design': 'D',
567 'switch_to_take_thing': 'z',
568 'switch_to_drop_thing': 'u',
577 'toggle_map_mode': 'L',
578 'toggle_tile_draw': 'm',
579 'hex_move_upleft': 'w',
580 'hex_move_upright': 'e',
581 'hex_move_right': 'd',
582 'hex_move_downright': 'x',
583 'hex_move_downleft': 'y',
584 'hex_move_left': 'a',
585 'square_move_up': 'w',
586 'square_move_left': 'a',
587 'square_move_down': 's',
588 'square_move_right': 'd',
590 if os.path.isfile('config.json'):
591 with open('config.json', 'r') as f:
592 keys_conf = json.loads(f.read())
594 self.keys[k] = keys_conf[k]
595 self.show_help = False
596 self.disconnected = True
597 self.force_instant_connect = True
598 self.input_lines = []
602 self.ascii_draw_stage = 0
603 self.full_ascii_draw = ''
604 self.offset = YX(0,0)
605 curses.wrapper(self.loop)
609 def handle_recv(msg):
615 self.log_msg('@ attempting connect')
616 socket_client_class = PlomSocketClient
617 if self.host.startswith('ws://') or self.host.startswith('wss://'):
618 socket_client_class = WebSocketClient
620 self.socket = socket_client_class(handle_recv, self.host)
621 self.socket_thread = threading.Thread(target=self.socket.run)
622 self.socket_thread.start()
623 self.disconnected = False
624 self.game.thing_types = {}
625 self.game.terrains = {}
626 self.is_admin = False
627 time.sleep(0.1) # give potential SSL negotation some time …
628 self.socket.send('TASKS')
629 self.socket.send('TERRAINS')
630 self.socket.send('THING_TYPES')
631 self.switch_mode('login')
632 except ConnectionRefusedError:
633 self.log_msg('@ server connect failure')
634 self.disconnected = True
635 self.switch_mode('waiting_for_server')
636 self.do_refresh = True
639 self.log_msg('@ attempting reconnect')
641 # necessitated by some strange SSL race conditions with ws4py
642 time.sleep(0.1) # FIXME find out why exactly necessary
643 self.switch_mode('waiting_for_server')
648 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
649 raise BrokenSocketConnection
650 self.socket.send(msg)
651 except (BrokenPipeError, BrokenSocketConnection):
652 self.log_msg('@ server disconnected :(')
653 self.disconnected = True
654 self.force_instant_connect = True
655 self.do_refresh = True
657 def log_msg(self, msg):
659 if len(self.log) > 100:
660 self.log = self.log[-100:]
662 def restore_input_values(self):
663 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
664 self.input_ = self.game.annotations[self.explorer]
665 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
666 self.input_ = self.game.portals[self.explorer]
667 elif self.mode.name == 'password':
668 self.input_ = self.password
669 elif self.mode.name == 'name_thing':
670 if hasattr(self.game.player.carrying, 'name'):
671 self.input_ = self.game.player.carrying.name
672 elif self.mode.name == 'admin_thing_protect':
673 if hasattr(self.game.player.carrying, 'protection'):
674 self.input_ = self.game.player.carrying.protection
675 elif self.mode.name == 'enter_face':
676 start = self.ascii_draw_stage * 6
677 end = (self.ascii_draw_stage + 1) * 6
678 self.input_ = self.game.player.face[start:end]
679 elif self.mode.name == 'enter_design':
680 width = self.game.player.carrying.design[0].x
681 start = self.ascii_draw_stage * width
682 end = (self.ascii_draw_stage + 1) * width
683 self.input_ = self.game.player.carrying.design[1][start:end]
685 def send_tile_control_command(self):
686 self.send('SET_TILE_CONTROL %s %s' %
687 (self.explorer, quote(self.tile_control_char)))
689 def toggle_map_mode(self):
690 if self.map_mode == 'terrain only':
691 self.map_mode = 'terrain + annotations'
692 elif self.map_mode == 'terrain + annotations':
693 self.map_mode = 'terrain + things'
694 elif self.map_mode == 'terrain + things':
695 self.map_mode = 'protections'
696 elif self.map_mode == 'protections':
697 self.map_mode = 'terrain only'
699 def switch_mode(self, mode_name):
701 def fail(msg, return_mode='play'):
702 self.log_msg('? ' + msg)
704 self.switch_mode(return_mode)
706 if self.mode and self.mode.name == 'control_tile_draw':
707 self.log_msg('@ finished tile protection drawing.')
708 self.draw_face = False
709 self.tile_draw = False
710 self.ascii_draw_stage = 0
711 self.full_ascii_draw = ''
712 if mode_name == 'command_thing' and\
713 (not self.game.player.carrying or
714 not self.game.player.carrying.commandable):
715 return fail('not carrying anything commandable')
716 if mode_name == 'name_thing' and not self.game.player.carrying:
717 return fail('not carrying anything to re-name', 'edit')
718 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
719 return fail('not carrying anything to protect')
720 if mode_name == 'take_thing' and self.game.player.carrying:
721 return fail('already carrying something')
722 if mode_name == 'drop_thing' and not self.game.player.carrying:
723 return fail('not carrying anything droppable')
724 if mode_name == 'enter_design' and\
725 (not self.game.player.carrying or
726 not hasattr(self.game.player.carrying, 'design')):
727 return fail('not carrying designable to edit', 'edit')
728 if mode_name == 'admin_enter' and self.is_admin:
730 self.mode = getattr(self, 'mode_' + mode_name)
731 if self.mode.name in {'control_tile_draw', 'control_tile_type',
733 self.map_mode = 'protections'
734 elif self.mode.name != 'edit':
735 self.map_mode = 'terrain + things'
736 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
737 self.explorer = YX(self.game.player.position.y,
738 self.game.player.position.x)
739 if self.mode.is_single_char_entry:
740 self.show_help = True
741 if len(self.mode.intro_msg) > 0:
742 self.log_msg(self.mode.intro_msg)
743 if self.mode.name == 'login':
745 self.send('LOGIN ' + quote(self.login_name))
747 self.log_msg('@ enter username')
748 elif self.mode.name == 'take_thing':
749 self.log_msg('Portable things in reach for pick-up:')
751 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
753 if type(self.game.map_geometry) == MapGeometrySquare:
754 directed_moves['UP'] = YX(-1, 0)
755 directed_moves['DOWN'] = YX(1, 0)
756 elif type(self.game.map_geometry) == MapGeometryHex:
757 if self.game.player.position.y % 2:
758 directed_moves['UPLEFT'] = YX(-1, 0)
759 directed_moves['UPRIGHT'] = YX(-1, 1)
760 directed_moves['DOWNLEFT'] = YX(1, 0)
761 directed_moves['DOWNRIGHT'] = YX(1, 1)
763 directed_moves['UPLEFT'] = YX(-1, -1)
764 directed_moves['UPRIGHT'] = YX(-1, 0)
765 directed_moves['DOWNLEFT'] = YX(1, -1)
766 directed_moves['DOWNRIGHT'] = YX(1, 0)
768 for direction in directed_moves:
769 move = directed_moves[direction]
770 select_range[direction] = self.game.player.position + move
771 self.selectables = []
773 for direction in select_range:
774 for t in [t for t in self.game.things
775 if t.portable and t.position == select_range[direction]]:
776 self.selectables += [t.id_]
777 directions += [direction]
778 if len(self.selectables) == 0:
779 return fail('nothing to pick-up')
781 for i in range(len(self.selectables)):
782 t = self.game.get_thing(self.selectables[i])
783 self.log_msg('%s %s: %s' % (i, directions[i],
784 self.get_thing_info(t)))
785 elif self.mode.name == 'drop_thing':
786 self.log_msg('Direction to drop thing to:')
788 ['HERE'] + list(self.game.tui.movement_keys.values())
789 for i in range(len(self.selectables)):
790 self.log_msg(str(i) + ': ' + self.selectables[i])
791 elif self.mode.name == 'enter_design':
792 if self.game.player.carrying.type_ == 'Hat':
793 self.log_msg('@ The design you enter must be %s lines of max %s '
794 'characters width each'
795 % (self.game.player.carrying.design[0].y,
796 self.game.player.carrying.design[0].x))
797 self.log_msg('@ Legal characters: ' + self.game.players_hat_chars)
798 self.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)')
800 self.log_msg('@ Width of first line determines maximum width for remaining design')
801 self.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
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 if curses.can_change_color():
813 curses.init_color(7, 1000, 1000, 1000)
814 curses.init_color(0, 0, 0, 0)
815 self.do_refresh = True
817 def set_random_colors(self):
821 return int(offset + random.random()*375)
823 if curses.can_change_color():
824 curses.init_color(7, rand(625), rand(625), rand(625))
825 curses.init_color(0, rand(0), rand(0), rand(0))
826 self.do_refresh = True
830 return self.info_cached
831 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
833 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
834 info_to_cache += 'outside field of view'
836 for t in self.game.things:
837 if t.position == self.explorer:
838 info_to_cache += '%s' % self.get_thing_info(t, True)
839 terrain_char = self.game.map_content[pos_i]
841 if terrain_char in self.game.terrains:
842 terrain_desc = self.game.terrains[terrain_char]
843 info_to_cache += 'TERRAIN: %s (%s' % (terrain_char,
845 protection = self.game.map_control_content[pos_i]
846 if protection != '.':
847 info_to_cache += '/protection:%s' % protection
848 info_to_cache += ')\n'
849 if self.explorer in self.game.portals:
850 info_to_cache += 'PORTAL: ' +\
851 self.game.portals[self.explorer] + '\n'
852 if self.explorer in self.game.annotations:
853 info_to_cache += 'ANNOTATION: ' +\
854 self.game.annotations[self.explorer]
855 self.info_cached = info_to_cache
856 return self.info_cached
858 def get_thing_info(self, t, detailed=False):
862 info += self.game.thing_types[t.type_]
863 if hasattr(t, 'thing_char'):
865 if hasattr(t, 'name'):
866 info += ': %s' % t.name
867 info += ' (%s' % t.type_
868 if hasattr(t, 'installed'):
870 if t.type_ == 'Bottle':
871 if t.thing_char == '_':
873 elif t.thing_char == '~':
876 protection = t.protection
877 if protection != '.':
878 info += '/protection:%s' % protection
880 if hasattr(t, 'hat') or hasattr(t, 'face'):
881 info += '----------\n'
882 if hasattr(t, 'hat'):
883 info += '| %s |\n' % t.hat[0:6]
884 info += '| %s |\n' % t.hat[6:12]
885 info += '| %s |\n' % t.hat[12:18]
886 if hasattr(t, 'face'):
887 info += '| %s |\n' % t.face[0:6]
888 info += '| %s |\n' % t.face[6:12]
889 info += '| %s |\n' % t.face[12:18]
890 info += '----------\n'
891 if hasattr(t, 'design'):
892 line_length = t.design[0].x
894 for i in range(t.design[0].y):
895 start = i * line_length
896 end = (i + 1) * line_length
897 lines += [t.design[1][start:end]]
898 info += '-' * (line_length + 4) + '\n'
900 info += '| %s |\n' % line
901 info += '-' * (line_length + 4) + '\n'
906 def loop(self, stdscr):
909 def safe_addstr(y, x, line):
910 if y < self.size.y - 1 or x + len(line) < self.size.x:
911 stdscr.addstr(y, x, line, curses.color_pair(1))
912 else: # workaround to <https://stackoverflow.com/q/7063128>
913 cut_i = self.size.x - x - 1
915 last_char = line[cut_i]
916 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
917 stdscr.insstr(y, self.size.x - 2, ' ')
918 stdscr.addstr(y, x, cut, curses.color_pair(1))
920 def handle_input(msg):
921 command, args = self.parser.parse(msg)
924 def task_action_on(action):
925 return action_tasks[action] in self.game.tasks
927 def msg_into_lines_of_width(msg, width):
931 for i in range(len(msg)):
932 if x >= width or msg[i] == "\n":
944 def reset_screen_size():
945 self.size = YX(*stdscr.getmaxyx())
946 self.size = self.size - YX(self.size.y % 4, 0)
947 self.size = self.size - YX(0, self.size.x % 4)
948 self.left_window_width = min(52, int(self.size.x / 2))
949 self.right_window_width = self.size.x - self.left_window_width
951 def recalc_input_lines():
952 if not self.mode.has_input_prompt:
953 self.input_lines = []
955 self.input_lines = msg_into_lines_of_width(input_prompt
957 self.right_window_width)
959 def move_explorer(direction):
960 target = self.game.map_geometry.move_yx(self.explorer, direction)
962 self.info_cached = None
963 self.explorer = target
965 self.send_tile_control_command()
971 for line in self.log:
972 lines += msg_into_lines_of_width(line, self.right_window_width)
975 max_y = self.size.y - len(self.input_lines)
976 for i in range(len(lines)):
977 if (i >= max_y - height_header):
979 safe_addstr(max_y - i - 1, self.left_window_width, lines[i])
982 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
983 lines = msg_into_lines_of_width(info, self.right_window_width)
985 for i in range(len(lines)):
986 y = height_header + i
987 if y >= self.size.y - len(self.input_lines):
989 safe_addstr(y, self.left_window_width, lines[i])
992 y = self.size.y - len(self.input_lines)
993 for i in range(len(self.input_lines)):
994 safe_addstr(y, self.left_window_width, self.input_lines[i])
998 stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
999 self.game.bladder_pressure)
1000 safe_addstr(0, self.left_window_width, stats)
1003 help = "hit [%s] for help" % self.keys['help']
1004 if self.mode.has_input_prompt:
1005 help = "enter /help for help"
1006 safe_addstr(1, self.left_window_width,
1007 'MODE: %s – %s' % (self.mode.short_desc, help))
1010 if (not self.game.turn_complete) and len(self.map_lines) == 0:
1012 if self.game.turn_complete:
1013 map_lines_split = []
1014 for y in range(self.game.map_geometry.size.y):
1015 start = self.game.map_geometry.size.x * y
1016 end = start + self.game.map_geometry.size.x
1017 if self.map_mode == 'protections':
1018 map_lines_split += [[c + ' ' for c
1019 in self.game.map_control_content[start:end]]]
1021 map_lines_split += [[c + ' ' for c
1022 in self.game.map_content[start:end]]]
1023 if self.map_mode == 'terrain + annotations':
1024 for p in self.game.annotations:
1025 map_lines_split[p.y][p.x] = 'A '
1026 elif self.map_mode == 'terrain + things':
1027 for p in self.game.portals.keys():
1028 original = map_lines_split[p.y][p.x]
1029 map_lines_split[p.y][p.x] = original[0] + 'P'
1032 def draw_thing(t, used_positions):
1033 symbol = self.game.thing_types[t.type_]
1035 if hasattr(t, 'thing_char'):
1036 meta_char = t.thing_char
1037 if t.position in used_positions:
1039 if hasattr(t, 'carrying') and t.carrying:
1041 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
1042 used_positions += [t.position]
1044 for t in [t for t in self.game.things if t.type_ != 'Player']:
1045 draw_thing(t, used_positions)
1046 for t in [t for t in self.game.things if t.type_ == 'Player']:
1047 draw_thing(t, used_positions)
1048 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1049 map_lines_split[self.explorer.y][self.explorer.x] = '??'
1050 elif self.map_mode != 'terrain + things':
1051 map_lines_split[self.game.player.position.y]\
1052 [self.game.player.position.x] = '??'
1054 if type(self.game.map_geometry) == MapGeometryHex:
1056 for line in map_lines_split:
1057 self.map_lines += [indent * ' ' + ''.join(line)]
1058 indent = 0 if indent else 1
1060 for line in map_lines_split:
1061 self.map_lines += [''.join(line)]
1062 window_center = YX(int(self.size.y / 2),
1063 int(self.left_window_width / 2))
1064 center = self.game.player.position
1065 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1066 center = self.explorer
1067 center = YX(center.y, center.x * 2)
1068 self.offset = center - window_center
1069 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1070 self.offset += YX(0, 1)
1071 term_y = max(0, -self.offset.y)
1072 term_x = max(0, -self.offset.x)
1073 map_y = max(0, self.offset.y)
1074 map_x = max(0, self.offset.x)
1075 while term_y < self.size.y and map_y < len(self.map_lines):
1076 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
1077 safe_addstr(term_y, term_x, to_draw)
1081 def draw_face_popup():
1082 t = self.game.get_thing(self.draw_face)
1083 if not t or not hasattr(t, 'face'):
1084 self.draw_face = False
1087 start_x = self.left_window_width - 10
1088 def draw_body_part(body_part, end_y):
1089 safe_addstr(end_y - 3, start_x, '----------')
1090 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1091 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1092 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1094 if hasattr(t, 'face'):
1095 draw_body_part(t.face, self.size.y - 3)
1096 if hasattr(t, 'hat'):
1097 draw_body_part(t.hat, self.size.y - 6)
1098 safe_addstr(self.size.y - 2, start_x, '----------')
1101 name = name[:6] + '…'
1102 safe_addstr(self.size.y - 1, start_x,
1103 '@%s:%s' % (t.thing_char, name))
1106 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1107 self.mode.help_intro)
1108 if len(self.mode.available_actions) > 0:
1109 content += "Available actions:\n"
1110 for action in self.mode.available_actions:
1111 if action in action_tasks:
1112 if action_tasks[action] not in self.game.tasks:
1114 if action == 'move_explorer':
1116 if action == 'move':
1117 key = ','.join(self.movement_keys)
1119 key = self.keys[action]
1120 content += '[%s] – %s\n' % (key, action_descriptions[action])
1122 content += self.mode.list_available_modes(self)
1123 for i in range(self.size.y):
1125 self.left_window_width * (not self.mode.has_input_prompt),
1126 ' ' * self.left_window_width)
1128 for line in content.split('\n'):
1129 lines += msg_into_lines_of_width(line, self.right_window_width)
1130 for i in range(len(lines)):
1131 if i >= self.size.y:
1134 self.left_window_width * (not self.mode.has_input_prompt),
1139 stdscr.bkgd(' ', curses.color_pair(1))
1140 recalc_input_lines()
1141 if self.mode.has_input_prompt:
1143 if self.mode.shows_info:
1148 if not self.mode.is_intro:
1153 if self.draw_face and self.mode.name in {'chat', 'play'}:
1156 def pick_selectable(task_name):
1158 i = int(self.input_)
1159 if i < 0 or i >= len(self.selectables):
1160 self.log_msg('? invalid index, aborted')
1162 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1164 self.log_msg('? invalid index, aborted')
1166 self.switch_mode('play')
1168 def enter_ascii_art(command, height, width,
1169 with_pw=False, with_size=False):
1170 if with_size and self.ascii_draw_stage == 0:
1171 width = len(self.input_)
1173 self.log_msg('? input too long, must be max 36; try again')
1174 # TODO: move max width mechanism server-side
1176 old_size = self.game.player.carrying.design[0]
1177 if width != old_size.x:
1178 # TODO: save remaining design?
1179 self.game.player.carrying.design[1] = ''
1180 self.game.player.carrying.design[0] = YX(old_size.y, width)
1181 elif len(self.input_) > width:
1182 self.log_msg('? input too long, '
1183 'must be max %s; try again' % width)
1185 self.log_msg(' ' + self.input_)
1186 if with_size and self.input_ in {'', ' '}\
1187 and self.ascii_draw_stage > 0:
1188 height = self.ascii_draw_stage
1191 height = self.ascii_draw_stage + 2
1192 if len(self.input_) < width:
1193 self.input_ += ' ' * (width - len(self.input_))
1194 self.full_ascii_draw += self.input_
1196 old_size = self.game.player.carrying.design[0]
1197 self.game.player.carrying.design[0] = YX(height, old_size.x)
1198 self.ascii_draw_stage += 1
1199 if self.ascii_draw_stage < height:
1200 self.restore_input_values()
1202 if with_pw and with_size:
1203 self.send('%s_SIZE %s %s' % (command, YX(height, width),
1204 quote(self.password)))
1206 self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1207 quote(self.password)))
1209 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1210 self.full_ascii_draw = ""
1211 self.ascii_draw_stage = 0
1213 self.switch_mode('edit')
1215 action_descriptions = {
1217 'flatten': 'flatten surroundings',
1218 'teleport': 'teleport',
1219 'take_thing': 'pick up thing',
1220 'drop_thing': 'drop thing',
1221 'toggle_map_mode': 'toggle map view',
1222 'toggle_tile_draw': 'toggle protection character drawing',
1223 'install': '(un-)install',
1224 'wear': '(un-)wear',
1225 'door': 'open/close',
1226 'consume': 'consume',
1232 'flatten': 'FLATTEN_SURROUNDINGS',
1233 'take_thing': 'PICK_UP',
1234 'drop_thing': 'DROP',
1236 'install': 'INSTALL',
1239 'command': 'COMMAND',
1240 'consume': 'INTOXICATE',
1245 curses.curs_set(0) # hide cursor
1246 curses.start_color()
1247 self.set_default_colors()
1248 curses.init_pair(1, 7, 0)
1249 if not curses.can_change_color():
1250 self.log_msg('@ unfortunately, your terminal does not seem to '
1251 'support re-definition of colors; you might miss out '
1252 'on some color effects')
1255 self.explorer = YX(0, 0)
1257 store_widechar = False
1259 interval = datetime.timedelta(seconds=5)
1260 last_ping = datetime.datetime.now() - interval
1262 if self.disconnected and self.force_instant_connect:
1263 self.force_instant_connect = False
1265 now = datetime.datetime.now()
1266 if now - last_ping > interval:
1267 if self.disconnected:
1277 self.do_refresh = False
1280 msg = self.queue.get(block=False)
1285 key = stdscr.getkey()
1286 self.do_refresh = True
1287 except curses.error:
1292 # workaround for <https://stackoverflow.com/a/56390915>
1294 store_widechar = False
1295 key = bytes([195, keycode]).decode()
1297 store_widechar = True
1299 self.show_help = False
1300 self.draw_face = False
1301 if key == 'KEY_RESIZE':
1303 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1304 self.input_ = self.input_[:-1]
1305 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1306 or (self.mode.has_input_prompt and key == '\n'
1307 and self.input_ == ''\
1308 and self.mode.name in {'chat', 'command_thing',
1309 'take_thing', 'drop_thing',
1311 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1312 self.log_msg('@ aborted')
1313 self.switch_mode('play')
1314 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1315 self.show_help = True
1317 self.restore_input_values()
1318 elif self.mode.has_input_prompt and key != '\n': # Return key
1320 max_length = self.right_window_width * self.size.y - len(input_prompt) - 1
1321 if len(self.input_) > max_length:
1322 self.input_ = self.input_[:max_length]
1323 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1324 self.show_help = True
1325 elif self.mode.name == 'login' and key == '\n':
1326 self.login_name = self.input_
1327 self.send('LOGIN ' + quote(self.input_))
1329 elif self.mode.name == 'enter_face' and key == '\n':
1330 enter_ascii_art('PLAYER_FACE', 3, 6)
1331 elif self.mode.name == 'enter_design' and key == '\n':
1332 if self.game.player.carrying.type_ == 'Hat':
1333 enter_ascii_art('THING_DESIGN',
1334 self.game.player.carrying.design[0].y,
1335 self.game.player.carrying.design[0].x, True)
1337 enter_ascii_art('THING_DESIGN',
1338 self.game.player.carrying.design[0].y,
1339 self.game.player.carrying.design[0].x,
1341 elif self.mode.name == 'take_thing' and key == '\n':
1342 pick_selectable('PICK_UP')
1343 elif self.mode.name == 'drop_thing' and key == '\n':
1344 pick_selectable('DROP')
1345 elif self.mode.name == 'command_thing' and key == '\n':
1346 self.send('TASK:COMMAND ' + quote(self.input_))
1348 elif self.mode.name == 'control_pw_pw' and key == '\n':
1349 if self.input_ == '':
1350 self.log_msg('@ aborted')
1352 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1353 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1354 self.switch_mode('admin')
1355 elif self.mode.name == 'password' and key == '\n':
1356 if self.input_ == '':
1358 self.password = self.input_
1359 self.switch_mode('edit')
1360 elif self.mode.name == 'admin_enter' and key == '\n':
1361 self.send('BECOME_ADMIN ' + quote(self.input_))
1362 self.switch_mode('play')
1363 elif self.mode.name == 'control_pw_type' and key == '\n':
1364 if len(self.input_) != 1:
1365 self.log_msg('@ entered non-single-char, therefore aborted')
1366 self.switch_mode('admin')
1368 self.tile_control_char = self.input_
1369 self.switch_mode('control_pw_pw')
1370 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1371 if len(self.input_) != 1:
1372 self.log_msg('@ entered non-single-char, therefore aborted')
1374 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1375 self.log_msg('@ sent new protection character for thing')
1376 self.switch_mode('admin')
1377 elif self.mode.name == 'control_tile_type' and key == '\n':
1378 if len(self.input_) != 1:
1379 self.log_msg('@ entered non-single-char, therefore aborted')
1380 self.switch_mode('admin')
1382 self.tile_control_char = self.input_
1383 self.switch_mode('control_tile_draw')
1384 elif self.mode.name == 'chat' and key == '\n':
1385 if self.input_ == '':
1387 if self.input_[0] == '/':
1388 if self.input_.startswith('/nick'):
1389 tokens = self.input_.split(maxsplit=1)
1390 if len(tokens) == 2:
1391 self.send('NICK ' + quote(tokens[1]))
1393 self.log_msg('? need login name')
1395 self.log_msg('? unknown command')
1397 self.send('ALL ' + quote(self.input_))
1399 elif self.mode.name == 'name_thing' and key == '\n':
1400 if self.input_ == '':
1402 self.send('THING_NAME %s %s' % (quote(self.input_),
1403 quote(self.password)))
1404 self.switch_mode('edit')
1405 elif self.mode.name == 'annotate' and key == '\n':
1406 if self.input_ == '':
1408 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1409 quote(self.password)))
1410 self.switch_mode('edit')
1411 elif self.mode.name == 'portal' and key == '\n':
1412 if self.input_ == '':
1414 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1415 quote(self.password)))
1416 self.switch_mode('edit')
1417 elif self.mode.name == 'study':
1418 if self.mode.mode_switch_on_key(self, key):
1420 elif key == self.keys['toggle_map_mode']:
1421 self.toggle_map_mode()
1422 elif key in self.movement_keys:
1423 move_explorer(self.movement_keys[key])
1424 elif self.mode.name == 'play':
1425 if self.mode.mode_switch_on_key(self, key):
1427 elif key == self.keys['door'] and task_action_on('door'):
1428 self.send('TASK:DOOR')
1429 elif key == self.keys['consume'] and task_action_on('consume'):
1430 self.send('TASK:INTOXICATE')
1431 elif key == self.keys['wear'] and task_action_on('wear'):
1432 self.send('TASK:WEAR')
1433 elif key == self.keys['spin'] and task_action_on('spin'):
1434 self.send('TASK:SPIN')
1435 elif key == self.keys['dance'] and task_action_on('dance'):
1436 self.send('TASK:DANCE')
1437 elif key == self.keys['teleport']:
1438 if self.game.player.position in self.game.portals:
1439 self.host = self.game.portals[self.game.player.position]
1443 self.log_msg('? not standing on portal')
1444 elif key in self.movement_keys and task_action_on('move'):
1445 self.send('TASK:MOVE ' + self.movement_keys[key])
1446 elif self.mode.name == 'write':
1447 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1448 self.switch_mode('edit')
1449 elif self.mode.name == 'control_tile_draw':
1450 if self.mode.mode_switch_on_key(self, key):
1452 elif key in self.movement_keys:
1453 move_explorer(self.movement_keys[key])
1454 elif key == self.keys['toggle_tile_draw']:
1455 self.tile_draw = False if self.tile_draw else True
1456 elif self.mode.name == 'admin':
1457 if self.mode.mode_switch_on_key(self, key):
1459 elif key == self.keys['toggle_map_mode']:
1460 self.toggle_map_mode()
1461 elif key in self.movement_keys and task_action_on('move'):
1462 self.send('TASK:MOVE ' + self.movement_keys[key])
1463 elif self.mode.name == 'edit':
1464 if self.mode.mode_switch_on_key(self, key):
1466 elif key == self.keys['flatten'] and task_action_on('flatten'):
1467 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1468 elif key == self.keys['install'] and task_action_on('install'):
1469 self.send('TASK:INSTALL %s' % quote(self.password))
1470 elif key == self.keys['toggle_map_mode']:
1471 self.toggle_map_mode()
1472 elif key in self.movement_keys and task_action_on('move'):
1473 self.send('TASK:MOVE ' + self.movement_keys[key])
1475 if len(sys.argv) != 2:
1476 raise ArgError('wrong number of arguments, need game host')