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.energy = game.energy_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, energy):
368 game.bladder_pressure_new = bladder_pressure
369 game.energy_new = energy
370 cmd_STATS.argtypes = 'int:nonneg int'
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", "dance"]
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", "toggle_map_mode"]
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',
571 'toggle_map_mode': 'L',
572 'toggle_tile_draw': 'm',
573 'hex_move_upleft': 'w',
574 'hex_move_upright': 'e',
575 'hex_move_right': 'd',
576 'hex_move_downright': 'x',
577 'hex_move_downleft': 'y',
578 'hex_move_left': 'a',
579 'square_move_up': 'w',
580 'square_move_left': 'a',
581 'square_move_down': 's',
582 'square_move_right': 'd',
584 if os.path.isfile('config.json'):
585 with open('config.json', 'r') as f:
586 keys_conf = json.loads(f.read())
588 self.keys[k] = keys_conf[k]
589 self.show_help = False
590 self.disconnected = True
591 self.force_instant_connect = True
592 self.input_lines = []
596 self.ascii_draw_stage = 0
597 self.full_ascii_draw = ''
598 self.offset = YX(0,0)
599 curses.wrapper(self.loop)
603 def handle_recv(msg):
609 self.log_msg('@ attempting connect')
610 socket_client_class = PlomSocketClient
611 if self.host.startswith('ws://') or self.host.startswith('wss://'):
612 socket_client_class = WebSocketClient
614 self.socket = socket_client_class(handle_recv, self.host)
615 self.socket_thread = threading.Thread(target=self.socket.run)
616 self.socket_thread.start()
617 self.disconnected = False
618 self.game.thing_types = {}
619 self.game.terrains = {}
620 time.sleep(0.1) # give potential SSL negotation some time …
621 self.socket.send('TASKS')
622 self.socket.send('TERRAINS')
623 self.socket.send('THING_TYPES')
624 self.switch_mode('login')
625 except ConnectionRefusedError:
626 self.log_msg('@ server connect failure')
627 self.disconnected = True
628 self.switch_mode('waiting_for_server')
629 self.do_refresh = True
632 self.log_msg('@ attempting reconnect')
634 # necessitated by some strange SSL race conditions with ws4py
635 time.sleep(0.1) # FIXME find out why exactly necessary
636 self.switch_mode('waiting_for_server')
641 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
642 raise BrokenSocketConnection
643 self.socket.send(msg)
644 except (BrokenPipeError, BrokenSocketConnection):
645 self.log_msg('@ server disconnected :(')
646 self.disconnected = True
647 self.force_instant_connect = True
648 self.do_refresh = True
650 def log_msg(self, msg):
652 if len(self.log) > 100:
653 self.log = self.log[-100:]
655 def restore_input_values(self):
656 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
657 self.input_ = self.game.annotations[self.explorer]
658 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
659 self.input_ = self.game.portals[self.explorer]
660 elif self.mode.name == 'password':
661 self.input_ = self.password
662 elif self.mode.name == 'name_thing':
663 if hasattr(self.game.player.carrying, 'name'):
664 self.input_ = self.game.player.carrying.name
665 elif self.mode.name == 'admin_thing_protect':
666 if hasattr(self.game.player.carrying, 'protection'):
667 self.input_ = self.game.player.carrying.protection
668 elif self.mode.name in {'enter_face', 'enter_hat'}:
669 start = self.ascii_draw_stage * 6
670 end = (self.ascii_draw_stage + 1) * 6
671 if self.mode.name == 'enter_face':
672 self.input_ = self.game.player.face[start:end]
673 elif self.mode.name == 'enter_hat':
674 self.input_ = self.game.player.hat[start:end]
676 def send_tile_control_command(self):
677 self.send('SET_TILE_CONTROL %s %s' %
678 (self.explorer, quote(self.tile_control_char)))
680 def toggle_map_mode(self):
681 if self.map_mode == 'terrain only':
682 self.map_mode = 'terrain + annotations'
683 elif self.map_mode == 'terrain + annotations':
684 self.map_mode = 'terrain + things'
685 elif self.map_mode == 'terrain + things':
686 self.map_mode = 'protections'
687 elif self.map_mode == 'protections':
688 self.map_mode = 'terrain only'
690 def switch_mode(self, mode_name):
692 def fail(msg, return_mode='play'):
693 self.log_msg('? ' + msg)
695 self.switch_mode(return_mode)
697 if self.mode and self.mode.name == 'control_tile_draw':
698 self.log_msg('@ finished tile protection drawing.')
699 self.draw_face = False
700 self.tile_draw = False
701 if mode_name == 'command_thing' and\
702 (not self.game.player.carrying or
703 not self.game.player.carrying.commandable):
704 return fail('not carrying anything commandable')
705 if mode_name == 'name_thing' and not self.game.player.carrying:
706 return fail('not carrying anything to re-name', 'edit')
707 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
708 return fail('not carrying anything to protect')
709 if mode_name == 'take_thing' and self.game.player.carrying:
710 return fail('already carrying something')
711 if mode_name == 'drop_thing' and not self.game.player.carrying:
712 return fail('not carrying anything droppable')
713 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
714 return fail('not wearing hat to edit', 'edit')
715 if mode_name == 'admin_enter' and self.is_admin:
717 self.mode = getattr(self, 'mode_' + mode_name)
718 if self.mode.name in {'control_tile_draw', 'control_tile_type',
720 self.map_mode = 'protections'
721 elif self.mode.name != 'edit':
722 self.map_mode = 'terrain + things'
723 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
724 self.explorer = YX(self.game.player.position.y,
725 self.game.player.position.x)
726 if self.mode.is_single_char_entry:
727 self.show_help = True
728 if len(self.mode.intro_msg) > 0:
729 self.log_msg(self.mode.intro_msg)
730 if self.mode.name == 'login':
732 self.send('LOGIN ' + quote(self.login_name))
734 self.log_msg('@ enter username')
735 elif self.mode.name == 'take_thing':
736 self.log_msg('Portable things in reach for pick-up:')
738 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
740 if type(self.game.map_geometry) == MapGeometrySquare:
741 directed_moves['UP'] = YX(-1, 0)
742 directed_moves['DOWN'] = YX(1, 0)
743 elif type(self.game.map_geometry) == MapGeometryHex:
744 if self.game.player.position.y % 2:
745 directed_moves['UPLEFT'] = YX(-1, 0)
746 directed_moves['UPRIGHT'] = YX(-1, 1)
747 directed_moves['DOWNLEFT'] = YX(1, 0)
748 directed_moves['DOWNRIGHT'] = YX(1, 1)
750 directed_moves['UPLEFT'] = YX(-1, -1)
751 directed_moves['UPRIGHT'] = YX(-1, 0)
752 directed_moves['DOWNLEFT'] = YX(1, -1)
753 directed_moves['DOWNRIGHT'] = YX(1, 0)
755 for direction in directed_moves:
756 move = directed_moves[direction]
757 select_range[direction] = self.game.player.position + move
758 self.selectables = []
760 for direction in select_range:
761 for t in [t for t in self.game.things
762 if t.portable and t.position == select_range[direction]]:
763 self.selectables += [t.id_]
764 directions += [direction]
765 if len(self.selectables) == 0:
766 return fail('nothing to pick-up')
768 for i in range(len(self.selectables)):
769 t = self.game.get_thing(self.selectables[i])
770 self.log_msg('%s %s: %s' % (i, directions[i],
771 self.get_thing_info(t)))
772 elif self.mode.name == 'drop_thing':
773 self.log_msg('Direction to drop thing to:')
775 ['HERE'] + list(self.game.tui.movement_keys.values())
776 for i in range(len(self.selectables)):
777 self.log_msg(str(i) + ': ' + self.selectables[i])
778 elif self.mode.name == 'enter_hat':
779 self.log_msg('legal characters: ' + self.game.players_hat_chars)
780 elif self.mode.name == 'command_thing':
781 self.send('TASK:COMMAND ' + quote('HELP'))
782 elif self.mode.name == 'control_pw_pw':
783 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
784 elif self.mode.name == 'control_tile_draw':
785 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']))
787 self.restore_input_values()
789 def set_default_colors(self):
790 curses.init_color(1, 1000, 1000, 1000)
791 curses.init_color(2, 0, 0, 0)
792 self.do_refresh = True
794 def set_random_colors(self):
798 return int(offset + random.random()*375)
800 curses.init_color(1, rand(625), rand(625), rand(625))
801 curses.init_color(2, rand(0), rand(0), rand(0))
802 self.do_refresh = True
806 return self.info_cached
807 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
809 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
810 info_to_cache += 'outside field of view'
812 for t in self.game.things:
813 if t.position == self.explorer:
814 info_to_cache += 'THING: %s' % self.get_thing_info(t)
815 protection = t.protection
816 if protection == '.':
818 info_to_cache += ' / protection: %s\n' % protection
819 if hasattr(t, 'hat'):
820 info_to_cache += t.hat[0:6] + '\n'
821 info_to_cache += t.hat[6:12] + '\n'
822 info_to_cache += t.hat[12:18] + '\n'
823 if hasattr(t, 'face'):
824 info_to_cache += t.face[0:6] + '\n'
825 info_to_cache += t.face[6:12] + '\n'
826 info_to_cache += t.face[12:18] + '\n'
827 terrain_char = self.game.map_content[pos_i]
829 if terrain_char in self.game.terrains:
830 terrain_desc = self.game.terrains[terrain_char]
831 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
833 protection = self.game.map_control_content[pos_i]
834 if protection == '.':
835 protection = 'unprotected'
836 info_to_cache += 'PROTECTION: %s\n' % protection
837 if self.explorer in self.game.portals:
838 info_to_cache += 'PORTAL: ' +\
839 self.game.portals[self.explorer] + '\n'
841 info_to_cache += 'PORTAL: (none)\n'
842 if self.explorer in self.game.annotations:
843 info_to_cache += 'ANNOTATION: ' +\
844 self.game.annotations[self.explorer]
845 self.info_cached = info_to_cache
846 return self.info_cached
848 def get_thing_info(self, t):
850 (t.type_, self.game.thing_types[t.type_])
851 if hasattr(t, 'thing_char'):
853 if hasattr(t, 'name'):
854 info += ' (%s)' % t.name
855 if hasattr(t, 'installed'):
856 info += ' / installed'
859 def loop(self, stdscr):
862 def safe_addstr(y, x, line):
863 if y < self.size.y - 1 or x + len(line) < self.size.x:
864 stdscr.addstr(y, x, line, curses.color_pair(1))
865 else: # workaround to <https://stackoverflow.com/q/7063128>
866 cut_i = self.size.x - x - 1
868 last_char = line[cut_i]
869 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
870 stdscr.insstr(y, self.size.x - 2, ' ')
871 stdscr.addstr(y, x, cut, curses.color_pair(1))
873 def handle_input(msg):
874 command, args = self.parser.parse(msg)
877 def task_action_on(action):
878 return action_tasks[action] in self.game.tasks
880 def msg_into_lines_of_width(msg, width):
884 for i in range(len(msg)):
885 if x >= width or msg[i] == "\n":
897 def reset_screen_size():
898 self.size = YX(*stdscr.getmaxyx())
899 self.size = self.size - YX(self.size.y % 4, 0)
900 self.size = self.size - YX(0, self.size.x % 4)
901 self.window_width = int(self.size.x / 2)
903 def recalc_input_lines():
904 if not self.mode.has_input_prompt:
905 self.input_lines = []
907 self.input_lines = msg_into_lines_of_width(input_prompt
911 def move_explorer(direction):
912 target = self.game.map_geometry.move_yx(self.explorer, direction)
914 self.info_cached = None
915 self.explorer = target
917 self.send_tile_control_command()
923 for line in self.log:
924 lines += msg_into_lines_of_width(line, self.window_width)
927 max_y = self.size.y - len(self.input_lines)
928 for i in range(len(lines)):
929 if (i >= max_y - height_header):
931 safe_addstr(max_y - i - 1, self.window_width, lines[i])
934 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
935 lines = msg_into_lines_of_width(info, self.window_width)
937 for i in range(len(lines)):
938 y = height_header + i
939 if y >= self.size.y - len(self.input_lines):
941 safe_addstr(y, self.window_width, lines[i])
944 y = self.size.y - len(self.input_lines)
945 for i in range(len(self.input_lines)):
946 safe_addstr(y, self.window_width, self.input_lines[i])
950 stats = 'ENERGY: %s BLADDER: %s' % (self.game.energy,
951 self.game.bladder_pressure)
952 safe_addstr(0, self.window_width, stats)
955 help = "hit [%s] for help" % self.keys['help']
956 if self.mode.has_input_prompt:
957 help = "enter /help for help"
958 safe_addstr(1, self.window_width,
959 'MODE: %s – %s' % (self.mode.short_desc, help))
962 if (not self.game.turn_complete) and len(self.map_lines) == 0:
964 if self.game.turn_complete:
966 for y in range(self.game.map_geometry.size.y):
967 start = self.game.map_geometry.size.x * y
968 end = start + self.game.map_geometry.size.x
969 if self.map_mode == 'protections':
970 map_lines_split += [[c + ' ' for c
971 in self.game.map_control_content[start:end]]]
973 map_lines_split += [[c + ' ' for c
974 in self.game.map_content[start:end]]]
975 if self.map_mode == 'terrain + annotations':
976 for p in self.game.annotations:
977 map_lines_split[p.y][p.x] = 'A '
978 elif self.map_mode == 'terrain + things':
979 for p in self.game.portals.keys():
980 original = map_lines_split[p.y][p.x]
981 map_lines_split[p.y][p.x] = original[0] + 'P'
984 def draw_thing(t, used_positions):
985 symbol = self.game.thing_types[t.type_]
987 if hasattr(t, 'thing_char'):
988 meta_char = t.thing_char
989 if t.position in used_positions:
991 if hasattr(t, 'carrying') and t.carrying:
993 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
994 used_positions += [t.position]
996 for t in [t for t in self.game.things if t.type_ != 'Player']:
997 draw_thing(t, used_positions)
998 for t in [t for t in self.game.things if t.type_ == 'Player']:
999 draw_thing(t, used_positions)
1000 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1001 map_lines_split[self.explorer.y][self.explorer.x] = '??'
1002 elif self.map_mode != 'terrain + things':
1003 map_lines_split[self.game.player.position.y]\
1004 [self.game.player.position.x] = '??'
1006 if type(self.game.map_geometry) == MapGeometryHex:
1008 for line in map_lines_split:
1009 self.map_lines += [indent * ' ' + ''.join(line)]
1010 indent = 0 if indent else 1
1012 for line in map_lines_split:
1013 self.map_lines += [''.join(line)]
1014 window_center = YX(int(self.size.y / 2),
1015 int(self.window_width / 2))
1016 center = self.game.player.position
1017 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1018 center = self.explorer
1019 center = YX(center.y, center.x * 2)
1020 self.offset = center - window_center
1021 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1022 self.offset += YX(0, 1)
1023 term_y = max(0, -self.offset.y)
1024 term_x = max(0, -self.offset.x)
1025 map_y = max(0, self.offset.y)
1026 map_x = max(0, self.offset.x)
1027 while term_y < self.size.y and map_y < len(self.map_lines):
1028 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1029 safe_addstr(term_y, term_x, to_draw)
1033 def draw_face_popup():
1034 t = self.game.get_thing(self.draw_face)
1035 if not t or not hasattr(t, 'face'):
1036 self.draw_face = False
1039 start_x = self.window_width - 10
1041 if hasattr(t, 'thing_char'):
1042 t_char = t.thing_char
1043 def draw_body_part(body_part, end_y):
1044 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1045 safe_addstr(end_y - 3, start_x, '| |')
1046 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1047 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1048 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1050 if hasattr(t, 'face'):
1051 draw_body_part(t.face, self.size.y - 2)
1052 if hasattr(t, 'hat'):
1053 draw_body_part(t.hat, self.size.y - 5)
1054 safe_addstr(self.size.y - 1, start_x, '| |')
1057 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1058 self.mode.help_intro)
1059 if len(self.mode.available_actions) > 0:
1060 content += "Available actions:\n"
1061 for action in self.mode.available_actions:
1062 if action in action_tasks:
1063 if action_tasks[action] not in self.game.tasks:
1065 if action == 'move_explorer':
1067 if action == 'move':
1068 key = ','.join(self.movement_keys)
1070 key = self.keys[action]
1071 content += '[%s] – %s\n' % (key, action_descriptions[action])
1073 content += self.mode.list_available_modes(self)
1074 for i in range(self.size.y):
1076 self.window_width * (not self.mode.has_input_prompt),
1077 ' ' * self.window_width)
1079 for line in content.split('\n'):
1080 lines += msg_into_lines_of_width(line, self.window_width)
1081 for i in range(len(lines)):
1082 if i >= self.size.y:
1085 self.window_width * (not self.mode.has_input_prompt),
1090 stdscr.bkgd(' ', curses.color_pair(1))
1091 recalc_input_lines()
1092 if self.mode.has_input_prompt:
1094 if self.mode.shows_info:
1099 if not self.mode.is_intro:
1104 if self.draw_face and self.mode.name in {'chat', 'play'}:
1107 def pick_selectable(task_name):
1109 i = int(self.input_)
1110 if i < 0 or i >= len(self.selectables):
1111 self.log_msg('? invalid index, aborted')
1113 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1115 self.log_msg('? invalid index, aborted')
1117 self.switch_mode('play')
1119 def enter_ascii_art(command):
1120 if len(self.input_) != 6:
1121 self.log_msg('? wrong input length, must be 6; try again')
1123 self.log_msg(' ' + self.input_)
1124 self.full_ascii_draw += self.input_
1125 self.ascii_draw_stage += 1
1126 if self.ascii_draw_stage < 3:
1127 self.restore_input_values()
1129 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1130 self.full_ascii_draw = ""
1131 self.ascii_draw_stage = 0
1133 self.switch_mode('edit')
1135 action_descriptions = {
1137 'flatten': 'flatten surroundings',
1138 'teleport': 'teleport',
1139 'take_thing': 'pick up thing',
1140 'drop_thing': 'drop thing',
1141 'toggle_map_mode': 'toggle map view',
1142 'toggle_tile_draw': 'toggle protection character drawing',
1143 'install': '(un-)install',
1144 'wear': '(un-)wear',
1145 'door': 'open/close',
1146 'consume': 'consume',
1152 'flatten': 'FLATTEN_SURROUNDINGS',
1153 'take_thing': 'PICK_UP',
1154 'drop_thing': 'DROP',
1156 'install': 'INSTALL',
1159 'command': 'COMMAND',
1160 'consume': 'INTOXICATE',
1165 curses.curs_set(False) # hide cursor
1166 curses.start_color()
1167 self.set_default_colors()
1168 curses.init_pair(1, 1, 2)
1171 self.explorer = YX(0, 0)
1173 store_widechar = False
1175 interval = datetime.timedelta(seconds=5)
1176 last_ping = datetime.datetime.now() - interval
1178 if self.disconnected and self.force_instant_connect:
1179 self.force_instant_connect = False
1181 now = datetime.datetime.now()
1182 if now - last_ping > interval:
1183 if self.disconnected:
1193 self.do_refresh = False
1196 msg = self.queue.get(block=False)
1201 key = stdscr.getkey()
1202 self.do_refresh = True
1203 except curses.error:
1208 # workaround for <https://stackoverflow.com/a/56390915>
1210 store_widechar = False
1211 key = bytes([195, keycode]).decode()
1213 store_widechar = True
1215 self.show_help = False
1216 self.draw_face = False
1217 if key == 'KEY_RESIZE':
1219 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1220 self.input_ = self.input_[:-1]
1221 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1222 or (self.mode.has_input_prompt and key == '\n'
1223 and self.input_ == ''\
1224 and self.mode.name in {'chat', 'command_thing',
1225 'take_thing', 'drop_thing',
1227 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1228 self.log_msg('@ aborted')
1229 self.switch_mode('play')
1230 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1231 self.show_help = True
1233 self.restore_input_values()
1234 elif self.mode.has_input_prompt and key != '\n': # Return key
1236 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1237 if len(self.input_) > max_length:
1238 self.input_ = self.input_[:max_length]
1239 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1240 self.show_help = True
1241 elif self.mode.name == 'login' and key == '\n':
1242 self.login_name = self.input_
1243 self.send('LOGIN ' + quote(self.input_))
1245 elif self.mode.name == 'enter_face' and key == '\n':
1246 enter_ascii_art('PLAYER_FACE')
1247 elif self.mode.name == 'enter_hat' and key == '\n':
1248 enter_ascii_art('PLAYER_HAT')
1249 elif self.mode.name == 'take_thing' and key == '\n':
1250 pick_selectable('PICK_UP')
1251 elif self.mode.name == 'drop_thing' and key == '\n':
1252 pick_selectable('DROP')
1253 elif self.mode.name == 'command_thing' and key == '\n':
1254 self.send('TASK:COMMAND ' + quote(self.input_))
1256 elif self.mode.name == 'control_pw_pw' and key == '\n':
1257 if self.input_ == '':
1258 self.log_msg('@ aborted')
1260 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1261 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1262 self.switch_mode('admin')
1263 elif self.mode.name == 'password' and key == '\n':
1264 if self.input_ == '':
1266 self.password = self.input_
1267 self.switch_mode('edit')
1268 elif self.mode.name == 'admin_enter' and key == '\n':
1269 self.send('BECOME_ADMIN ' + quote(self.input_))
1270 self.switch_mode('play')
1271 elif self.mode.name == 'control_pw_type' and key == '\n':
1272 if len(self.input_) != 1:
1273 self.log_msg('@ entered non-single-char, therefore aborted')
1274 self.switch_mode('admin')
1276 self.tile_control_char = self.input_
1277 self.switch_mode('control_pw_pw')
1278 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1279 if len(self.input_) != 1:
1280 self.log_msg('@ entered non-single-char, therefore aborted')
1282 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1283 self.log_msg('@ sent new protection character for thing')
1284 self.switch_mode('admin')
1285 elif self.mode.name == 'control_tile_type' and key == '\n':
1286 if len(self.input_) != 1:
1287 self.log_msg('@ entered non-single-char, therefore aborted')
1288 self.switch_mode('admin')
1290 self.tile_control_char = self.input_
1291 self.switch_mode('control_tile_draw')
1292 elif self.mode.name == 'chat' and key == '\n':
1293 if self.input_ == '':
1295 if self.input_[0] == '/':
1296 if self.input_.startswith('/nick'):
1297 tokens = self.input_.split(maxsplit=1)
1298 if len(tokens) == 2:
1299 self.send('NICK ' + quote(tokens[1]))
1301 self.log_msg('? need login name')
1303 self.log_msg('? unknown command')
1305 self.send('ALL ' + quote(self.input_))
1307 elif self.mode.name == 'name_thing' and key == '\n':
1308 if self.input_ == '':
1310 self.send('THING_NAME %s %s' % (quote(self.input_),
1311 quote(self.password)))
1312 self.switch_mode('edit')
1313 elif self.mode.name == 'annotate' and key == '\n':
1314 if self.input_ == '':
1316 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1317 quote(self.password)))
1318 self.switch_mode('edit')
1319 elif self.mode.name == 'portal' and key == '\n':
1320 if self.input_ == '':
1322 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1323 quote(self.password)))
1324 self.switch_mode('edit')
1325 elif self.mode.name == 'study':
1326 if self.mode.mode_switch_on_key(self, key):
1328 elif key == self.keys['toggle_map_mode']:
1329 self.toggle_map_mode()
1330 elif key in self.movement_keys:
1331 move_explorer(self.movement_keys[key])
1332 elif self.mode.name == 'play':
1333 if self.mode.mode_switch_on_key(self, key):
1335 elif key == self.keys['door'] and task_action_on('door'):
1336 self.send('TASK:DOOR')
1337 elif key == self.keys['consume'] and task_action_on('consume'):
1338 self.send('TASK:INTOXICATE')
1339 elif key == self.keys['wear'] and task_action_on('wear'):
1340 self.send('TASK:WEAR')
1341 elif key == self.keys['spin'] and task_action_on('spin'):
1342 self.send('TASK:SPIN')
1343 elif key == self.keys['dance'] and task_action_on('dance'):
1344 self.send('TASK:DANCE')
1345 elif key == self.keys['teleport']:
1346 if self.game.player.position in self.game.portals:
1347 self.host = self.game.portals[self.game.player.position]
1351 self.log_msg('? not standing on portal')
1352 elif key in self.movement_keys and task_action_on('move'):
1353 self.send('TASK:MOVE ' + self.movement_keys[key])
1354 elif self.mode.name == 'write':
1355 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1356 self.switch_mode('edit')
1357 elif self.mode.name == 'control_tile_draw':
1358 if self.mode.mode_switch_on_key(self, key):
1360 elif key in self.movement_keys:
1361 move_explorer(self.movement_keys[key])
1362 elif key == self.keys['toggle_tile_draw']:
1363 self.tile_draw = False if self.tile_draw else True
1364 elif self.mode.name == 'admin':
1365 if self.mode.mode_switch_on_key(self, key):
1367 elif key == self.keys['toggle_map_mode']:
1368 self.toggle_map_mode()
1369 elif key in self.movement_keys and task_action_on('move'):
1370 self.send('TASK:MOVE ' + self.movement_keys[key])
1371 elif self.mode.name == 'edit':
1372 if self.mode.mode_switch_on_key(self, key):
1374 elif key == self.keys['flatten'] and task_action_on('flatten'):
1375 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1376 elif key == self.keys['install'] and task_action_on('install'):
1377 self.send('TASK:INSTALL %s' % quote(self.password))
1378 elif key == self.keys['toggle_map_mode']:
1379 self.toggle_map_mode()
1380 elif key in self.movement_keys and task_action_on('move'):
1381 self.send('TASK:MOVE ' + self.movement_keys[key])
1383 if len(sys.argv) != 2:
1384 raise ArgError('wrong number of arguments, need game host')