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)
1080 players = [t for t in self.game.things if t.type_ == 'Player']
1081 players.sort(key=lambda t: len(t.name))
1083 shrink_offset = max(0, (self.size.y - self.left_window_width // 2) // 2)
1086 offset_y = y - shrink_offset
1087 max_len = max(5, (self.left_window_width // 2) - (offset_y * 2) - 8)
1089 if len(name) > max_len:
1090 name = name[:max_len - 1] + '…'
1091 safe_addstr(y, 0, '@%s:%s' % (t.thing_char, name))
1093 if y >= self.size.y:
1096 def draw_face_popup():
1097 t = self.game.get_thing(self.draw_face)
1098 if not t or not hasattr(t, 'face'):
1099 self.draw_face = False
1102 start_x = self.left_window_width - 10
1103 def draw_body_part(body_part, end_y):
1104 safe_addstr(end_y - 3, start_x, '----------')
1105 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1106 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1107 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1109 if hasattr(t, 'face'):
1110 draw_body_part(t.face, self.size.y - 3)
1111 if hasattr(t, 'hat'):
1112 draw_body_part(t.hat, self.size.y - 6)
1113 safe_addstr(self.size.y - 2, start_x, '----------')
1116 name = name[:6 - 1] + '…'
1117 safe_addstr(self.size.y - 1, start_x,
1118 '@%s:%s' % (t.thing_char, name))
1121 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1122 self.mode.help_intro)
1123 if len(self.mode.available_actions) > 0:
1124 content += "Available actions:\n"
1125 for action in self.mode.available_actions:
1126 if action in action_tasks:
1127 if action_tasks[action] not in self.game.tasks:
1129 if action == 'move_explorer':
1131 if action == 'move':
1132 key = ','.join(self.movement_keys)
1134 key = self.keys[action]
1135 content += '[%s] – %s\n' % (key, action_descriptions[action])
1137 content += self.mode.list_available_modes(self)
1138 for i in range(self.size.y):
1140 self.left_window_width * (not self.mode.has_input_prompt),
1141 ' ' * self.left_window_width)
1143 for line in content.split('\n'):
1144 lines += msg_into_lines_of_width(line, self.right_window_width)
1145 for i in range(len(lines)):
1146 if i >= self.size.y:
1149 self.left_window_width * (not self.mode.has_input_prompt),
1154 stdscr.bkgd(' ', curses.color_pair(1))
1155 recalc_input_lines()
1156 if self.mode.has_input_prompt:
1158 if self.mode.shows_info:
1163 if not self.mode.is_intro:
1168 if self.mode.name in {'chat', 'play'}:
1173 def pick_selectable(task_name):
1175 i = int(self.input_)
1176 if i < 0 or i >= len(self.selectables):
1177 self.log_msg('? invalid index, aborted')
1179 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1181 self.log_msg('? invalid index, aborted')
1183 self.switch_mode('play')
1185 def enter_ascii_art(command, height, width,
1186 with_pw=False, with_size=False):
1187 if with_size and self.ascii_draw_stage == 0:
1188 width = len(self.input_)
1190 self.log_msg('? input too long, must be max 36; try again')
1191 # TODO: move max width mechanism server-side
1193 old_size = self.game.player.carrying.design[0]
1194 if width != old_size.x:
1195 # TODO: save remaining design?
1196 self.game.player.carrying.design[1] = ''
1197 self.game.player.carrying.design[0] = YX(old_size.y, width)
1198 elif len(self.input_) > width:
1199 self.log_msg('? input too long, '
1200 'must be max %s; try again' % width)
1202 self.log_msg(' ' + self.input_)
1203 if with_size and self.input_ in {'', ' '}\
1204 and self.ascii_draw_stage > 0:
1205 height = self.ascii_draw_stage
1208 height = self.ascii_draw_stage + 2
1209 if len(self.input_) < width:
1210 self.input_ += ' ' * (width - len(self.input_))
1211 self.full_ascii_draw += self.input_
1213 old_size = self.game.player.carrying.design[0]
1214 self.game.player.carrying.design[0] = YX(height, old_size.x)
1215 self.ascii_draw_stage += 1
1216 if self.ascii_draw_stage < height:
1217 self.restore_input_values()
1219 if with_pw and with_size:
1220 self.send('%s_SIZE %s %s' % (command, YX(height, width),
1221 quote(self.password)))
1223 self.send('%s %s %s' % (command, quote(self.full_ascii_draw),
1224 quote(self.password)))
1226 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1227 self.full_ascii_draw = ""
1228 self.ascii_draw_stage = 0
1230 self.switch_mode('edit')
1232 action_descriptions = {
1234 'flatten': 'flatten surroundings',
1235 'teleport': 'teleport',
1236 'take_thing': 'pick up thing',
1237 'drop_thing': 'drop thing',
1238 'toggle_map_mode': 'toggle map view',
1239 'toggle_tile_draw': 'toggle protection character drawing',
1240 'install': '(un-)install',
1241 'wear': '(un-)wear',
1242 'door': 'open/close',
1243 'consume': 'consume',
1249 'flatten': 'FLATTEN_SURROUNDINGS',
1250 'take_thing': 'PICK_UP',
1251 'drop_thing': 'DROP',
1253 'install': 'INSTALL',
1256 'command': 'COMMAND',
1257 'consume': 'INTOXICATE',
1262 curses.curs_set(0) # hide cursor
1263 curses.start_color()
1264 self.set_default_colors()
1265 curses.init_pair(1, 7, 0)
1266 if not curses.can_change_color():
1267 self.log_msg('@ unfortunately, your terminal does not seem to '
1268 'support re-definition of colors; you might miss out '
1269 'on some color effects')
1272 self.explorer = YX(0, 0)
1274 store_widechar = False
1276 interval = datetime.timedelta(seconds=5)
1277 last_ping = datetime.datetime.now() - interval
1279 if self.disconnected and self.force_instant_connect:
1280 self.force_instant_connect = False
1282 now = datetime.datetime.now()
1283 if now - last_ping > interval:
1284 if self.disconnected:
1294 self.do_refresh = False
1297 msg = self.queue.get(block=False)
1302 key = stdscr.getkey()
1303 self.do_refresh = True
1304 except curses.error:
1309 # workaround for <https://stackoverflow.com/a/56390915>
1311 store_widechar = False
1312 key = bytes([195, keycode]).decode()
1314 store_widechar = True
1316 self.show_help = False
1317 self.draw_face = False
1318 if key == 'KEY_RESIZE':
1320 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1321 self.input_ = self.input_[:-1]
1322 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1323 or (self.mode.has_input_prompt and key == '\n'
1324 and self.input_ == ''\
1325 and self.mode.name in {'chat', 'command_thing',
1326 'take_thing', 'drop_thing',
1328 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1329 self.log_msg('@ aborted')
1330 self.switch_mode('play')
1331 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1332 self.show_help = True
1334 self.restore_input_values()
1335 elif self.mode.has_input_prompt and key != '\n': # Return key
1337 max_length = self.right_window_width * self.size.y - len(input_prompt) - 1
1338 if len(self.input_) > max_length:
1339 self.input_ = self.input_[:max_length]
1340 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1341 self.show_help = True
1342 elif self.mode.name == 'login' and key == '\n':
1343 self.login_name = self.input_
1344 self.send('LOGIN ' + quote(self.input_))
1346 elif self.mode.name == 'enter_face' and key == '\n':
1347 enter_ascii_art('PLAYER_FACE', 3, 6)
1348 elif self.mode.name == 'enter_design' and key == '\n':
1349 if self.game.player.carrying.type_ == 'Hat':
1350 enter_ascii_art('THING_DESIGN',
1351 self.game.player.carrying.design[0].y,
1352 self.game.player.carrying.design[0].x, True)
1354 enter_ascii_art('THING_DESIGN',
1355 self.game.player.carrying.design[0].y,
1356 self.game.player.carrying.design[0].x,
1358 elif self.mode.name == 'take_thing' and key == '\n':
1359 pick_selectable('PICK_UP')
1360 elif self.mode.name == 'drop_thing' and key == '\n':
1361 pick_selectable('DROP')
1362 elif self.mode.name == 'command_thing' and key == '\n':
1363 self.send('TASK:COMMAND ' + quote(self.input_))
1365 elif self.mode.name == 'control_pw_pw' and key == '\n':
1366 if self.input_ == '':
1367 self.log_msg('@ aborted')
1369 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1370 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1371 self.switch_mode('admin')
1372 elif self.mode.name == 'password' and key == '\n':
1373 if self.input_ == '':
1375 self.password = self.input_
1376 self.switch_mode('edit')
1377 elif self.mode.name == 'admin_enter' and key == '\n':
1378 self.send('BECOME_ADMIN ' + quote(self.input_))
1379 self.switch_mode('play')
1380 elif self.mode.name == 'control_pw_type' and key == '\n':
1381 if len(self.input_) != 1:
1382 self.log_msg('@ entered non-single-char, therefore aborted')
1383 self.switch_mode('admin')
1385 self.tile_control_char = self.input_
1386 self.switch_mode('control_pw_pw')
1387 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1388 if len(self.input_) != 1:
1389 self.log_msg('@ entered non-single-char, therefore aborted')
1391 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1392 self.log_msg('@ sent new protection character for thing')
1393 self.switch_mode('admin')
1394 elif self.mode.name == 'control_tile_type' and key == '\n':
1395 if len(self.input_) != 1:
1396 self.log_msg('@ entered non-single-char, therefore aborted')
1397 self.switch_mode('admin')
1399 self.tile_control_char = self.input_
1400 self.switch_mode('control_tile_draw')
1401 elif self.mode.name == 'chat' and key == '\n':
1402 if self.input_ == '':
1404 if self.input_[0] == '/':
1405 if self.input_.startswith('/nick'):
1406 tokens = self.input_.split(maxsplit=1)
1407 if len(tokens) == 2:
1408 self.send('NICK ' + quote(tokens[1]))
1410 self.log_msg('? need login name')
1412 self.log_msg('? unknown command')
1414 self.send('ALL ' + quote(self.input_))
1416 elif self.mode.name == 'name_thing' and key == '\n':
1417 if self.input_ == '':
1419 self.send('THING_NAME %s %s' % (quote(self.input_),
1420 quote(self.password)))
1421 self.switch_mode('edit')
1422 elif self.mode.name == 'annotate' and key == '\n':
1423 if self.input_ == '':
1425 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1426 quote(self.password)))
1427 self.switch_mode('edit')
1428 elif self.mode.name == 'portal' and key == '\n':
1429 if self.input_ == '':
1431 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1432 quote(self.password)))
1433 self.switch_mode('edit')
1434 elif self.mode.name == 'study':
1435 if self.mode.mode_switch_on_key(self, key):
1437 elif key == self.keys['toggle_map_mode']:
1438 self.toggle_map_mode()
1439 elif key in self.movement_keys:
1440 move_explorer(self.movement_keys[key])
1441 elif self.mode.name == 'play':
1442 if self.mode.mode_switch_on_key(self, key):
1444 elif key == self.keys['door'] and task_action_on('door'):
1445 self.send('TASK:DOOR')
1446 elif key == self.keys['consume'] and task_action_on('consume'):
1447 self.send('TASK:INTOXICATE')
1448 elif key == self.keys['wear'] and task_action_on('wear'):
1449 self.send('TASK:WEAR')
1450 elif key == self.keys['spin'] and task_action_on('spin'):
1451 self.send('TASK:SPIN')
1452 elif key == self.keys['dance'] and task_action_on('dance'):
1453 self.send('TASK:DANCE')
1454 elif key == self.keys['teleport']:
1455 if self.game.player.position in self.game.portals:
1456 self.host = self.game.portals[self.game.player.position]
1460 self.log_msg('? not standing on portal')
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 == 'write':
1464 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1465 self.switch_mode('edit')
1466 elif self.mode.name == 'control_tile_draw':
1467 if self.mode.mode_switch_on_key(self, key):
1469 elif key in self.movement_keys:
1470 move_explorer(self.movement_keys[key])
1471 elif key == self.keys['toggle_tile_draw']:
1472 self.tile_draw = False if self.tile_draw else True
1473 elif self.mode.name == 'admin':
1474 if self.mode.mode_switch_on_key(self, key):
1476 elif key == self.keys['toggle_map_mode']:
1477 self.toggle_map_mode()
1478 elif key in self.movement_keys and task_action_on('move'):
1479 self.send('TASK:MOVE ' + self.movement_keys[key])
1480 elif self.mode.name == 'edit':
1481 if self.mode.mode_switch_on_key(self, key):
1483 elif key == self.keys['flatten'] and task_action_on('flatten'):
1484 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1485 elif key == self.keys['install'] and task_action_on('install'):
1486 self.send('TASK:INSTALL %s' % quote(self.password))
1487 elif key == self.keys['toggle_map_mode']:
1488 self.toggle_map_mode()
1489 elif key in self.movement_keys and task_action_on('move'):
1490 self.send('TASK:MOVE ' + self.movement_keys[key])
1492 if len(sys.argv) != 2:
1493 raise ArgError('wrong number of arguments, need game host')