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 (enter nothing to abort):',
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..'
61 'intro': '@ enter hat line (enter nothing to abort):',
62 '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. Eat cookies to extend the ASCII characters available for drawing.'
67 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
70 'short': 'change protection character password',
71 'intro': '@ enter protection character for which you want to change the password:',
72 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
75 'short': 'change protection character password',
77 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
79 'control_tile_type': {
80 'short': 'change tiles protection',
81 'intro': '@ enter protection character which you want to draw:',
82 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
84 'control_tile_draw': {
85 'short': 'change tiles protection',
87 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
90 'short': 'annotate tile',
92 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
95 'short': 'edit portal',
97 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
102 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
107 'long': 'Enter your player name.'
109 'waiting_for_server': {
110 'short': 'waiting for server response',
111 'intro': '@ waiting for server …',
112 'long': 'Waiting for a server response.'
115 'short': 'waiting for server response',
117 'long': 'Waiting for a server response.'
120 'short': 'set world edit password',
122 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
125 'short': 'become admin',
126 'intro': '@ enter admin password:',
127 'long': 'This mode allows you to become admin if you know an admin password.'
132 'long': 'This mode allows you access to actions limited to administrators.'
136 from ws4py.client import WebSocketBaseClient
137 class WebSocketClient(WebSocketBaseClient):
139 def __init__(self, recv_handler, *args, **kwargs):
140 super().__init__(*args, **kwargs)
141 self.recv_handler = recv_handler
144 def received_message(self, message):
146 message = str(message)
147 self.recv_handler(message)
150 def plom_closed(self):
151 return self.client_terminated
153 from plomrogue.io_tcp import PlomSocket
154 class PlomSocketClient(PlomSocket):
156 def __init__(self, recv_handler, url):
158 self.recv_handler = recv_handler
159 host, port = url.split(':')
160 super().__init__(socket.create_connection((host, port)))
168 for msg in self.recv():
169 if msg == 'NEED_SSL':
170 self.socket = ssl.wrap_socket(self.socket)
172 self.recv_handler(msg)
173 except BrokenSocketConnection:
174 pass # we assume socket will be known as dead by now
176 def cmd_TURN(game, n):
177 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_OTHER_WIPE(game):
181 game.portals_new = {}
182 game.annotations_new = {}
184 cmd_OTHER_WIPE.argtypes = ''
186 def cmd_LOGIN_OK(game):
187 game.tui.switch_mode('post_login_wait')
188 game.tui.send('GET_GAMESTATE')
189 game.tui.log_msg('@ welcome!')
190 game.tui.log_msg('@ hint: see top of terminal for how to get help.')
191 game.tui.log_msg('@ hint: enter study mode to understand your environment.')
192 cmd_LOGIN_OK.argtypes = ''
194 def cmd_ADMIN_OK(game):
195 game.tui.is_admin = True
196 game.tui.log_msg('@ you now have admin rights')
197 game.tui.switch_mode('admin')
198 game.tui.do_refresh = True
199 cmd_ADMIN_OK.argtypes = ''
201 def cmd_REPLY(game, msg):
202 game.tui.log_msg('#MUSICPLAYER: ' + msg)
203 game.tui.do_refresh = True
204 cmd_REPLY.argtypes = 'string'
206 def cmd_CHAT(game, msg):
207 game.tui.log_msg('# ' + msg)
208 game.tui.do_refresh = True
209 cmd_CHAT.argtypes = 'string'
211 def cmd_CHATFACE(game, thing_id):
212 game.tui.draw_face = thing_id
213 game.tui.do_refresh = True
214 cmd_CHATFACE.argtypes = 'int:pos'
216 def cmd_PLAYER_ID(game, player_id):
217 game.player_id = player_id
218 cmd_PLAYER_ID.argtypes = 'int:nonneg'
220 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
221 game.players_hat_chars_new = hat_chars
222 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
224 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
225 t = game.get_thing_temp(thing_id)
227 t = ThingBase(game, thing_id)
228 game.things_new += [t]
231 t.protection = protection
232 t.portable = portable
233 t.commandable = commandable
234 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
236 def cmd_THING_NAME(game, thing_id, name):
237 t = game.get_thing_temp(thing_id)
239 cmd_THING_NAME.argtypes = 'int:pos string'
241 def cmd_THING_FACE(game, thing_id, face):
242 t = game.get_thing_temp(thing_id)
244 cmd_THING_FACE.argtypes = 'int:pos string'
246 def cmd_THING_HAT(game, thing_id, hat):
247 t = game.get_thing_temp(thing_id)
249 cmd_THING_HAT.argtypes = 'int:pos string'
251 def cmd_THING_CHAR(game, thing_id, c):
252 t = game.get_thing_temp(thing_id)
254 cmd_THING_CHAR.argtypes = 'int:pos char'
256 def cmd_MAP(game, geometry, size, content):
257 map_geometry_class = globals()['MapGeometry' + geometry]
258 game.map_geometry_new = map_geometry_class(size)
259 game.map_content_new = content
260 if type(game.map_geometry_new) == MapGeometrySquare:
261 game.tui.movement_keys = {
262 game.tui.keys['square_move_up']: 'UP',
263 game.tui.keys['square_move_left']: 'LEFT',
264 game.tui.keys['square_move_down']: 'DOWN',
265 game.tui.keys['square_move_right']: 'RIGHT',
267 elif type(game.map_geometry_new) == MapGeometryHex:
268 game.tui.movement_keys = {
269 game.tui.keys['hex_move_upleft']: 'UPLEFT',
270 game.tui.keys['hex_move_upright']: 'UPRIGHT',
271 game.tui.keys['hex_move_right']: 'RIGHT',
272 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
273 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
274 game.tui.keys['hex_move_left']: 'LEFT',
276 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
278 def cmd_FOV(game, content):
279 game.fov_new = content
280 cmd_FOV.argtypes = 'string'
282 def cmd_MAP_CONTROL(game, content):
283 game.map_control_content_new = content
284 cmd_MAP_CONTROL.argtypes = 'string'
286 def cmd_GAME_STATE_COMPLETE(game):
287 game.tui.do_refresh = True
288 game.tui.info_cached = None
289 game.things = game.things_new
290 game.portals = game.portals_new
291 game.annotations = game.annotations_new
292 game.fov = game.fov_new
293 game.map_geometry = game.map_geometry_new
294 game.map_content = game.map_content_new
295 game.map_control_content = game.map_control_content_new
296 game.player = game.get_thing(game.player_id)
297 game.players_hat_chars = game.players_hat_chars_new
298 game.bladder_pressure = game.bladder_pressure_new
299 game.weariness = game.weariness_new
300 game.turn_complete = True
301 if game.tui.mode.name == 'post_login_wait':
302 game.tui.switch_mode('play')
303 cmd_GAME_STATE_COMPLETE.argtypes = ''
305 def cmd_PORTAL(game, position, msg):
306 game.portals_new[position] = msg
307 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
309 def cmd_PLAY_ERROR(game, msg):
310 game.tui.log_msg('? ' + msg)
311 game.tui.flash = True
312 game.tui.do_refresh = True
313 cmd_PLAY_ERROR.argtypes = 'string'
315 def cmd_GAME_ERROR(game, msg):
316 game.tui.log_msg('? game error: ' + msg)
317 game.tui.do_refresh = True
318 cmd_GAME_ERROR.argtypes = 'string'
320 def cmd_ARGUMENT_ERROR(game, msg):
321 game.tui.log_msg('? syntax error: ' + msg)
322 game.tui.do_refresh = True
323 cmd_ARGUMENT_ERROR.argtypes = 'string'
325 def cmd_ANNOTATION(game, position, msg):
326 game.annotations_new[position] = msg
327 if game.tui.mode.shows_info:
328 game.tui.do_refresh = True
329 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
331 def cmd_TASKS(game, tasks_comma_separated):
332 game.tasks = tasks_comma_separated.split(',')
333 game.tui.mode_write.legal = 'WRITE' in game.tasks
334 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
335 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
336 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
337 cmd_TASKS.argtypes = 'string'
339 def cmd_THING_TYPE(game, thing_type, symbol_hint):
340 game.thing_types[thing_type] = symbol_hint
341 cmd_THING_TYPE.argtypes = 'string char'
343 def cmd_THING_INSTALLED(game, thing_id):
344 game.get_thing_temp(thing_id).installed = True
345 cmd_THING_INSTALLED.argtypes = 'int:pos'
347 def cmd_THING_CARRYING(game, thing_id, carried_id):
348 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
349 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
351 def cmd_TERRAIN(game, terrain_char, terrain_desc):
352 game.terrains[terrain_char] = terrain_desc
353 cmd_TERRAIN.argtypes = 'char string'
357 cmd_PONG.argtypes = ''
359 def cmd_DEFAULT_COLORS(game):
360 game.tui.set_default_colors()
361 cmd_DEFAULT_COLORS.argtypes = ''
363 def cmd_RANDOM_COLORS(game):
364 game.tui.set_random_colors()
365 cmd_RANDOM_COLORS.argtypes = ''
367 def cmd_STATS(game, bladder_pressure, weariness):
368 game.bladder_pressure_new = bladder_pressure
369 game.weariness_new = weariness
370 cmd_STATS.argtypes = 'int:nonneg int:nonneg'
372 class Game(GameBase):
373 turn_complete = False
378 def __init__(self, *args, **kwargs):
379 super().__init__(*args, **kwargs)
380 self.register_command(cmd_LOGIN_OK)
381 self.register_command(cmd_ADMIN_OK)
382 self.register_command(cmd_PONG)
383 self.register_command(cmd_CHAT)
384 self.register_command(cmd_CHATFACE)
385 self.register_command(cmd_REPLY)
386 self.register_command(cmd_PLAYER_ID)
387 self.register_command(cmd_TURN)
388 self.register_command(cmd_OTHER_WIPE)
389 self.register_command(cmd_THING)
390 self.register_command(cmd_THING_TYPE)
391 self.register_command(cmd_THING_NAME)
392 self.register_command(cmd_THING_CHAR)
393 self.register_command(cmd_THING_FACE)
394 self.register_command(cmd_THING_HAT)
395 self.register_command(cmd_THING_CARRYING)
396 self.register_command(cmd_THING_INSTALLED)
397 self.register_command(cmd_TERRAIN)
398 self.register_command(cmd_MAP)
399 self.register_command(cmd_MAP_CONTROL)
400 self.register_command(cmd_PORTAL)
401 self.register_command(cmd_ANNOTATION)
402 self.register_command(cmd_GAME_STATE_COMPLETE)
403 self.register_command(cmd_PLAYERS_HAT_CHARS)
404 self.register_command(cmd_ARGUMENT_ERROR)
405 self.register_command(cmd_GAME_ERROR)
406 self.register_command(cmd_PLAY_ERROR)
407 self.register_command(cmd_TASKS)
408 self.register_command(cmd_FOV)
409 self.register_command(cmd_DEFAULT_COLORS)
410 self.register_command(cmd_RANDOM_COLORS)
411 self.register_command(cmd_STATS)
412 self.map_content = ''
413 self.players_hat_chars = ''
415 self.annotations = {}
416 self.annotations_new = {}
418 self.portals_new = {}
422 def get_string_options(self, string_option_type):
423 if string_option_type == 'map_geometry':
424 return ['Hex', 'Square']
425 elif string_option_type == 'thing_type':
426 return self.thing_types.keys()
429 def get_command(self, command_name):
430 from functools import partial
431 f = partial(self.commands[command_name], self)
432 f.argtypes = self.commands[command_name].argtypes
435 def get_thing_temp(self, id_):
436 for thing in self.things_new:
443 def __init__(self, name, has_input_prompt=False, shows_info=False,
444 is_intro=False, is_single_char_entry=False):
446 self.short_desc = mode_helps[name]['short']
447 self.available_modes = []
448 self.available_actions = []
449 self.has_input_prompt = has_input_prompt
450 self.shows_info = shows_info
451 self.is_intro = is_intro
452 self.help_intro = mode_helps[name]['long']
453 self.intro_msg = mode_helps[name]['intro']
454 self.is_single_char_entry = is_single_char_entry
457 def iter_available_modes(self, tui):
458 for mode_name in self.available_modes:
459 mode = getattr(tui, 'mode_' + mode_name)
462 key = tui.keys['switch_to_' + mode.name]
465 def list_available_modes(self, tui):
467 if len(self.available_modes) > 0:
468 msg = 'Other modes available from here:\n'
469 for mode, key in self.iter_available_modes(tui):
470 msg += '[%s] – %s\n' % (key, mode.short_desc)
473 def mode_switch_on_key(self, tui, key_pressed):
474 for mode, key in self.iter_available_modes(tui):
475 if key_pressed == key:
476 tui.switch_mode(mode.name)
481 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
482 mode_admin = Mode('admin')
483 mode_play = Mode('play')
484 mode_study = Mode('study', shows_info=True)
485 mode_write = Mode('write', is_single_char_entry=True)
486 mode_edit = Mode('edit')
487 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
488 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
489 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
490 mode_control_tile_draw = Mode('control_tile_draw')
491 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
492 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
493 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
494 mode_chat = Mode('chat', has_input_prompt=True)
495 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
496 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
497 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
498 mode_password = Mode('password', has_input_prompt=True)
499 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
500 mode_command_thing = Mode('command_thing', has_input_prompt=True)
501 mode_take_thing = Mode('take_thing', has_input_prompt=True)
502 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
503 mode_enter_face = Mode('enter_face', has_input_prompt=True)
504 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
508 def __init__(self, host):
511 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
512 "command_thing", "take_thing",
514 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
515 "install", "wear", "spin"]
516 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
517 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
518 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
519 "control_tile_type", "chat",
520 "study", "play", "edit"]
521 self.mode_admin.available_actions = ["move"]
522 self.mode_control_tile_draw.available_modes = ["admin_enter"]
523 self.mode_control_tile_draw.available_actions = ["move_explorer",
525 self.mode_edit.available_modes = ["write", "annotate", "portal",
526 "name_thing", "enter_face", "enter_hat",
528 "chat", "study", "play", "admin_enter"]
529 self.mode_edit.available_actions = ["move", "flatten", "install",
535 self.parser = Parser(self.game)
537 self.do_refresh = True
538 self.queue = queue.Queue()
539 self.login_name = None
540 self.map_mode = 'terrain + things'
541 self.password = 'foo'
542 self.switch_mode('waiting_for_server')
544 'switch_to_chat': 't',
545 'switch_to_play': 'p',
546 'switch_to_password': 'P',
547 'switch_to_annotate': 'M',
548 'switch_to_portal': 'T',
549 'switch_to_study': '?',
550 'switch_to_edit': 'E',
551 'switch_to_write': 'm',
552 'switch_to_name_thing': 'N',
553 'switch_to_command_thing': 'O',
554 'switch_to_admin_enter': 'A',
555 'switch_to_control_pw_type': 'C',
556 'switch_to_control_tile_type': 'Q',
557 'switch_to_admin_thing_protect': 'T',
559 'switch_to_enter_face': 'f',
560 'switch_to_enter_hat': 'H',
561 'switch_to_take_thing': 'z',
562 'switch_to_drop_thing': 'u',
570 'toggle_map_mode': 'L',
571 'toggle_tile_draw': 'm',
572 'hex_move_upleft': 'w',
573 'hex_move_upright': 'e',
574 'hex_move_right': 'd',
575 'hex_move_downright': 'x',
576 'hex_move_downleft': 'y',
577 'hex_move_left': 'a',
578 'square_move_up': 'w',
579 'square_move_left': 'a',
580 'square_move_down': 's',
581 'square_move_right': 'd',
583 if os.path.isfile('config.json'):
584 with open('config.json', 'r') as f:
585 keys_conf = json.loads(f.read())
587 self.keys[k] = keys_conf[k]
588 self.show_help = False
589 self.disconnected = True
590 self.force_instant_connect = True
591 self.input_lines = []
595 self.ascii_draw_stage = 0
596 self.full_ascii_draw = ''
597 self.offset = YX(0,0)
598 curses.wrapper(self.loop)
602 def handle_recv(msg):
608 self.log_msg('@ attempting connect')
609 socket_client_class = PlomSocketClient
610 if self.host.startswith('ws://') or self.host.startswith('wss://'):
611 socket_client_class = WebSocketClient
613 self.socket = socket_client_class(handle_recv, self.host)
614 self.socket_thread = threading.Thread(target=self.socket.run)
615 self.socket_thread.start()
616 self.disconnected = False
617 self.game.thing_types = {}
618 self.game.terrains = {}
619 time.sleep(0.1) # give potential SSL negotation some time …
620 self.socket.send('TASKS')
621 self.socket.send('TERRAINS')
622 self.socket.send('THING_TYPES')
623 self.switch_mode('login')
624 except ConnectionRefusedError:
625 self.log_msg('@ server connect failure')
626 self.disconnected = True
627 self.switch_mode('waiting_for_server')
628 self.do_refresh = True
631 self.log_msg('@ attempting reconnect')
633 # necessitated by some strange SSL race conditions with ws4py
634 time.sleep(0.1) # FIXME find out why exactly necessary
635 self.switch_mode('waiting_for_server')
640 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
641 raise BrokenSocketConnection
642 self.socket.send(msg)
643 except (BrokenPipeError, BrokenSocketConnection):
644 self.log_msg('@ server disconnected :(')
645 self.disconnected = True
646 self.force_instant_connect = True
647 self.do_refresh = True
649 def log_msg(self, msg):
651 if len(self.log) > 100:
652 self.log = self.log[-100:]
654 def restore_input_values(self):
655 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
656 self.input_ = self.game.annotations[self.explorer]
657 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
658 self.input_ = self.game.portals[self.explorer]
659 elif self.mode.name == 'password':
660 self.input_ = self.password
661 elif self.mode.name == 'name_thing':
662 if hasattr(self.game.player.carrying, 'name'):
663 self.input_ = self.game.player.carrying.name
664 elif self.mode.name == 'admin_thing_protect':
665 if hasattr(self.game.player.carrying, 'protection'):
666 self.input_ = self.game.player.carrying.protection
667 elif self.mode.name in {'enter_face', 'enter_hat'}:
668 start = self.ascii_draw_stage * 6
669 end = (self.ascii_draw_stage + 1) * 6
670 if self.mode.name == 'enter_face':
671 self.input_ = self.game.player.face[start:end]
672 elif self.mode.name == 'enter_hat':
673 self.input_ = self.game.player.hat[start:end]
675 def send_tile_control_command(self):
676 self.send('SET_TILE_CONTROL %s %s' %
677 (self.explorer, quote(self.tile_control_char)))
679 def toggle_map_mode(self):
680 if self.map_mode == 'terrain only':
681 self.map_mode = 'terrain + annotations'
682 elif self.map_mode == 'terrain + annotations':
683 self.map_mode = 'terrain + things'
684 elif self.map_mode == 'terrain + things':
685 self.map_mode = 'protections'
686 elif self.map_mode == 'protections':
687 self.map_mode = 'terrain only'
689 def switch_mode(self, mode_name):
691 def fail(msg, return_mode='play'):
692 self.log_msg('? ' + msg)
694 self.switch_mode(return_mode)
696 if self.mode and self.mode.name == 'control_tile_draw':
697 self.log_msg('@ finished tile protection drawing.')
698 self.draw_face = False
699 self.tile_draw = False
700 if mode_name == 'command_thing' and\
701 (not self.game.player.carrying or
702 not self.game.player.carrying.commandable):
703 return fail('not carrying anything commandable')
704 if mode_name == 'name_thing' and not self.game.player.carrying:
705 return fail('not carrying anything to re-name')
706 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
707 return fail('not carrying anything to protect')
708 if mode_name == 'take_thing' and self.game.player.carrying:
709 return fail('already carrying something')
710 if mode_name == 'drop_thing' and not self.game.player.carrying:
711 return fail('not carrying anything droppable')
712 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
713 return fail('not wearing hat to edit', 'edit')
714 if mode_name == 'admin_enter' and self.is_admin:
716 self.mode = getattr(self, 'mode_' + mode_name)
717 if self.mode.name in {'control_tile_draw', 'control_tile_type',
719 self.map_mode = 'protections'
720 elif self.mode.name != 'edit':
721 self.map_mode = 'terrain + things'
722 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
723 self.explorer = YX(self.game.player.position.y,
724 self.game.player.position.x)
725 if self.mode.is_single_char_entry:
726 self.show_help = True
727 if len(self.mode.intro_msg) > 0:
728 self.log_msg(self.mode.intro_msg)
729 if self.mode.name == 'login':
731 self.send('LOGIN ' + quote(self.login_name))
733 self.log_msg('@ enter username')
734 elif self.mode.name == 'take_thing':
735 self.log_msg('Portable things in reach for pick-up:')
737 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
739 if type(self.game.map_geometry) == MapGeometrySquare:
740 directed_moves['UP'] = YX(-1, 0)
741 directed_moves['DOWN'] = YX(1, 0)
742 elif type(self.game.map_geometry) == MapGeometryHex:
743 if self.game.player.position.y % 2:
744 directed_moves['UPLEFT'] = YX(-1, 0)
745 directed_moves['UPRIGHT'] = YX(-1, 1)
746 directed_moves['DOWNLEFT'] = YX(1, 0)
747 directed_moves['DOWNRIGHT'] = YX(1, 1)
749 directed_moves['UPLEFT'] = YX(-1, -1)
750 directed_moves['UPRIGHT'] = YX(-1, 0)
751 directed_moves['DOWNLEFT'] = YX(1, -1)
752 directed_moves['DOWNRIGHT'] = YX(1, 0)
754 for direction in directed_moves:
755 move = directed_moves[direction]
756 select_range[direction] = self.game.player.position + move
757 self.selectables = []
759 for direction in select_range:
760 for t in [t for t in self.game.things
761 if t.portable and t.position == select_range[direction]]:
762 self.selectables += [t.id_]
763 directions += [direction]
764 if len(self.selectables) == 0:
765 return fail('nothing to pick-up')
767 for i in range(len(self.selectables)):
768 t = self.game.get_thing(self.selectables[i])
769 self.log_msg('%s %s: %s' % (i, directions[i],
770 self.get_thing_info(t)))
771 elif self.mode.name == 'drop_thing':
772 self.log_msg('Direction to drop thing to:')
774 ['HERE'] + list(self.game.tui.movement_keys.values())
775 for i in range(len(self.selectables)):
776 self.log_msg(str(i) + ': ' + self.selectables[i])
777 elif self.mode.name == 'enter_hat':
778 self.log_msg('legal characters: ' + self.game.players_hat_chars)
779 elif self.mode.name == 'command_thing':
780 self.send('TASK:COMMAND ' + quote('HELP'))
781 elif self.mode.name == 'control_pw_pw':
782 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
783 elif self.mode.name == 'control_tile_draw':
784 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']))
786 self.restore_input_values()
788 def set_default_colors(self):
789 curses.init_color(1, 1000, 1000, 1000)
790 curses.init_color(2, 0, 0, 0)
791 self.do_refresh = True
793 def set_random_colors(self):
797 return int(offset + random.random()*375)
799 curses.init_color(1, rand(625), rand(625), rand(625))
800 curses.init_color(2, rand(0), rand(0), rand(0))
801 self.do_refresh = True
805 return self.info_cached
806 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
808 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
809 info_to_cache += 'outside field of view'
811 for t in self.game.things:
812 if t.position == self.explorer:
813 info_to_cache += 'THING: %s' % self.get_thing_info(t)
814 protection = t.protection
815 if protection == '.':
817 info_to_cache += ' / protection: %s\n' % protection
818 if hasattr(t, 'hat'):
819 info_to_cache += t.hat[0:6] + '\n'
820 info_to_cache += t.hat[6:12] + '\n'
821 info_to_cache += t.hat[12:18] + '\n'
822 if hasattr(t, 'face'):
823 info_to_cache += t.face[0:6] + '\n'
824 info_to_cache += t.face[6:12] + '\n'
825 info_to_cache += t.face[12:18] + '\n'
826 terrain_char = self.game.map_content[pos_i]
828 if terrain_char in self.game.terrains:
829 terrain_desc = self.game.terrains[terrain_char]
830 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
832 protection = self.game.map_control_content[pos_i]
833 if protection == '.':
834 protection = 'unprotected'
835 info_to_cache += 'PROTECTION: %s\n' % protection
836 if self.explorer in self.game.portals:
837 info_to_cache += 'PORTAL: ' +\
838 self.game.portals[self.explorer] + '\n'
840 info_to_cache += 'PORTAL: (none)\n'
841 if self.explorer in self.game.annotations:
842 info_to_cache += 'ANNOTATION: ' +\
843 self.game.annotations[self.explorer]
844 self.info_cached = info_to_cache
845 return self.info_cached
847 def get_thing_info(self, t):
849 (t.type_, self.game.thing_types[t.type_])
850 if hasattr(t, 'thing_char'):
852 if hasattr(t, 'name'):
853 info += ' (%s)' % t.name
854 if hasattr(t, 'installed'):
855 info += ' / installed'
858 def loop(self, stdscr):
861 def safe_addstr(y, x, line):
862 if y < self.size.y - 1 or x + len(line) < self.size.x:
863 stdscr.addstr(y, x, line, curses.color_pair(1))
864 else: # workaround to <https://stackoverflow.com/q/7063128>
865 cut_i = self.size.x - x - 1
867 last_char = line[cut_i]
868 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
869 stdscr.insstr(y, self.size.x - 2, ' ')
870 stdscr.addstr(y, x, cut, curses.color_pair(1))
872 def handle_input(msg):
873 command, args = self.parser.parse(msg)
876 def task_action_on(action):
877 return action_tasks[action] in self.game.tasks
879 def msg_into_lines_of_width(msg, width):
883 for i in range(len(msg)):
884 if x >= width or msg[i] == "\n":
896 def reset_screen_size():
897 self.size = YX(*stdscr.getmaxyx())
898 self.size = self.size - YX(self.size.y % 4, 0)
899 self.size = self.size - YX(0, self.size.x % 4)
900 self.window_width = int(self.size.x / 2)
902 def recalc_input_lines():
903 if not self.mode.has_input_prompt:
904 self.input_lines = []
906 self.input_lines = msg_into_lines_of_width(input_prompt
910 def move_explorer(direction):
911 target = self.game.map_geometry.move_yx(self.explorer, direction)
913 self.info_cached = None
914 self.explorer = target
916 self.send_tile_control_command()
922 for line in self.log:
923 lines += msg_into_lines_of_width(line, self.window_width)
926 max_y = self.size.y - len(self.input_lines)
927 for i in range(len(lines)):
928 if (i >= max_y - height_header):
930 safe_addstr(max_y - i - 1, self.window_width, lines[i])
933 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
934 lines = msg_into_lines_of_width(info, self.window_width)
936 for i in range(len(lines)):
937 y = height_header + i
938 if y >= self.size.y - len(self.input_lines):
940 safe_addstr(y, self.window_width, lines[i])
943 y = self.size.y - len(self.input_lines)
944 for i in range(len(self.input_lines)):
945 safe_addstr(y, self.window_width, self.input_lines[i])
949 stats = 'WEARY: %s BLADDER: %s' % (self.game.weariness,
950 self.game.bladder_pressure)
951 safe_addstr(0, self.window_width, stats)
954 help = "hit [%s] for help" % self.keys['help']
955 if self.mode.has_input_prompt:
956 help = "enter /help for help"
957 safe_addstr(1, self.window_width,
958 'MODE: %s – %s' % (self.mode.short_desc, help))
961 if (not self.game.turn_complete) and len(self.map_lines) == 0:
963 if self.game.turn_complete:
965 for y in range(self.game.map_geometry.size.y):
966 start = self.game.map_geometry.size.x * y
967 end = start + self.game.map_geometry.size.x
968 if self.map_mode == 'protections':
969 map_lines_split += [[c + ' ' for c
970 in self.game.map_control_content[start:end]]]
972 map_lines_split += [[c + ' ' for c
973 in self.game.map_content[start:end]]]
974 if self.map_mode == 'terrain + annotations':
975 for p in self.game.annotations:
976 map_lines_split[p.y][p.x] = 'A '
977 elif self.map_mode == 'terrain + things':
978 for p in self.game.portals.keys():
979 original = map_lines_split[p.y][p.x]
980 map_lines_split[p.y][p.x] = original[0] + 'P'
983 def draw_thing(t, used_positions):
984 symbol = self.game.thing_types[t.type_]
986 if hasattr(t, 'thing_char'):
987 meta_char = t.thing_char
988 if t.position in used_positions:
990 if hasattr(t, 'carrying') and t.carrying:
992 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
993 used_positions += [t.position]
995 for t in [t for t in self.game.things if t.type_ != 'Player']:
996 draw_thing(t, used_positions)
997 for t in [t for t in self.game.things if t.type_ == 'Player']:
998 draw_thing(t, used_positions)
999 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1000 map_lines_split[self.explorer.y][self.explorer.x] = '??'
1001 elif self.map_mode != 'terrain + things':
1002 map_lines_split[self.game.player.position.y]\
1003 [self.game.player.position.x] = '??'
1005 if type(self.game.map_geometry) == MapGeometryHex:
1007 for line in map_lines_split:
1008 self.map_lines += [indent * ' ' + ''.join(line)]
1009 indent = 0 if indent else 1
1011 for line in map_lines_split:
1012 self.map_lines += [''.join(line)]
1013 window_center = YX(int(self.size.y / 2),
1014 int(self.window_width / 2))
1015 center = self.game.player.position
1016 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1017 center = self.explorer
1018 center = YX(center.y, center.x * 2)
1019 self.offset = center - window_center
1020 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1021 self.offset += YX(0, 1)
1022 term_y = max(0, -self.offset.y)
1023 term_x = max(0, -self.offset.x)
1024 map_y = max(0, self.offset.y)
1025 map_x = max(0, self.offset.x)
1026 while term_y < self.size.y and map_y < len(self.map_lines):
1027 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1028 safe_addstr(term_y, term_x, to_draw)
1032 def draw_face_popup():
1033 t = self.game.get_thing(self.draw_face)
1034 if not t or not hasattr(t, 'face'):
1035 self.draw_face = False
1038 start_x = self.window_width - 10
1040 if hasattr(t, 'thing_char'):
1041 t_char = t.thing_char
1042 def draw_body_part(body_part, end_y):
1043 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1044 safe_addstr(end_y - 3, start_x, '| |')
1045 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1046 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1047 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1049 if hasattr(t, 'face'):
1050 draw_body_part(t.face, self.size.y - 2)
1051 if hasattr(t, 'hat'):
1052 draw_body_part(t.hat, self.size.y - 5)
1053 safe_addstr(self.size.y - 1, start_x, '| |')
1056 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1057 self.mode.help_intro)
1058 if len(self.mode.available_actions) > 0:
1059 content += "Available actions:\n"
1060 for action in self.mode.available_actions:
1061 if action in action_tasks:
1062 if action_tasks[action] not in self.game.tasks:
1064 if action == 'move_explorer':
1066 if action == 'move':
1067 key = ','.join(self.movement_keys)
1069 key = self.keys[action]
1070 content += '[%s] – %s\n' % (key, action_descriptions[action])
1072 content += self.mode.list_available_modes(self)
1073 for i in range(self.size.y):
1075 self.window_width * (not self.mode.has_input_prompt),
1076 ' ' * self.window_width)
1078 for line in content.split('\n'):
1079 lines += msg_into_lines_of_width(line, self.window_width)
1080 for i in range(len(lines)):
1081 if i >= self.size.y:
1084 self.window_width * (not self.mode.has_input_prompt),
1089 stdscr.bkgd(' ', curses.color_pair(1))
1090 recalc_input_lines()
1091 if self.mode.has_input_prompt:
1093 if self.mode.shows_info:
1098 if not self.mode.is_intro:
1103 if self.draw_face and self.mode.name in {'chat', 'play'}:
1106 def pick_selectable(task_name):
1108 i = int(self.input_)
1109 if i < 0 or i >= len(self.selectables):
1110 self.log_msg('? invalid index, aborted')
1112 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1114 self.log_msg('? invalid index, aborted')
1116 self.switch_mode('play')
1118 def enter_ascii_art(command):
1119 if len(self.input_) != 6:
1120 self.log_msg('? wrong input length, must be 6; try again')
1122 self.log_msg(' ' + self.input_)
1123 self.full_ascii_draw += self.input_
1124 self.ascii_draw_stage += 1
1125 if self.ascii_draw_stage < 3:
1126 self.restore_input_values()
1128 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1129 self.full_ascii_draw = ""
1130 self.ascii_draw_stage = 0
1132 self.switch_mode('edit')
1134 action_descriptions = {
1136 'flatten': 'flatten surroundings',
1137 'teleport': 'teleport',
1138 'take_thing': 'pick up thing',
1139 'drop_thing': 'drop thing',
1140 'toggle_map_mode': 'toggle map view',
1141 'toggle_tile_draw': 'toggle protection character drawing',
1142 'install': '(un-)install',
1143 'wear': '(un-)wear',
1144 'door': 'open/close',
1145 'consume': 'consume',
1150 'flatten': 'FLATTEN_SURROUNDINGS',
1151 'take_thing': 'PICK_UP',
1152 'drop_thing': 'DROP',
1154 'install': 'INSTALL',
1157 'command': 'COMMAND',
1158 'consume': 'INTOXICATE',
1162 curses.curs_set(False) # hide cursor
1163 curses.start_color()
1164 self.set_default_colors()
1165 curses.init_pair(1, 1, 2)
1168 self.explorer = YX(0, 0)
1171 interval = datetime.timedelta(seconds=5)
1172 last_ping = datetime.datetime.now() - interval
1174 if self.disconnected and self.force_instant_connect:
1175 self.force_instant_connect = False
1177 now = datetime.datetime.now()
1178 if now - last_ping > interval:
1179 if self.disconnected:
1189 self.do_refresh = False
1192 msg = self.queue.get(block=False)
1197 key = stdscr.getkey()
1198 self.do_refresh = True
1199 except curses.error:
1204 self.show_help = False
1205 self.draw_face = False
1206 if key == 'KEY_RESIZE':
1208 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1209 self.input_ = self.input_[:-1]
1210 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1211 or (self.mode.has_input_prompt and key == '\n'
1212 and self.input_ == ''\
1213 and self.mode.name in {'chat', 'command_thing',
1214 'take_thing', 'drop_thing',
1216 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1217 self.log_msg('@ aborted')
1218 self.switch_mode('play')
1219 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1220 self.show_help = True
1222 self.restore_input_values()
1223 elif self.mode.has_input_prompt and key != '\n': # Return key
1225 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1226 if len(self.input_) > max_length:
1227 self.input_ = self.input_[:max_length]
1228 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1229 self.show_help = True
1230 elif self.mode.name == 'login' and key == '\n':
1231 self.login_name = self.input_
1232 self.send('LOGIN ' + quote(self.input_))
1234 elif self.mode.name == 'enter_face' and key == '\n':
1235 enter_ascii_art('PLAYER_FACE')
1236 elif self.mode.name == 'enter_hat' and key == '\n':
1237 enter_ascii_art('PLAYER_HAT')
1238 elif self.mode.name == 'take_thing' and key == '\n':
1239 pick_selectable('PICK_UP')
1240 elif self.mode.name == 'drop_thing' and key == '\n':
1241 pick_selectable('DROP')
1242 elif self.mode.name == 'command_thing' and key == '\n':
1243 self.send('TASK:COMMAND ' + quote(self.input_))
1245 elif self.mode.name == 'control_pw_pw' and key == '\n':
1246 if self.input_ == '':
1247 self.log_msg('@ aborted')
1249 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1250 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1251 self.switch_mode('admin')
1252 elif self.mode.name == 'password' and key == '\n':
1253 if self.input_ == '':
1255 self.password = self.input_
1256 self.switch_mode('edit')
1257 elif self.mode.name == 'admin_enter' and key == '\n':
1258 self.send('BECOME_ADMIN ' + quote(self.input_))
1259 self.switch_mode('play')
1260 elif self.mode.name == 'control_pw_type' and key == '\n':
1261 if len(self.input_) != 1:
1262 self.log_msg('@ entered non-single-char, therefore aborted')
1263 self.switch_mode('admin')
1265 self.tile_control_char = self.input_
1266 self.switch_mode('control_pw_pw')
1267 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1268 if len(self.input_) != 1:
1269 self.log_msg('@ entered non-single-char, therefore aborted')
1271 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1272 self.log_msg('@ sent new protection character for thing')
1273 self.switch_mode('admin')
1274 elif self.mode.name == 'control_tile_type' and key == '\n':
1275 if len(self.input_) != 1:
1276 self.log_msg('@ entered non-single-char, therefore aborted')
1277 self.switch_mode('admin')
1279 self.tile_control_char = self.input_
1280 self.switch_mode('control_tile_draw')
1281 elif self.mode.name == 'chat' and key == '\n':
1282 if self.input_ == '':
1284 if self.input_[0] == '/':
1285 if self.input_.startswith('/nick'):
1286 tokens = self.input_.split(maxsplit=1)
1287 if len(tokens) == 2:
1288 self.send('NICK ' + quote(tokens[1]))
1290 self.log_msg('? need login name')
1292 self.log_msg('? unknown command')
1294 self.send('ALL ' + quote(self.input_))
1296 elif self.mode.name == 'name_thing' and key == '\n':
1297 if self.input_ == '':
1299 self.send('THING_NAME %s %s' % (quote(self.input_),
1300 quote(self.password)))
1301 self.switch_mode('edit')
1302 elif self.mode.name == 'annotate' and key == '\n':
1303 if self.input_ == '':
1305 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1306 quote(self.password)))
1307 self.switch_mode('edit')
1308 elif self.mode.name == 'portal' and key == '\n':
1309 if self.input_ == '':
1311 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1312 quote(self.password)))
1313 self.switch_mode('edit')
1314 elif self.mode.name == 'study':
1315 if self.mode.mode_switch_on_key(self, key):
1317 elif key == self.keys['toggle_map_mode']:
1318 self.toggle_map_mode()
1319 elif key in self.movement_keys:
1320 move_explorer(self.movement_keys[key])
1321 elif self.mode.name == 'play':
1322 if self.mode.mode_switch_on_key(self, key):
1324 elif key == self.keys['door'] and task_action_on('door'):
1325 self.send('TASK:DOOR')
1326 elif key == self.keys['consume'] and task_action_on('consume'):
1327 self.send('TASK:INTOXICATE')
1328 elif key == self.keys['wear'] and task_action_on('wear'):
1329 self.send('TASK:WEAR')
1330 elif key == self.keys['spin'] and task_action_on('spin'):
1331 self.send('TASK:SPIN')
1332 elif key == self.keys['teleport']:
1333 if self.game.player.position in self.game.portals:
1334 self.host = self.game.portals[self.game.player.position]
1338 self.log_msg('? not standing on portal')
1339 elif key in self.movement_keys and task_action_on('move'):
1340 self.send('TASK:MOVE ' + self.movement_keys[key])
1341 elif self.mode.name == 'write':
1342 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1343 self.switch_mode('edit')
1344 elif self.mode.name == 'control_tile_draw':
1345 if self.mode.mode_switch_on_key(self, key):
1347 elif key in self.movement_keys:
1348 move_explorer(self.movement_keys[key])
1349 elif key == self.keys['toggle_tile_draw']:
1350 self.tile_draw = False if self.tile_draw else True
1351 elif self.mode.name == 'admin':
1352 if self.mode.mode_switch_on_key(self, key):
1354 elif key in self.movement_keys and task_action_on('move'):
1355 self.send('TASK:MOVE ' + self.movement_keys[key])
1356 elif self.mode.name == 'edit':
1357 if self.mode.mode_switch_on_key(self, key):
1359 elif key == self.keys['flatten'] and task_action_on('flatten'):
1360 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1361 elif key == self.keys['install'] and task_action_on('install'):
1362 self.send('TASK:INSTALL %s' % quote(self.password))
1363 elif key == self.keys['toggle_map_mode']:
1364 self.toggle_map_mode()
1365 elif key in self.movement_keys and task_action_on('move'):
1366 self.send('TASK:MOVE ' + self.movement_keys[key])
1368 if len(sys.argv) != 2:
1369 raise ArgError('wrong number of arguments, need game host')