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 cmd_LOGIN_OK.argtypes = ''
192 def cmd_ADMIN_OK(game):
193 game.tui.is_admin = True
194 game.tui.log_msg('@ you now have admin rights')
195 game.tui.switch_mode('admin')
196 game.tui.do_refresh = True
197 cmd_ADMIN_OK.argtypes = ''
199 def cmd_REPLY(game, msg):
200 game.tui.log_msg('#MUSICPLAYER: ' + msg)
201 game.tui.do_refresh = True
202 cmd_REPLY.argtypes = 'string'
204 def cmd_CHAT(game, msg):
205 game.tui.log_msg('# ' + msg)
206 game.tui.do_refresh = True
207 cmd_CHAT.argtypes = 'string'
209 def cmd_CHATFACE(game, thing_id):
210 game.tui.draw_face = thing_id
211 game.tui.do_refresh = True
212 cmd_CHATFACE.argtypes = 'int:pos'
214 def cmd_PLAYER_ID(game, player_id):
215 game.player_id = player_id
216 cmd_PLAYER_ID.argtypes = 'int:nonneg'
218 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
219 game.players_hat_chars_new = hat_chars
220 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
222 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
223 t = game.get_thing_temp(thing_id)
225 t = ThingBase(game, thing_id)
226 game.things_new += [t]
229 t.protection = protection
230 t.portable = portable
231 t.commandable = commandable
232 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
234 def cmd_THING_NAME(game, thing_id, name):
235 t = game.get_thing_temp(thing_id)
237 cmd_THING_NAME.argtypes = 'int:pos string'
239 def cmd_THING_FACE(game, thing_id, face):
240 t = game.get_thing_temp(thing_id)
242 cmd_THING_FACE.argtypes = 'int:pos string'
244 def cmd_THING_HAT(game, thing_id, hat):
245 t = game.get_thing_temp(thing_id)
247 cmd_THING_HAT.argtypes = 'int:pos string'
249 def cmd_THING_DESIGN(game, thing_id, size, design):
250 t = game.get_thing_temp(thing_id)
251 t.design = [size, design]
252 cmd_THING_DESIGN.argtypes = 'int:pos yx_tuple string'
254 def cmd_THING_CHAR(game, thing_id, c):
255 t = game.get_thing_temp(thing_id)
257 cmd_THING_CHAR.argtypes = 'int:pos char'
259 def cmd_MAP(game, geometry, size, content):
260 map_geometry_class = globals()['MapGeometry' + geometry]
261 game.map_geometry_new = map_geometry_class(size)
262 game.map_content_new = content
263 if type(game.map_geometry_new) == MapGeometrySquare:
264 game.tui.movement_keys = {
265 game.tui.keys['square_move_up']: 'UP',
266 game.tui.keys['square_move_left']: 'LEFT',
267 game.tui.keys['square_move_down']: 'DOWN',
268 game.tui.keys['square_move_right']: 'RIGHT',
270 elif type(game.map_geometry_new) == MapGeometryHex:
271 game.tui.movement_keys = {
272 game.tui.keys['hex_move_upleft']: 'UPLEFT',
273 game.tui.keys['hex_move_upright']: 'UPRIGHT',
274 game.tui.keys['hex_move_right']: 'RIGHT',
275 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
276 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
277 game.tui.keys['hex_move_left']: 'LEFT',
279 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
281 def cmd_FOV(game, content):
282 game.fov_new = content
283 cmd_FOV.argtypes = 'string'
285 def cmd_MAP_CONTROL(game, content):
286 game.map_control_content_new = content
287 cmd_MAP_CONTROL.argtypes = 'string'
289 def cmd_GAME_STATE_COMPLETE(game):
290 game.tui.do_refresh = True
291 game.tui.info_cached = None
292 game.things = game.things_new
293 game.portals = game.portals_new
294 game.annotations = game.annotations_new
295 game.fov = game.fov_new
296 game.map_geometry = game.map_geometry_new
297 game.map_content = game.map_content_new
298 game.map_control_content = game.map_control_content_new
299 game.player = game.get_thing(game.player_id)
300 game.players_hat_chars = game.players_hat_chars_new
301 game.bladder_pressure = game.bladder_pressure_new
302 game.energy = game.energy_new
303 game.turn_complete = True
304 if game.tui.mode.name == 'post_login_wait':
305 game.tui.switch_mode('play')
306 cmd_GAME_STATE_COMPLETE.argtypes = ''
308 def cmd_PORTAL(game, position, msg):
309 game.portals_new[position] = msg
310 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
312 def cmd_PLAY_ERROR(game, msg):
313 game.tui.log_msg('? ' + msg)
314 game.tui.flash = True
315 game.tui.do_refresh = True
316 cmd_PLAY_ERROR.argtypes = 'string'
318 def cmd_GAME_ERROR(game, msg):
319 game.tui.log_msg('? game error: ' + msg)
320 game.tui.do_refresh = True
321 cmd_GAME_ERROR.argtypes = 'string'
323 def cmd_ARGUMENT_ERROR(game, msg):
324 game.tui.log_msg('? syntax error: ' + msg)
325 game.tui.do_refresh = True
326 cmd_ARGUMENT_ERROR.argtypes = 'string'
328 def cmd_ANNOTATION(game, position, msg):
329 game.annotations_new[position] = msg
330 if game.tui.mode.shows_info:
331 game.tui.do_refresh = True
332 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
334 def cmd_TASKS(game, tasks_comma_separated):
335 game.tasks = tasks_comma_separated.split(',')
336 game.tui.mode_write.legal = 'WRITE' in game.tasks
337 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
338 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
339 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
340 cmd_TASKS.argtypes = 'string'
342 def cmd_THING_TYPE(game, thing_type, symbol_hint):
343 game.thing_types[thing_type] = symbol_hint
344 cmd_THING_TYPE.argtypes = 'string char'
346 def cmd_THING_INSTALLED(game, thing_id):
347 game.get_thing_temp(thing_id).installed = True
348 cmd_THING_INSTALLED.argtypes = 'int:pos'
350 def cmd_THING_CARRYING(game, thing_id, carried_id):
351 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
352 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
354 def cmd_TERRAIN(game, terrain_char, terrain_desc):
355 game.terrains[terrain_char] = terrain_desc
356 cmd_TERRAIN.argtypes = 'char string'
360 cmd_PONG.argtypes = ''
362 def cmd_DEFAULT_COLORS(game):
363 game.tui.set_default_colors()
364 cmd_DEFAULT_COLORS.argtypes = ''
366 def cmd_RANDOM_COLORS(game):
367 game.tui.set_random_colors()
368 cmd_RANDOM_COLORS.argtypes = ''
370 def cmd_STATS(game, bladder_pressure, energy):
371 game.bladder_pressure_new = bladder_pressure
372 game.energy_new = energy
373 cmd_STATS.argtypes = 'int:nonneg int'
375 class Game(GameBase):
376 turn_complete = False
381 def __init__(self, *args, **kwargs):
382 super().__init__(*args, **kwargs)
383 self.register_command(cmd_LOGIN_OK)
384 self.register_command(cmd_ADMIN_OK)
385 self.register_command(cmd_PONG)
386 self.register_command(cmd_CHAT)
387 self.register_command(cmd_CHATFACE)
388 self.register_command(cmd_REPLY)
389 self.register_command(cmd_PLAYER_ID)
390 self.register_command(cmd_TURN)
391 self.register_command(cmd_OTHER_WIPE)
392 self.register_command(cmd_THING)
393 self.register_command(cmd_THING_TYPE)
394 self.register_command(cmd_THING_NAME)
395 self.register_command(cmd_THING_CHAR)
396 self.register_command(cmd_THING_FACE)
397 self.register_command(cmd_THING_HAT)
398 self.register_command(cmd_THING_DESIGN)
399 self.register_command(cmd_THING_CARRYING)
400 self.register_command(cmd_THING_INSTALLED)
401 self.register_command(cmd_TERRAIN)
402 self.register_command(cmd_MAP)
403 self.register_command(cmd_MAP_CONTROL)
404 self.register_command(cmd_PORTAL)
405 self.register_command(cmd_ANNOTATION)
406 self.register_command(cmd_GAME_STATE_COMPLETE)
407 self.register_command(cmd_PLAYERS_HAT_CHARS)
408 self.register_command(cmd_ARGUMENT_ERROR)
409 self.register_command(cmd_GAME_ERROR)
410 self.register_command(cmd_PLAY_ERROR)
411 self.register_command(cmd_TASKS)
412 self.register_command(cmd_FOV)
413 self.register_command(cmd_DEFAULT_COLORS)
414 self.register_command(cmd_RANDOM_COLORS)
415 self.register_command(cmd_STATS)
416 self.map_content = ''
417 self.players_hat_chars = ''
419 self.annotations = {}
420 self.annotations_new = {}
422 self.portals_new = {}
426 def get_string_options(self, string_option_type):
427 if string_option_type == 'map_geometry':
428 return ['Hex', 'Square']
429 elif string_option_type == 'thing_type':
430 return self.thing_types.keys()
433 def get_command(self, command_name):
434 from functools import partial
435 f = partial(self.commands[command_name], self)
436 f.argtypes = self.commands[command_name].argtypes
439 def get_thing_temp(self, id_):
440 for thing in self.things_new:
447 def __init__(self, name, has_input_prompt=False, shows_info=False,
448 is_intro=False, is_single_char_entry=False):
450 self.short_desc = mode_helps[name]['short']
451 self.available_modes = []
452 self.available_actions = []
453 self.has_input_prompt = has_input_prompt
454 self.shows_info = shows_info
455 self.is_intro = is_intro
456 self.help_intro = mode_helps[name]['long']
457 self.intro_msg = mode_helps[name]['intro']
458 self.is_single_char_entry = is_single_char_entry
461 def iter_available_modes(self, tui):
462 for mode_name in self.available_modes:
463 mode = getattr(tui, 'mode_' + mode_name)
466 key = tui.keys['switch_to_' + mode.name]
469 def list_available_modes(self, tui):
471 if len(self.available_modes) > 0:
472 msg = 'Other modes available from here:\n'
473 for mode, key in self.iter_available_modes(tui):
474 msg += '[%s] – %s\n' % (key, mode.short_desc)
477 def mode_switch_on_key(self, tui, key_pressed):
478 for mode, key in self.iter_available_modes(tui):
479 if key_pressed == key:
480 tui.switch_mode(mode.name)
485 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
486 mode_admin = Mode('admin')
487 mode_play = Mode('play')
488 mode_study = Mode('study', shows_info=True)
489 mode_write = Mode('write', is_single_char_entry=True)
490 mode_edit = Mode('edit')
491 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
492 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
493 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
494 mode_control_tile_draw = Mode('control_tile_draw')
495 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
496 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
497 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
498 mode_chat = Mode('chat', has_input_prompt=True)
499 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
500 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
501 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
502 mode_password = Mode('password', has_input_prompt=True)
503 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
504 mode_command_thing = Mode('command_thing', has_input_prompt=True)
505 mode_take_thing = Mode('take_thing', has_input_prompt=True)
506 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
507 mode_enter_face = Mode('enter_face', has_input_prompt=True)
508 mode_enter_design = Mode('enter_design', has_input_prompt=True)
512 def __init__(self, host):
515 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
516 "command_thing", "take_thing",
518 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
519 "install", "wear", "spin", "dance"]
520 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
521 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
522 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
523 "control_tile_type", "chat",
524 "study", "play", "edit"]
525 self.mode_admin.available_actions = ["move", "toggle_map_mode"]
526 self.mode_control_tile_draw.available_modes = ["admin_enter"]
527 self.mode_control_tile_draw.available_actions = ["move_explorer",
529 self.mode_edit.available_modes = ["write", "annotate", "portal",
530 "name_thing", "enter_face", "enter_design",
532 "chat", "study", "play", "admin_enter"]
533 self.mode_edit.available_actions = ["move", "flatten", "install",
539 self.parser = Parser(self.game)
541 self.do_refresh = True
542 self.queue = queue.Queue()
543 self.login_name = None
544 self.map_mode = 'terrain + things'
545 self.password = 'foo'
546 self.switch_mode('waiting_for_server')
548 'switch_to_chat': 't',
549 'switch_to_play': 'p',
550 'switch_to_password': 'P',
551 'switch_to_annotate': 'M',
552 'switch_to_portal': 'T',
553 'switch_to_study': '?',
554 'switch_to_edit': 'E',
555 'switch_to_write': 'm',
556 'switch_to_name_thing': 'N',
557 'switch_to_command_thing': 'O',
558 'switch_to_admin_enter': 'A',
559 'switch_to_control_pw_type': 'C',
560 'switch_to_control_tile_type': 'Q',
561 'switch_to_admin_thing_protect': 'T',
563 'switch_to_enter_face': 'f',
564 'switch_to_enter_design': 'D',
565 'switch_to_take_thing': 'z',
566 'switch_to_drop_thing': 'u',
575 'toggle_map_mode': 'L',
576 'toggle_tile_draw': 'm',
577 'hex_move_upleft': 'w',
578 'hex_move_upright': 'e',
579 'hex_move_right': 'd',
580 'hex_move_downright': 'x',
581 'hex_move_downleft': 'y',
582 'hex_move_left': 'a',
583 'square_move_up': 'w',
584 'square_move_left': 'a',
585 'square_move_down': 's',
586 'square_move_right': 'd',
588 if os.path.isfile('config.json'):
589 with open('config.json', 'r') as f:
590 keys_conf = json.loads(f.read())
592 self.keys[k] = keys_conf[k]
593 self.show_help = False
594 self.disconnected = True
595 self.force_instant_connect = True
596 self.input_lines = []
600 self.ascii_draw_stage = 0
601 self.full_ascii_draw = ''
602 self.offset = YX(0,0)
603 curses.wrapper(self.loop)
607 def handle_recv(msg):
613 self.log_msg('@ attempting connect')
614 socket_client_class = PlomSocketClient
615 if self.host.startswith('ws://') or self.host.startswith('wss://'):
616 socket_client_class = WebSocketClient
618 self.socket = socket_client_class(handle_recv, self.host)
619 self.socket_thread = threading.Thread(target=self.socket.run)
620 self.socket_thread.start()
621 self.disconnected = False
622 self.game.thing_types = {}
623 self.game.terrains = {}
624 self.is_admin = False
625 time.sleep(0.1) # give potential SSL negotation some time …
626 self.socket.send('TASKS')
627 self.socket.send('TERRAINS')
628 self.socket.send('THING_TYPES')
629 self.switch_mode('login')
630 except ConnectionRefusedError:
631 self.log_msg('@ server connect failure')
632 self.disconnected = True
633 self.switch_mode('waiting_for_server')
634 self.do_refresh = True
637 self.log_msg('@ attempting reconnect')
639 # necessitated by some strange SSL race conditions with ws4py
640 time.sleep(0.1) # FIXME find out why exactly necessary
641 self.switch_mode('waiting_for_server')
646 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
647 raise BrokenSocketConnection
648 self.socket.send(msg)
649 except (BrokenPipeError, BrokenSocketConnection):
650 self.log_msg('@ server disconnected :(')
651 self.disconnected = True
652 self.force_instant_connect = True
653 self.do_refresh = True
655 def log_msg(self, msg):
657 if len(self.log) > 100:
658 self.log = self.log[-100:]
660 def restore_input_values(self):
661 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
662 self.input_ = self.game.annotations[self.explorer]
663 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
664 self.input_ = self.game.portals[self.explorer]
665 elif self.mode.name == 'password':
666 self.input_ = self.password
667 elif self.mode.name == 'name_thing':
668 if hasattr(self.game.player.carrying, 'name'):
669 self.input_ = self.game.player.carrying.name
670 elif self.mode.name == 'admin_thing_protect':
671 if hasattr(self.game.player.carrying, 'protection'):
672 self.input_ = self.game.player.carrying.protection
673 elif self.mode.name == 'enter_face':
674 start = self.ascii_draw_stage * 6
675 end = (self.ascii_draw_stage + 1) * 6
676 self.input_ = self.game.player.face[start:end]
677 elif self.mode.name == 'enter_design':
678 width = self.game.player.carrying.design[0].x
679 start = self.ascii_draw_stage * width
680 end = (self.ascii_draw_stage + 1) * width
681 self.input_ = self.game.player.carrying.design[1][start:end]
683 def send_tile_control_command(self):
684 self.send('SET_TILE_CONTROL %s %s' %
685 (self.explorer, quote(self.tile_control_char)))
687 def toggle_map_mode(self):
688 if self.map_mode == 'terrain only':
689 self.map_mode = 'terrain + annotations'
690 elif self.map_mode == 'terrain + annotations':
691 self.map_mode = 'terrain + things'
692 elif self.map_mode == 'terrain + things':
693 self.map_mode = 'protections'
694 elif self.map_mode == 'protections':
695 self.map_mode = 'terrain only'
697 def switch_mode(self, mode_name):
699 def fail(msg, return_mode='play'):
700 self.log_msg('? ' + msg)
702 self.switch_mode(return_mode)
704 if self.mode and self.mode.name == 'control_tile_draw':
705 self.log_msg('@ finished tile protection drawing.')
706 self.draw_face = False
707 self.tile_draw = False
708 self.ascii_draw_stage = 0
709 self.full_ascii_draw = ''
710 if mode_name == 'command_thing' and\
711 (not self.game.player.carrying or
712 not self.game.player.carrying.commandable):
713 return fail('not carrying anything commandable')
714 if mode_name == 'name_thing' and not self.game.player.carrying:
715 return fail('not carrying anything to re-name', 'edit')
716 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
717 return fail('not carrying anything to protect')
718 if mode_name == 'take_thing' and self.game.player.carrying:
719 return fail('already carrying something')
720 if mode_name == 'drop_thing' and not self.game.player.carrying:
721 return fail('not carrying anything droppable')
722 if mode_name == 'enter_design' and\
723 (not self.game.player.carrying or
724 not hasattr(self.game.player.carrying, 'design')):
725 return fail('not carrying designable to edit', 'edit')
726 if mode_name == 'admin_enter' and self.is_admin:
728 self.mode = getattr(self, 'mode_' + mode_name)
729 if self.mode.name in {'control_tile_draw', 'control_tile_type',
731 self.map_mode = 'protections'
732 elif self.mode.name != 'edit':
733 self.map_mode = 'terrain + things'
734 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
735 self.explorer = YX(self.game.player.position.y,
736 self.game.player.position.x)
737 if self.mode.is_single_char_entry:
738 self.show_help = True
739 if len(self.mode.intro_msg) > 0:
740 self.log_msg(self.mode.intro_msg)
741 if self.mode.name == 'login':
743 self.send('LOGIN ' + quote(self.login_name))
745 self.log_msg('@ enter username')
746 elif self.mode.name == 'take_thing':
747 self.log_msg('Portable things in reach for pick-up:')
749 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
751 if type(self.game.map_geometry) == MapGeometrySquare:
752 directed_moves['UP'] = YX(-1, 0)
753 directed_moves['DOWN'] = YX(1, 0)
754 elif type(self.game.map_geometry) == MapGeometryHex:
755 if self.game.player.position.y % 2:
756 directed_moves['UPLEFT'] = YX(-1, 0)
757 directed_moves['UPRIGHT'] = YX(-1, 1)
758 directed_moves['DOWNLEFT'] = YX(1, 0)
759 directed_moves['DOWNRIGHT'] = YX(1, 1)
761 directed_moves['UPLEFT'] = YX(-1, -1)
762 directed_moves['UPRIGHT'] = YX(-1, 0)
763 directed_moves['DOWNLEFT'] = YX(1, -1)
764 directed_moves['DOWNRIGHT'] = YX(1, 0)
766 for direction in directed_moves:
767 move = directed_moves[direction]
768 select_range[direction] = self.game.player.position + move
769 self.selectables = []
771 for direction in select_range:
772 for t in [t for t in self.game.things
773 if t.portable and t.position == select_range[direction]]:
774 self.selectables += [t.id_]
775 directions += [direction]
776 if len(self.selectables) == 0:
777 return fail('nothing to pick-up')
779 for i in range(len(self.selectables)):
780 t = self.game.get_thing(self.selectables[i])
781 self.log_msg('%s %s: %s' % (i, directions[i],
782 self.get_thing_info(t)))
783 elif self.mode.name == 'drop_thing':
784 self.log_msg('Direction to drop thing to:')
786 ['HERE'] + list(self.game.tui.movement_keys.values())
787 for i in range(len(self.selectables)):
788 self.log_msg(str(i) + ': ' + self.selectables[i])
789 elif self.mode.name == 'enter_design':
790 if self.game.player.carrying.type_ == 'Hat':
791 self.log_msg('@ The design you enter must be %s lines of max %s '
792 'characters width each'
793 % (self.game.player.carrying.design[0].y,
794 self.game.player.carrying.design[0].x))
795 self.log_msg('@ Legal characters: ' + self.game.players_hat_chars)
796 self.log_msg('@ (Eat cookies to extend the ASCII characters available for drawing.)')
798 self.log_msg('@ Width of first line determines maximum width for remaining design')
799 self.log_msg('@ Finish design by entering an empty line (multiple space characters do not count as empty)')
800 elif self.mode.name == 'command_thing':
801 self.send('TASK:COMMAND ' + quote('HELP'))
802 elif self.mode.name == 'control_pw_pw':
803 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
804 elif self.mode.name == 'control_tile_draw':
805 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']))
807 self.restore_input_values()
809 def set_default_colors(self):
810 if curses.can_change_color():
811 curses.init_color(7, 1000, 1000, 1000)
812 curses.init_color(0, 0, 0, 0)
813 self.do_refresh = True
815 def set_random_colors(self):
819 return int(offset + random.random()*375)
821 if curses.can_change_color():
822 curses.init_color(7, rand(625), rand(625), rand(625))
823 curses.init_color(0, 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 += '%s' % self.get_thing_info(t, True)
837 terrain_char = self.game.map_content[pos_i]
839 if terrain_char in self.game.terrains:
840 terrain_desc = self.game.terrains[terrain_char]
841 info_to_cache += 'TERRAIN: %s (%s' % (terrain_char,
843 protection = self.game.map_control_content[pos_i]
844 if protection != '.':
845 info_to_cache += '/protection:%s' % protection
846 info_to_cache += ')\n'
847 if self.explorer in self.game.portals:
848 info_to_cache += 'PORTAL: ' +\
849 self.game.portals[self.explorer] + '\n'
850 if self.explorer in self.game.annotations:
851 info_to_cache += 'ANNOTATION: ' +\
852 self.game.annotations[self.explorer]
853 self.info_cached = info_to_cache
854 return self.info_cached
856 def get_thing_info(self, t, detailed=False):
860 info += self.game.thing_types[t.type_]
861 if hasattr(t, 'thing_char'):
863 if hasattr(t, 'name'):
864 info += ': %s' % t.name
865 info += ' (%s' % t.type_
866 if hasattr(t, 'installed'):
868 if t.type_ == 'Bottle':
869 if t.thing_char == '_':
871 elif t.thing_char == '~':
874 protection = t.protection
875 if protection != '.':
876 info += '/protection:%s' % protection
878 if hasattr(t, 'hat') or hasattr(t, 'face'):
879 info += '----------\n'
880 if hasattr(t, 'hat'):
881 info += '| %s |\n' % t.hat[0:6]
882 info += '| %s |\n' % t.hat[6:12]
883 info += '| %s |\n' % t.hat[12:18]
884 if hasattr(t, 'face'):
885 info += '| %s |\n' % t.face[0:6]
886 info += '| %s |\n' % t.face[6:12]
887 info += '| %s |\n' % t.face[12:18]
888 info += '----------\n'
889 if hasattr(t, 'design'):
890 line_length = t.design[0].x
892 for i in range(t.design[0].y):
893 start = i * line_length
894 end = (i + 1) * line_length
895 lines += [t.design[1][start:end]]
896 info += '-' * (line_length + 4) + '\n'
898 info += '| %s |\n' % line
899 info += '-' * (line_length + 4) + '\n'
904 def loop(self, stdscr):
907 def safe_addstr(y, x, line):
908 if y < self.size.y - 1 or x + len(line) < self.size.x:
909 stdscr.addstr(y, x, line, curses.color_pair(1))
910 else: # workaround to <https://stackoverflow.com/q/7063128>
911 cut_i = self.size.x - x - 1
913 last_char = line[cut_i]
914 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
915 stdscr.insstr(y, self.size.x - 2, ' ')
916 stdscr.addstr(y, x, cut, curses.color_pair(1))
918 def handle_input(msg):
919 command, args = self.parser.parse(msg)
922 def task_action_on(action):
923 return action_tasks[action] in self.game.tasks
925 def msg_into_lines_of_width(msg, width):
929 for i in range(len(msg)):
930 if x >= width or msg[i] == "\n":
942 def reset_screen_size():
943 self.size = YX(*stdscr.getmaxyx())
944 self.size = self.size - YX(self.size.y % 4, 0)
945 self.size = self.size - YX(0, self.size.x % 4)
946 self.left_window_width = min(52, int(self.size.x / 2))
947 self.right_window_width = self.size.x - self.left_window_width
949 def recalc_input_lines():
950 if not self.mode.has_input_prompt:
951 self.input_lines = []
953 self.input_lines = msg_into_lines_of_width(input_prompt
955 self.right_window_width)
957 def move_explorer(direction):
958 target = self.game.map_geometry.move_yx(self.explorer, direction)
960 self.info_cached = None
961 self.explorer = target
963 self.send_tile_control_command()
969 for line in self.log:
970 lines += msg_into_lines_of_width(line, self.right_window_width)
973 max_y = self.size.y - len(self.input_lines)
974 for i in range(len(lines)):
975 if (i >= max_y - height_header):
977 safe_addstr(max_y - i - 1, self.left_window_width, lines[i])
980 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
981 lines = msg_into_lines_of_width(info, self.right_window_width)
983 for i in range(len(lines)):
984 y = height_header + i
985 if y >= self.size.y - len(self.input_lines):
987 safe_addstr(y, self.left_window_width, lines[i])
990 y = self.size.y - len(self.input_lines)
991 for i in range(len(self.input_lines)):
992 safe_addstr(y, self.left_window_width, self.input_lines[i])
996 stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
997 self.game.bladder_pressure)
998 safe_addstr(0, self.left_window_width, stats)
1001 help = "hit [%s] for help" % self.keys['help']
1002 if self.mode.has_input_prompt:
1003 help = "enter /help for help"
1004 safe_addstr(1, self.left_window_width,
1005 'MODE: %s – %s' % (self.mode.short_desc, help))
1008 if (not self.game.turn_complete) and len(self.map_lines) == 0:
1010 if self.game.turn_complete:
1011 map_lines_split = []
1012 for y in range(self.game.map_geometry.size.y):
1013 start = self.game.map_geometry.size.x * y
1014 end = start + self.game.map_geometry.size.x
1015 if self.map_mode == 'protections':
1016 map_lines_split += [[c + ' ' for c
1017 in self.game.map_control_content[start:end]]]
1019 map_lines_split += [[c + ' ' for c
1020 in self.game.map_content[start:end]]]
1021 if self.map_mode == 'terrain + annotations':
1022 for p in self.game.annotations:
1023 map_lines_split[p.y][p.x] = 'A '
1024 elif self.map_mode == 'terrain + things':
1025 for p in self.game.portals.keys():
1026 original = map_lines_split[p.y][p.x]
1027 map_lines_split[p.y][p.x] = original[0] + 'P'
1030 def draw_thing(t, used_positions):
1031 symbol = self.game.thing_types[t.type_]
1033 if hasattr(t, 'thing_char'):
1034 meta_char = t.thing_char
1035 if t.position in used_positions:
1037 if hasattr(t, 'carrying') and t.carrying:
1039 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
1040 used_positions += [t.position]
1042 for t in [t for t in self.game.things if t.type_ != 'Player']:
1043 draw_thing(t, used_positions)
1044 for t in [t for t in self.game.things if t.type_ == 'Player']:
1045 draw_thing(t, used_positions)
1046 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1047 map_lines_split[self.explorer.y][self.explorer.x] = '??'
1048 elif self.map_mode != 'terrain + things':
1049 map_lines_split[self.game.player.position.y]\
1050 [self.game.player.position.x] = '??'
1052 if type(self.game.map_geometry) == MapGeometryHex:
1054 for line in map_lines_split:
1055 self.map_lines += [indent * ' ' + ''.join(line)]
1056 indent = 0 if indent else 1
1058 for line in map_lines_split:
1059 self.map_lines += [''.join(line)]
1060 window_center = YX(int(self.size.y / 2),
1061 int(self.left_window_width / 2))
1062 center = self.game.player.position
1063 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1064 center = self.explorer
1065 center = YX(center.y, center.x * 2)
1066 self.offset = center - window_center
1067 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1068 self.offset += YX(0, 1)
1069 term_y = max(0, -self.offset.y)
1070 term_x = max(0, -self.offset.x)
1071 map_y = max(0, self.offset.y)
1072 map_x = max(0, self.offset.x)
1073 while term_y < self.size.y and map_y < len(self.map_lines):
1074 to_draw = self.map_lines[map_y][map_x:self.left_window_width + self.offset.x]
1075 safe_addstr(term_y, term_x, to_draw)
1079 def draw_face_popup():
1080 t = self.game.get_thing(self.draw_face)
1081 if not t or not hasattr(t, 'face'):
1082 self.draw_face = False
1085 start_x = self.left_window_width - 10
1086 def draw_body_part(body_part, end_y):
1087 safe_addstr(end_y - 3, start_x, '----------')
1088 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1089 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1090 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1092 if hasattr(t, 'face'):
1093 draw_body_part(t.face, self.size.y - 3)
1094 if hasattr(t, 'hat'):
1095 draw_body_part(t.hat, self.size.y - 6)
1096 safe_addstr(self.size.y - 2, start_x, '----------')
1099 name = name[:6] + '…'
1100 safe_addstr(self.size.y - 1, start_x,
1101 '@%s:%s' % (t.thing_char, name))
1104 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1105 self.mode.help_intro)
1106 if len(self.mode.available_actions) > 0:
1107 content += "Available actions:\n"
1108 for action in self.mode.available_actions:
1109 if action in action_tasks:
1110 if action_tasks[action] not in self.game.tasks:
1112 if action == 'move_explorer':
1114 if action == 'move':
1115 key = ','.join(self.movement_keys)
1117 key = self.keys[action]
1118 content += '[%s] – %s\n' % (key, action_descriptions[action])
1120 content += self.mode.list_available_modes(self)
1121 for i in range(self.size.y):
1123 self.left_window_width * (not self.mode.has_input_prompt),
1124 ' ' * self.left_window_width)
1126 for line in content.split('\n'):
1127 lines += msg_into_lines_of_width(line, self.right_window_width)
1128 for i in range(len(lines)):
1129 if i >= self.size.y:
1132 self.left_window_width * (not self.mode.has_input_prompt),
1137 stdscr.bkgd(' ', curses.color_pair(1))
1138 recalc_input_lines()
1139 if self.mode.has_input_prompt:
1141 if self.mode.shows_info:
1146 if not self.mode.is_intro:
1151 if self.draw_face and self.mode.name in {'chat', 'play'}:
1154 def pick_selectable(task_name):
1156 i = int(self.input_)
1157 if i < 0 or i >= len(self.selectables):
1158 self.log_msg('? invalid index, aborted')
1160 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1162 self.log_msg('? invalid index, aborted')
1164 self.switch_mode('play')
1166 def enter_ascii_art(command, height, width,
1167 with_pw=False, with_size=False):
1168 if with_size and self.ascii_draw_stage == 0:
1169 width = len(self.input_)
1171 self.log_msg('? input too long, must be max 36; try again')
1172 # TODO: move max width mechanism server-side
1174 old_size = self.game.player.carrying.design[0]
1175 if width != old_size.x:
1176 # TODO: save remaining design?
1177 self.game.player.carrying.design[1] = ''
1178 self.game.player.carrying.design[0] = YX(old_size.y, width)
1179 elif len(self.input_) > width:
1180 self.log_msg('? input too long, '
1181 'must be max %s; try again' % width)
1183 self.log_msg(' ' + self.input_)
1184 if with_size and self.input_ in {'', ' '}\
1185 and self.ascii_draw_stage > 0:
1186 height = self.ascii_draw_stage
1189 height = self.ascii_draw_stage + 2
1190 if len(self.input_) < width:
1191 self.input_ += ' ' * (width - len(self.input_))
1192 self.full_ascii_draw += self.input_
1194 old_size = self.game.player.carrying.design[0]
1195 self.game.player.carrying.design[0] = YX(height, old_size.x)
1196 self.ascii_draw_stage += 1
1197 if self.ascii_draw_stage < height:
1198 self.restore_input_values()
1200 if with_pw and with_size:
1201 self.send('%s_SIZE %s %s' % (command, YX(height, width),
1202 quote(self.password)))
1204 self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1205 quote(self.password)))
1207 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1208 self.full_ascii_draw = ""
1209 self.ascii_draw_stage = 0
1211 self.switch_mode('edit')
1213 action_descriptions = {
1215 'flatten': 'flatten surroundings',
1216 'teleport': 'teleport',
1217 'take_thing': 'pick up thing',
1218 'drop_thing': 'drop thing',
1219 'toggle_map_mode': 'toggle map view',
1220 'toggle_tile_draw': 'toggle protection character drawing',
1221 'install': '(un-)install',
1222 'wear': '(un-)wear',
1223 'door': 'open/close',
1224 'consume': 'consume',
1230 'flatten': 'FLATTEN_SURROUNDINGS',
1231 'take_thing': 'PICK_UP',
1232 'drop_thing': 'DROP',
1234 'install': 'INSTALL',
1237 'command': 'COMMAND',
1238 'consume': 'INTOXICATE',
1243 curses.curs_set(0) # hide cursor
1244 curses.start_color()
1245 self.set_default_colors()
1246 curses.init_pair(1, 7, 0)
1247 if not curses.can_change_color():
1248 self.log_msg('@ unfortunately, your terminal does not seem to '
1249 'support re-definition of colors; you might miss out '
1250 'on some color effects')
1253 self.explorer = YX(0, 0)
1255 store_widechar = False
1257 interval = datetime.timedelta(seconds=5)
1258 last_ping = datetime.datetime.now() - interval
1260 if self.disconnected and self.force_instant_connect:
1261 self.force_instant_connect = False
1263 now = datetime.datetime.now()
1264 if now - last_ping > interval:
1265 if self.disconnected:
1275 self.do_refresh = False
1278 msg = self.queue.get(block=False)
1283 key = stdscr.getkey()
1284 self.do_refresh = True
1285 except curses.error:
1290 # workaround for <https://stackoverflow.com/a/56390915>
1292 store_widechar = False
1293 key = bytes([195, keycode]).decode()
1295 store_widechar = True
1297 self.show_help = False
1298 self.draw_face = False
1299 if key == 'KEY_RESIZE':
1301 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1302 self.input_ = self.input_[:-1]
1303 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1304 or (self.mode.has_input_prompt and key == '\n'
1305 and self.input_ == ''\
1306 and self.mode.name in {'chat', 'command_thing',
1307 'take_thing', 'drop_thing',
1309 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1310 self.log_msg('@ aborted')
1311 self.switch_mode('play')
1312 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1313 self.show_help = True
1315 self.restore_input_values()
1316 elif self.mode.has_input_prompt and key != '\n': # Return key
1318 max_length = self.right_window_width * self.size.y - len(input_prompt) - 1
1319 if len(self.input_) > max_length:
1320 self.input_ = self.input_[:max_length]
1321 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1322 self.show_help = True
1323 elif self.mode.name == 'login' and key == '\n':
1324 self.login_name = self.input_
1325 self.send('LOGIN ' + quote(self.input_))
1327 elif self.mode.name == 'enter_face' and key == '\n':
1328 enter_ascii_art('PLAYER_FACE', 3, 6)
1329 elif self.mode.name == 'enter_design' and key == '\n':
1330 if self.game.player.carrying.type_ == 'Hat':
1331 enter_ascii_art('THING_DESIGN',
1332 self.game.player.carrying.design[0].y,
1333 self.game.player.carrying.design[0].x, True)
1335 enter_ascii_art('THING_DESIGN',
1336 self.game.player.carrying.design[0].y,
1337 self.game.player.carrying.design[0].x,
1339 elif self.mode.name == 'take_thing' and key == '\n':
1340 pick_selectable('PICK_UP')
1341 elif self.mode.name == 'drop_thing' and key == '\n':
1342 pick_selectable('DROP')
1343 elif self.mode.name == 'command_thing' and key == '\n':
1344 self.send('TASK:COMMAND ' + quote(self.input_))
1346 elif self.mode.name == 'control_pw_pw' and key == '\n':
1347 if self.input_ == '':
1348 self.log_msg('@ aborted')
1350 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1351 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1352 self.switch_mode('admin')
1353 elif self.mode.name == 'password' and key == '\n':
1354 if self.input_ == '':
1356 self.password = self.input_
1357 self.switch_mode('edit')
1358 elif self.mode.name == 'admin_enter' and key == '\n':
1359 self.send('BECOME_ADMIN ' + quote(self.input_))
1360 self.switch_mode('play')
1361 elif self.mode.name == 'control_pw_type' and key == '\n':
1362 if len(self.input_) != 1:
1363 self.log_msg('@ entered non-single-char, therefore aborted')
1364 self.switch_mode('admin')
1366 self.tile_control_char = self.input_
1367 self.switch_mode('control_pw_pw')
1368 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1369 if len(self.input_) != 1:
1370 self.log_msg('@ entered non-single-char, therefore aborted')
1372 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1373 self.log_msg('@ sent new protection character for thing')
1374 self.switch_mode('admin')
1375 elif self.mode.name == 'control_tile_type' and key == '\n':
1376 if len(self.input_) != 1:
1377 self.log_msg('@ entered non-single-char, therefore aborted')
1378 self.switch_mode('admin')
1380 self.tile_control_char = self.input_
1381 self.switch_mode('control_tile_draw')
1382 elif self.mode.name == 'chat' and key == '\n':
1383 if self.input_ == '':
1385 if self.input_[0] == '/':
1386 if self.input_.startswith('/nick'):
1387 tokens = self.input_.split(maxsplit=1)
1388 if len(tokens) == 2:
1389 self.send('NICK ' + quote(tokens[1]))
1391 self.log_msg('? need login name')
1393 self.log_msg('? unknown command')
1395 self.send('ALL ' + quote(self.input_))
1397 elif self.mode.name == 'name_thing' and key == '\n':
1398 if self.input_ == '':
1400 self.send('THING_NAME %s %s' % (quote(self.input_),
1401 quote(self.password)))
1402 self.switch_mode('edit')
1403 elif self.mode.name == 'annotate' and key == '\n':
1404 if self.input_ == '':
1406 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1407 quote(self.password)))
1408 self.switch_mode('edit')
1409 elif self.mode.name == 'portal' and key == '\n':
1410 if self.input_ == '':
1412 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1413 quote(self.password)))
1414 self.switch_mode('edit')
1415 elif self.mode.name == 'study':
1416 if self.mode.mode_switch_on_key(self, key):
1418 elif key == self.keys['toggle_map_mode']:
1419 self.toggle_map_mode()
1420 elif key in self.movement_keys:
1421 move_explorer(self.movement_keys[key])
1422 elif self.mode.name == 'play':
1423 if self.mode.mode_switch_on_key(self, key):
1425 elif key == self.keys['door'] and task_action_on('door'):
1426 self.send('TASK:DOOR')
1427 elif key == self.keys['consume'] and task_action_on('consume'):
1428 self.send('TASK:INTOXICATE')
1429 elif key == self.keys['wear'] and task_action_on('wear'):
1430 self.send('TASK:WEAR')
1431 elif key == self.keys['spin'] and task_action_on('spin'):
1432 self.send('TASK:SPIN')
1433 elif key == self.keys['dance'] and task_action_on('dance'):
1434 self.send('TASK:DANCE')
1435 elif key == self.keys['teleport']:
1436 if self.game.player.position in self.game.portals:
1437 self.host = self.game.portals[self.game.player.position]
1441 self.log_msg('? not standing on portal')
1442 elif key in self.movement_keys and task_action_on('move'):
1443 self.send('TASK:MOVE ' + self.movement_keys[key])
1444 elif self.mode.name == 'write':
1445 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1446 self.switch_mode('edit')
1447 elif self.mode.name == 'control_tile_draw':
1448 if self.mode.mode_switch_on_key(self, key):
1450 elif key in self.movement_keys:
1451 move_explorer(self.movement_keys[key])
1452 elif key == self.keys['toggle_tile_draw']:
1453 self.tile_draw = False if self.tile_draw else True
1454 elif self.mode.name == 'admin':
1455 if self.mode.mode_switch_on_key(self, key):
1457 elif key == self.keys['toggle_map_mode']:
1458 self.toggle_map_mode()
1459 elif key in self.movement_keys and task_action_on('move'):
1460 self.send('TASK:MOVE ' + self.movement_keys[key])
1461 elif self.mode.name == 'edit':
1462 if self.mode.mode_switch_on_key(self, key):
1464 elif key == self.keys['flatten'] and task_action_on('flatten'):
1465 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1466 elif key == self.keys['install'] and task_action_on('install'):
1467 self.send('TASK:INSTALL %s' % quote(self.password))
1468 elif key == self.keys['toggle_map_mode']:
1469 self.toggle_map_mode()
1470 elif key in self.movement_keys and task_action_on('move'):
1471 self.send('TASK:MOVE ' + self.movement_keys[key])
1473 if len(sys.argv) != 2:
1474 raise ArgError('wrong number of arguments, need game host')