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 thing here.'
35 'short': 'command thing',
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
40 'short': 'take thing',
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.'
45 'short': 'drop thing',
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 thing here.'
55 'short': 'enter your face',
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..'
60 'short': 'change terrain',
62 '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.'
65 'short': 'change protection character password',
66 'intro': '@ enter protection character for which you want to change the password:',
67 '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.'
70 'short': 'change protection character password',
72 '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.'
74 'control_tile_type': {
75 'short': 'change tiles protection',
76 'intro': '@ enter protection character which you want to draw:',
77 '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.'
79 'control_tile_draw': {
80 'short': 'change tiles protection',
82 '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.'
85 'short': 'annotate tile',
87 '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.'
90 'short': 'edit portal',
92 '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.'
97 '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'
102 'long': 'Enter your player name.'
104 'waiting_for_server': {
105 'short': 'waiting for server response',
106 'intro': '@ waiting for server …',
107 'long': 'Waiting for a server response.'
110 'short': 'waiting for server response',
112 'long': 'Waiting for a server response.'
115 'short': 'set world edit password',
117 '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.'
120 'short': 'become admin',
121 'intro': '@ enter admin password:',
122 'long': 'This mode allows you to become admin if you know an admin password.'
127 'long': 'This mode allows you access to actions limited to administrators.'
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
134 def __init__(self, recv_handler, *args, **kwargs):
135 super().__init__(*args, **kwargs)
136 self.recv_handler = recv_handler
139 def received_message(self, message):
141 message = str(message)
142 self.recv_handler(message)
145 def plom_closed(self):
146 return self.client_terminated
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
151 def __init__(self, recv_handler, url):
153 self.recv_handler = recv_handler
154 host, port = url.split(':')
155 super().__init__(socket.create_connection((host, port)))
163 for msg in self.recv():
164 if msg == 'NEED_SSL':
165 self.socket = ssl.wrap_socket(self.socket)
167 self.recv_handler(msg)
168 except BrokenSocketConnection:
169 pass # we assume socket will be known as dead by now
171 def cmd_TURN(game, n):
172 game.annotations = {}
176 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_LOGIN_OK(game):
181 game.tui.switch_mode('post_login_wait')
182 game.tui.send('GET_GAMESTATE')
183 game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
186 def cmd_ADMIN_OK(game):
187 game.tui.is_admin = True
188 game.tui.log_msg('@ you now have admin rights')
189 game.tui.switch_mode('admin')
190 game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
193 def cmd_REPLY(game, msg):
194 game.tui.log_msg('#MUSICPLAYER: ' + msg)
195 game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
198 def cmd_CHAT(game, msg):
199 game.tui.log_msg('# ' + msg)
200 game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
203 def cmd_PLAYER_ID(game, player_id):
204 game.player_id = player_id
205 cmd_PLAYER_ID.argtypes = 'int:nonneg'
207 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
208 t = game.get_thing(thing_id)
210 t = ThingBase(game, thing_id)
214 t.protection = protection
215 t.portable = portable
216 t.commandable = commandable
217 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
219 def cmd_THING_NAME(game, thing_id, name):
220 t = game.get_thing(thing_id)
222 cmd_THING_NAME.argtypes = 'int:pos string'
224 def cmd_THING_FACE(game, thing_id, face):
225 t = game.get_thing(thing_id)
227 cmd_THING_FACE.argtypes = 'int:pos string'
229 def cmd_THING_HAT(game, thing_id, hat):
230 t = game.get_thing(thing_id)
232 cmd_THING_HAT.argtypes = 'int:pos string'
234 def cmd_THING_CHAR(game, thing_id, c):
235 t = game.get_thing(thing_id)
237 cmd_THING_CHAR.argtypes = 'int:pos char'
239 def cmd_MAP(game, geometry, size, content):
240 map_geometry_class = globals()['MapGeometry' + geometry]
241 game.map_geometry = map_geometry_class(size)
242 game.map_content = content
243 if type(game.map_geometry) == MapGeometrySquare:
244 game.tui.movement_keys = {
245 game.tui.keys['square_move_up']: 'UP',
246 game.tui.keys['square_move_left']: 'LEFT',
247 game.tui.keys['square_move_down']: 'DOWN',
248 game.tui.keys['square_move_right']: 'RIGHT',
250 elif type(game.map_geometry) == MapGeometryHex:
251 game.tui.movement_keys = {
252 game.tui.keys['hex_move_upleft']: 'UPLEFT',
253 game.tui.keys['hex_move_upright']: 'UPRIGHT',
254 game.tui.keys['hex_move_right']: 'RIGHT',
255 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
256 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
257 game.tui.keys['hex_move_left']: 'LEFT',
259 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
261 def cmd_FOV(game, content):
263 cmd_FOV.argtypes = 'string'
265 def cmd_MAP_CONTROL(game, content):
266 game.map_control_content = content
267 cmd_MAP_CONTROL.argtypes = 'string'
269 def cmd_GAME_STATE_COMPLETE(game):
270 if game.tui.mode.name == 'post_login_wait':
271 game.tui.switch_mode('play')
272 game.turn_complete = True
273 game.tui.do_refresh = True
274 game.tui.info_cached = None
275 cmd_GAME_STATE_COMPLETE.argtypes = ''
277 def cmd_PORTAL(game, position, msg):
278 game.portals[position] = msg
279 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
281 def cmd_PLAY_ERROR(game, msg):
282 game.tui.log_msg('? ' + msg)
283 game.tui.flash = True
284 game.tui.do_refresh = True
285 cmd_PLAY_ERROR.argtypes = 'string'
287 def cmd_GAME_ERROR(game, msg):
288 game.tui.log_msg('? game error: ' + msg)
289 game.tui.do_refresh = True
290 cmd_GAME_ERROR.argtypes = 'string'
292 def cmd_ARGUMENT_ERROR(game, msg):
293 game.tui.log_msg('? syntax error: ' + msg)
294 game.tui.do_refresh = True
295 cmd_ARGUMENT_ERROR.argtypes = 'string'
297 def cmd_ANNOTATION(game, position, msg):
298 game.annotations[position] = msg
299 if game.tui.mode.shows_info:
300 game.tui.do_refresh = True
301 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
303 def cmd_TASKS(game, tasks_comma_separated):
304 game.tasks = tasks_comma_separated.split(',')
305 game.tui.mode_write.legal = 'WRITE' in game.tasks
306 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
307 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
308 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
309 cmd_TASKS.argtypes = 'string'
311 def cmd_THING_TYPE(game, thing_type, symbol_hint):
312 game.thing_types[thing_type] = symbol_hint
313 cmd_THING_TYPE.argtypes = 'string char'
315 def cmd_THING_INSTALLED(game, thing_id):
316 game.get_thing(thing_id).installed = True
317 cmd_THING_INSTALLED.argtypes = 'int:pos'
319 def cmd_THING_CARRYING(game, thing_id, carried_id):
320 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
321 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
323 def cmd_TERRAIN(game, terrain_char, terrain_desc):
324 game.terrains[terrain_char] = terrain_desc
325 cmd_TERRAIN.argtypes = 'char string'
329 cmd_PONG.argtypes = ''
331 def cmd_DEFAULT_COLORS(game):
332 game.tui.set_default_colors()
333 cmd_DEFAULT_COLORS.argtypes = ''
335 def cmd_RANDOM_COLORS(game):
336 game.tui.set_random_colors()
337 cmd_RANDOM_COLORS.argtypes = ''
339 class Game(GameBase):
340 turn_complete = False
344 def __init__(self, *args, **kwargs):
345 super().__init__(*args, **kwargs)
346 self.register_command(cmd_LOGIN_OK)
347 self.register_command(cmd_ADMIN_OK)
348 self.register_command(cmd_PONG)
349 self.register_command(cmd_CHAT)
350 self.register_command(cmd_REPLY)
351 self.register_command(cmd_PLAYER_ID)
352 self.register_command(cmd_TURN)
353 self.register_command(cmd_THING)
354 self.register_command(cmd_THING_TYPE)
355 self.register_command(cmd_THING_NAME)
356 self.register_command(cmd_THING_CHAR)
357 self.register_command(cmd_THING_FACE)
358 self.register_command(cmd_THING_HAT)
359 self.register_command(cmd_THING_DESIGN)
360 self.register_command(cmd_THING_CARRYING)
361 self.register_command(cmd_THING_INSTALLED)
362 self.register_command(cmd_TERRAIN)
363 self.register_command(cmd_MAP)
364 self.register_command(cmd_MAP_CONTROL)
365 self.register_command(cmd_PORTAL)
366 self.register_command(cmd_ANNOTATION)
367 self.register_command(cmd_GAME_STATE_COMPLETE)
368 self.register_command(cmd_ARGUMENT_ERROR)
369 self.register_command(cmd_GAME_ERROR)
370 self.register_command(cmd_PLAY_ERROR)
371 self.register_command(cmd_TASKS)
372 self.register_command(cmd_FOV)
373 self.register_command(cmd_DEFAULT_COLORS)
374 self.register_command(cmd_RANDOM_COLORS)
375 self.map_content = ''
377 self.annotations = {}
381 def get_string_options(self, string_option_type):
382 if string_option_type == 'map_geometry':
383 return ['Hex', 'Square']
384 elif string_option_type == 'thing_type':
385 return self.thing_types.keys()
388 def get_command(self, command_name):
389 from functools import partial
390 f = partial(self.commands[command_name], self)
391 f.argtypes = self.commands[command_name].argtypes
396 def __init__(self, name, has_input_prompt=False, shows_info=False,
397 is_intro=False, is_single_char_entry=False):
399 self.short_desc = mode_helps[name]['short']
400 self.available_modes = []
401 self.available_actions = []
402 self.has_input_prompt = has_input_prompt
403 self.shows_info = shows_info
404 self.is_intro = is_intro
405 self.help_intro = mode_helps[name]['long']
406 self.intro_msg = mode_helps[name]['intro']
407 self.is_single_char_entry = is_single_char_entry
410 def iter_available_modes(self, tui):
411 for mode_name in self.available_modes:
412 mode = getattr(tui, 'mode_' + mode_name)
415 key = tui.keys['switch_to_' + mode.name]
418 def list_available_modes(self, tui):
420 if len(self.available_modes) > 0:
421 msg = 'Other modes available from here:\n'
422 for mode, key in self.iter_available_modes(tui):
423 msg += '[%s] – %s\n' % (key, mode.short_desc)
426 def mode_switch_on_key(self, tui, key_pressed):
427 for mode, key in self.iter_available_modes(tui):
428 if key_pressed == key:
429 tui.switch_mode(mode.name)
434 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
435 mode_admin = Mode('admin')
436 mode_play = Mode('play')
437 mode_study = Mode('study', shows_info=True)
438 mode_write = Mode('write', is_single_char_entry=True)
439 mode_edit = Mode('edit')
440 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
441 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
442 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
443 mode_control_tile_draw = Mode('control_tile_draw')
444 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
445 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
446 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
447 mode_chat = Mode('chat', has_input_prompt=True)
448 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
449 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
450 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
451 mode_password = Mode('password', has_input_prompt=True)
452 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
453 mode_command_thing = Mode('command_thing', has_input_prompt=True)
454 mode_take_thing = Mode('take_thing', has_input_prompt=True)
455 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
456 mode_enter_face = Mode('enter_face', has_input_prompt=True)
460 def __init__(self, host):
463 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
464 "command_thing", "take_thing",
466 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
467 "install", "wear", "spin"]
468 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
469 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
470 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
471 "control_tile_type", "chat",
472 "study", "play", "edit"]
473 self.mode_admin.available_actions = ["move"]
474 self.mode_control_tile_draw.available_modes = ["admin_enter"]
475 self.mode_control_tile_draw.available_actions = ["move_explorer",
477 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
478 "password", "chat", "study", "play",
479 "admin_enter", "enter_face"]
480 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
485 self.parser = Parser(self.game)
487 self.do_refresh = True
488 self.queue = queue.Queue()
489 self.login_name = None
490 self.map_mode = 'terrain + things'
491 self.password = 'foo'
492 self.switch_mode('waiting_for_server')
494 'switch_to_chat': 't',
495 'switch_to_play': 'p',
496 'switch_to_password': 'P',
497 'switch_to_annotate': 'M',
498 'switch_to_portal': 'T',
499 'switch_to_study': '?',
500 'switch_to_edit': 'E',
501 'switch_to_write': 'm',
502 'switch_to_name_thing': 'N',
503 'switch_to_command_thing': 'O',
504 'switch_to_admin_enter': 'A',
505 'switch_to_control_pw_type': 'C',
506 'switch_to_control_tile_type': 'Q',
507 'switch_to_admin_thing_protect': 'T',
509 'switch_to_enter_face': 'f',
510 'switch_to_take_thing': 'z',
511 'switch_to_drop_thing': 'u',
519 'toggle_map_mode': 'L',
520 'toggle_tile_draw': 'm',
521 'hex_move_upleft': 'w',
522 'hex_move_upright': 'e',
523 'hex_move_right': 'd',
524 'hex_move_downright': 'x',
525 'hex_move_downleft': 'y',
526 'hex_move_left': 'a',
527 'square_move_up': 'w',
528 'square_move_left': 'a',
529 'square_move_down': 's',
530 'square_move_right': 'd',
532 if os.path.isfile('config.json'):
533 with open('config.json', 'r') as f:
534 keys_conf = json.loads(f.read())
536 self.keys[k] = keys_conf[k]
537 self.show_help = False
538 self.disconnected = True
539 self.force_instant_connect = True
540 self.input_lines = []
544 self.offset = YX(0,0)
545 curses.wrapper(self.loop)
549 def handle_recv(msg):
555 self.log_msg('@ attempting connect')
556 socket_client_class = PlomSocketClient
557 if self.host.startswith('ws://') or self.host.startswith('wss://'):
558 socket_client_class = WebSocketClient
560 self.socket = socket_client_class(handle_recv, self.host)
561 self.socket_thread = threading.Thread(target=self.socket.run)
562 self.socket_thread.start()
563 self.disconnected = False
564 self.game.thing_types = {}
565 self.game.terrains = {}
566 time.sleep(0.1) # give potential SSL negotation some time …
567 self.socket.send('TASKS')
568 self.socket.send('TERRAINS')
569 self.socket.send('THING_TYPES')
570 self.switch_mode('login')
571 except ConnectionRefusedError:
572 self.log_msg('@ server connect failure')
573 self.disconnected = True
574 self.switch_mode('waiting_for_server')
575 self.do_refresh = True
578 self.log_msg('@ attempting reconnect')
580 # necessitated by some strange SSL race conditions with ws4py
581 time.sleep(0.1) # FIXME find out why exactly necessary
582 self.switch_mode('waiting_for_server')
587 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
588 raise BrokenSocketConnection
589 self.socket.send(msg)
590 except (BrokenPipeError, BrokenSocketConnection):
591 self.log_msg('@ server disconnected :(')
592 self.disconnected = True
593 self.force_instant_connect = True
594 self.do_refresh = True
596 def log_msg(self, msg):
598 if len(self.log) > 100:
599 self.log = self.log[-100:]
601 def restore_input_values(self):
602 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
603 self.input_ = self.game.annotations[self.explorer]
604 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
605 self.input_ = self.game.portals[self.explorer]
606 elif self.mode.name == 'password':
607 self.input_ = self.password
608 elif self.mode.name == 'name_thing':
609 if hasattr(self.thing_selected, 'name'):
610 self.input_ = self.thing_selected.name
611 elif self.mode.name == 'admin_thing_protect':
612 if hasattr(self.thing_selected, 'protection'):
613 self.input_ = self.thing_selected.protection
615 def send_tile_control_command(self):
616 self.send('SET_TILE_CONTROL %s %s' %
617 (self.explorer, quote(self.tile_control_char)))
619 def toggle_map_mode(self):
620 if self.map_mode == 'terrain only':
621 self.map_mode = 'terrain + annotations'
622 elif self.map_mode == 'terrain + annotations':
623 self.map_mode = 'terrain + things'
624 elif self.map_mode == 'terrain + things':
625 self.map_mode = 'protections'
626 elif self.map_mode == 'protections':
627 self.map_mode = 'terrain only'
629 def switch_mode(self, mode_name):
630 if self.mode and self.mode.name == 'control_tile_draw':
631 self.log_msg('@ finished tile protection drawing.')
632 self.tile_draw = False
633 player = self.game.get_thing(self.game.player_id)
634 if mode_name == 'command_thing' and\
635 (not hasattr(player, 'carrying') or not player.carrying.commandable):
636 self.log_msg('? not carrying anything commandable')
638 self.switch_mode('play')
640 if mode_name == 'drop_thing' and\
641 not (hasattr(player, 'carrying' or player.carrying)):
642 self.log_msg('? not carrying anything droppable')
644 self.switch_mode('play')
646 if mode_name == 'admin_enter' and self.is_admin:
648 elif mode_name in {'name_thing', 'admin_thing_protect'}:
650 for t in [t for t in self.game.things if t.position == player.position
651 and t.id_ != player.id_]:
656 self.log_msg('? not standing over thing')
659 self.thing_selected = thing
660 self.mode = getattr(self, 'mode_' + mode_name)
661 if self.mode.name in {'control_tile_draw', 'control_tile_type',
663 self.map_mode = 'protections'
664 elif self.mode.name != 'edit':
665 self.map_mode = 'terrain + things'
666 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
667 player = self.game.get_thing(self.game.player_id)
668 self.explorer = YX(player.position.y, player.position.x)
669 if self.mode.is_single_char_entry:
670 self.show_help = True
671 if len(self.mode.intro_msg) > 0:
672 self.log_msg(self.mode.intro_msg)
673 if self.mode.name == 'login':
675 self.send('LOGIN ' + quote(self.login_name))
677 self.log_msg('@ enter username')
678 elif self.mode.name == 'take_thing':
679 self.log_msg('Portable things in reach for pick-up:')
680 player = self.game.get_thing(self.game.player_id)
681 select_range = [player.position,
682 player.position + YX(0,-1),
683 player.position + YX(0, 1),
684 player.position + YX(-1, 0),
685 player.position + YX(1, 0)]
686 if type(self.game.map_geometry) == MapGeometryHex:
687 if player.position.y % 2:
688 select_range += [player.position + YX(-1, 1),
689 player.position + YX(1, 1)]
691 select_range += [player.position + YX(-1, -1),
692 player.position + YX(1, -1)]
693 self.selectables = [t.id_ for t in self.game.things
694 if t.portable and t.position in select_range]
695 if len(self.selectables) == 0:
698 self.switch_mode('play')
701 for i in range(len(self.selectables)):
702 t = self.game.get_thing(self.selectables[i])
703 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
704 elif self.mode.name == 'drop_thing':
705 self.log_msg('Direction to drop thing to:')
707 ['HERE'] + list(self.game.tui.movement_keys.values())
708 for i in range(len(self.selectables)):
709 self.log_msg(str(i) + ': ' + self.selectables[i])
710 elif self.mode.name == 'command_thing':
711 self.send('TASK:COMMAND ' + quote('HELP'))
712 elif self.mode.name == 'control_pw_pw':
713 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
714 elif self.mode.name == 'control_tile_draw':
715 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']))
717 self.restore_input_values()
719 def set_default_colors(self):
720 curses.init_color(1, 1000, 1000, 1000)
721 curses.init_color(2, 0, 0, 0)
722 self.do_refresh = True
724 def set_random_colors(self):
728 return int(offset + random.random()*375)
730 curses.init_color(1, rand(625), rand(625), rand(625))
731 curses.init_color(2, rand(0), rand(0), rand(0))
732 self.do_refresh = True
736 return self.info_cached
737 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
739 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
740 info_to_cache += 'outside field of view'
742 for t in self.game.things:
743 if t.position == self.explorer:
744 info_to_cache += 'THING: %s' % self.get_thing_info(t)
745 protection = t.protection
746 if protection == '.':
748 info_to_cache += ' / protection: %s\n' % protection
749 if hasattr(t, 'hat'):
750 info_to_cache += t.hat[0:6] + '\n'
751 info_to_cache += t.hat[6:12] + '\n'
752 info_to_cache += t.hat[12:18] + '\n'
753 if hasattr(t, 'face'):
754 info_to_cache += t.face[0:6] + '\n'
755 info_to_cache += t.face[6:12] + '\n'
756 info_to_cache += t.face[12:18] + '\n'
757 terrain_char = self.game.map_content[pos_i]
759 if terrain_char in self.game.terrains:
760 terrain_desc = self.game.terrains[terrain_char]
761 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
763 protection = self.game.map_control_content[pos_i]
764 if protection == '.':
765 protection = 'unprotected'
766 info_to_cache += 'PROTECTION: %s\n' % protection
767 if self.explorer in self.game.portals:
768 info_to_cache += 'PORTAL: ' +\
769 self.game.portals[self.explorer] + '\n'
771 info_to_cache += 'PORTAL: (none)\n'
772 if self.explorer in self.game.annotations:
773 info_to_cache += 'ANNOTATION: ' +\
774 self.game.annotations[self.explorer]
775 self.info_cached = info_to_cache
776 return self.info_cached
778 def get_thing_info(self, t):
780 (t.type_, self.game.thing_types[t.type_])
781 if hasattr(t, 'thing_char'):
783 if hasattr(t, 'name'):
784 info += ' (%s)' % t.name
785 if hasattr(t, 'installed'):
786 info += ' / installed'
789 def loop(self, stdscr):
792 def safe_addstr(y, x, line):
793 if y < self.size.y - 1 or x + len(line) < self.size.x:
794 stdscr.addstr(y, x, line, curses.color_pair(1))
795 else: # workaround to <https://stackoverflow.com/q/7063128>
796 cut_i = self.size.x - x - 1
798 last_char = line[cut_i]
799 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
800 stdscr.insstr(y, self.size.x - 2, ' ')
801 stdscr.addstr(y, x, cut, curses.color_pair(1))
803 def handle_input(msg):
804 command, args = self.parser.parse(msg)
807 def task_action_on(action):
808 return action_tasks[action] in self.game.tasks
810 def msg_into_lines_of_width(msg, width):
814 for i in range(len(msg)):
815 if x >= width or msg[i] == "\n":
827 def reset_screen_size():
828 self.size = YX(*stdscr.getmaxyx())
829 self.size = self.size - YX(self.size.y % 4, 0)
830 self.size = self.size - YX(0, self.size.x % 4)
831 self.window_width = int(self.size.x / 2)
833 def recalc_input_lines():
834 if not self.mode.has_input_prompt:
835 self.input_lines = []
837 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
840 def move_explorer(direction):
841 target = self.game.map_geometry.move_yx(self.explorer, direction)
843 self.info_cached = None
844 self.explorer = target
846 self.send_tile_control_command()
852 for line in self.log:
853 lines += msg_into_lines_of_width(line, self.window_width)
856 max_y = self.size.y - len(self.input_lines)
857 for i in range(len(lines)):
858 if (i >= max_y - height_header):
860 safe_addstr(max_y - i - 1, self.window_width, lines[i])
863 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
864 lines = msg_into_lines_of_width(info, self.window_width)
866 for i in range(len(lines)):
867 y = height_header + i
868 if y >= self.size.y - len(self.input_lines):
870 safe_addstr(y, self.window_width, lines[i])
873 y = self.size.y - len(self.input_lines)
874 for i in range(len(self.input_lines)):
875 safe_addstr(y, self.window_width, self.input_lines[i])
879 if not self.game.turn_complete:
881 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
884 help = "hit [%s] for help" % self.keys['help']
885 if self.mode.has_input_prompt:
886 help = "enter /help for help"
887 safe_addstr(1, self.window_width,
888 'MODE: %s – %s' % (self.mode.short_desc, help))
891 if not self.game.turn_complete and len(self.map_lines) == 0:
893 if self.game.turn_complete:
895 for y in range(self.game.map_geometry.size.y):
896 start = self.game.map_geometry.size.x * y
897 end = start + self.game.map_geometry.size.x
898 if self.map_mode == 'protections':
899 map_lines_split += [[c + ' ' for c
900 in self.game.map_control_content[start:end]]]
902 map_lines_split += [[c + ' ' for c
903 in self.game.map_content[start:end]]]
904 if self.map_mode == 'terrain + annotations':
905 for p in self.game.annotations:
906 map_lines_split[p.y][p.x] = 'A '
907 elif self.map_mode == 'terrain + things':
908 for p in self.game.portals.keys():
909 original = map_lines_split[p.y][p.x]
910 map_lines_split[p.y][p.x] = original[0] + 'P'
913 def draw_thing(t, used_positions):
914 symbol = self.game.thing_types[t.type_]
916 if hasattr(t, 'thing_char'):
917 meta_char = t.thing_char
918 if t.position in used_positions:
920 if hasattr(t, 'carrying') and t.carrying:
922 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
923 used_positions += [t.position]
925 for t in [t for t in self.game.things if t.type_ != 'Player']:
926 draw_thing(t, used_positions)
927 for t in [t for t in self.game.things if t.type_ == 'Player']:
928 draw_thing(t, used_positions)
929 player = self.game.get_thing(self.game.player_id)
930 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
931 map_lines_split[self.explorer.y][self.explorer.x] = '??'
932 elif self.map_mode != 'terrain + things':
933 map_lines_split[player.position.y][player.position.x] = '??'
935 if type(self.game.map_geometry) == MapGeometryHex:
937 for line in map_lines_split:
938 self.map_lines += [indent * ' ' + ''.join(line)]
939 indent = 0 if indent else 1
941 for line in map_lines_split:
942 self.map_lines += [''.join(line)]
943 window_center = YX(int(self.size.y / 2),
944 int(self.window_width / 2))
945 center = player.position
946 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
947 center = self.explorer
948 center = YX(center.y, center.x * 2)
949 self.offset = center - window_center
950 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
951 self.offset += YX(0, 1)
952 term_y = max(0, -self.offset.y)
953 term_x = max(0, -self.offset.x)
954 map_y = max(0, self.offset.y)
955 map_x = max(0, self.offset.x)
956 while term_y < self.size.y and map_y < len(self.map_lines):
957 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
958 safe_addstr(term_y, term_x, to_draw)
963 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
964 self.mode.help_intro)
965 if len(self.mode.available_actions) > 0:
966 content += "Available actions:\n"
967 for action in self.mode.available_actions:
968 if action in action_tasks:
969 if action_tasks[action] not in self.game.tasks:
971 if action == 'move_explorer':
974 key = ','.join(self.movement_keys)
976 key = self.keys[action]
977 content += '[%s] – %s\n' % (key, action_descriptions[action])
979 content += self.mode.list_available_modes(self)
980 for i in range(self.size.y):
982 self.window_width * (not self.mode.has_input_prompt),
983 ' ' * self.window_width)
985 for line in content.split('\n'):
986 lines += msg_into_lines_of_width(line, self.window_width)
987 for i in range(len(lines)):
991 self.window_width * (not self.mode.has_input_prompt),
996 stdscr.bkgd(' ', curses.color_pair(1))
998 if self.mode.has_input_prompt:
1000 if self.mode.shows_info:
1005 if not self.mode.is_intro:
1011 def pick_selectable(task_name):
1013 i = int(self.input_)
1014 if i < 0 or i >= len(self.selectables):
1015 self.log_msg('? invalid index, aborted')
1017 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1019 self.log_msg('? invalid index, aborted')
1021 self.switch_mode('play')
1023 action_descriptions = {
1025 'flatten': 'flatten surroundings',
1026 'teleport': 'teleport',
1027 'take_thing': 'pick up thing',
1028 'drop_thing': 'drop thing',
1029 'toggle_map_mode': 'toggle map view',
1030 'toggle_tile_draw': 'toggle protection character drawing',
1031 'install': '(un-)install',
1032 'wear': '(un-)wear',
1033 'door': 'open/close',
1034 'consume': 'consume',
1039 'flatten': 'FLATTEN_SURROUNDINGS',
1040 'take_thing': 'PICK_UP',
1041 'drop_thing': 'DROP',
1043 'install': 'INSTALL',
1046 'command': 'COMMAND',
1047 'consume': 'INTOXICATE',
1051 curses.curs_set(False) # hide cursor
1052 curses.start_color()
1053 self.set_default_colors()
1054 curses.init_pair(1, 1, 2)
1057 self.explorer = YX(0, 0)
1060 interval = datetime.timedelta(seconds=5)
1061 last_ping = datetime.datetime.now() - interval
1063 if self.disconnected and self.force_instant_connect:
1064 self.force_instant_connect = False
1066 now = datetime.datetime.now()
1067 if now - last_ping > interval:
1068 if self.disconnected:
1078 self.do_refresh = False
1081 msg = self.queue.get(block=False)
1086 key = stdscr.getkey()
1087 self.do_refresh = True
1088 except curses.error:
1093 if key == 'KEY_RESIZE':
1095 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1096 self.input_ = self.input_[:-1]
1097 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1098 or (self.mode.has_input_prompt and key == '\n'
1099 and self.input_ == ''\
1100 and self.mode.name in {'chat', 'command_thing',
1101 'take_thing', 'drop_thing',
1103 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1104 self.log_msg('@ aborted')
1105 self.switch_mode('play')
1106 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1107 self.show_help = True
1109 self.restore_input_values()
1110 elif self.mode.has_input_prompt and key != '\n': # Return key
1112 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1113 if len(self.input_) > max_length:
1114 self.input_ = self.input_[:max_length]
1115 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1116 self.show_help = True
1117 elif self.mode.name == 'login' and key == '\n':
1118 self.login_name = self.input_
1119 self.send('LOGIN ' + quote(self.input_))
1121 elif self.mode.name == 'enter_face' and key == '\n':
1122 if len(self.input_) != 18:
1123 self.log_msg('? wrong input length, aborting')
1125 self.send('PLAYER_FACE %s' % quote(self.input_))
1127 self.switch_mode('edit')
1128 elif self.mode.name == 'take_thing' and key == '\n':
1129 pick_selectable('PICK_UP')
1130 elif self.mode.name == 'drop_thing' and key == '\n':
1131 pick_selectable('DROP')
1132 elif self.mode.name == 'command_thing' and key == '\n':
1133 self.send('TASK:COMMAND ' + quote(self.input_))
1135 elif self.mode.name == 'control_pw_pw' and key == '\n':
1136 if self.input_ == '':
1137 self.log_msg('@ aborted')
1139 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1140 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1141 self.switch_mode('admin')
1142 elif self.mode.name == 'password' and key == '\n':
1143 if self.input_ == '':
1145 self.password = self.input_
1146 self.switch_mode('edit')
1147 elif self.mode.name == 'admin_enter' and key == '\n':
1148 self.send('BECOME_ADMIN ' + quote(self.input_))
1149 self.switch_mode('play')
1150 elif self.mode.name == 'control_pw_type' and key == '\n':
1151 if len(self.input_) != 1:
1152 self.log_msg('@ entered non-single-char, therefore aborted')
1153 self.switch_mode('admin')
1155 self.tile_control_char = self.input_
1156 self.switch_mode('control_pw_pw')
1157 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1158 if len(self.input_) != 1:
1159 self.log_msg('@ entered non-single-char, therefore aborted')
1161 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1162 quote(self.input_)))
1163 self.log_msg('@ sent new protection character for thing')
1164 self.switch_mode('admin')
1165 elif self.mode.name == 'control_tile_type' and key == '\n':
1166 if len(self.input_) != 1:
1167 self.log_msg('@ entered non-single-char, therefore aborted')
1168 self.switch_mode('admin')
1170 self.tile_control_char = self.input_
1171 self.switch_mode('control_tile_draw')
1172 elif self.mode.name == 'chat' and key == '\n':
1173 if self.input_ == '':
1175 if self.input_[0] == '/':
1176 if self.input_.startswith('/nick'):
1177 tokens = self.input_.split(maxsplit=1)
1178 if len(tokens) == 2:
1179 self.send('NICK ' + quote(tokens[1]))
1181 self.log_msg('? need login name')
1183 self.log_msg('? unknown command')
1185 self.send('ALL ' + quote(self.input_))
1187 elif self.mode.name == 'name_thing' and key == '\n':
1188 if self.input_ == '':
1190 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1192 quote(self.password)))
1193 self.switch_mode('edit')
1194 elif self.mode.name == 'annotate' and key == '\n':
1195 if self.input_ == '':
1197 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1198 quote(self.password)))
1199 self.switch_mode('edit')
1200 elif self.mode.name == 'portal' and key == '\n':
1201 if self.input_ == '':
1203 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1204 quote(self.password)))
1205 self.switch_mode('edit')
1206 elif self.mode.name == 'study':
1207 if self.mode.mode_switch_on_key(self, key):
1209 elif key == self.keys['toggle_map_mode']:
1210 self.toggle_map_mode()
1211 elif key in self.movement_keys:
1212 move_explorer(self.movement_keys[key])
1213 elif self.mode.name == 'play':
1214 if self.mode.mode_switch_on_key(self, key):
1216 elif key == self.keys['door'] and task_action_on('door'):
1217 self.send('TASK:DOOR')
1218 elif key == self.keys['consume'] and task_action_on('consume'):
1219 self.send('TASK:INTOXICATE')
1220 elif key == self.keys['install'] and task_action_on('install'):
1221 self.send('TASK:INSTALL')
1222 elif key == self.keys['wear'] and task_action_on('wear'):
1223 self.send('TASK:WEAR')
1224 elif key == self.keys['spin'] and task_action_on('spin'):
1225 self.send('TASK:SPIN')
1226 elif key == self.keys['teleport']:
1227 player = self.game.get_thing(self.game.player_id)
1228 if player.position in self.game.portals:
1229 self.host = self.game.portals[player.position]
1233 self.log_msg('? not standing on portal')
1234 elif key in self.movement_keys and task_action_on('move'):
1235 self.send('TASK:MOVE ' + self.movement_keys[key])
1236 elif self.mode.name == 'write':
1237 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1238 self.switch_mode('edit')
1239 elif self.mode.name == 'control_tile_draw':
1240 if self.mode.mode_switch_on_key(self, key):
1242 elif key in self.movement_keys:
1243 move_explorer(self.movement_keys[key])
1244 elif key == self.keys['toggle_tile_draw']:
1245 self.tile_draw = False if self.tile_draw else True
1246 elif self.mode.name == 'admin':
1247 if self.mode.mode_switch_on_key(self, key):
1249 elif key in self.movement_keys and task_action_on('move'):
1250 self.send('TASK:MOVE ' + self.movement_keys[key])
1251 elif self.mode.name == 'edit':
1252 if self.mode.mode_switch_on_key(self, key):
1254 elif key == self.keys['flatten'] and task_action_on('flatten'):
1255 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1256 elif key == self.keys['toggle_map_mode']:
1257 self.toggle_map_mode()
1258 elif key in self.movement_keys and task_action_on('move'):
1259 self.send('TASK:MOVE ' + self.movement_keys[key])
1261 if len(sys.argv) != 2:
1262 raise ArgError('wrong number of arguments, need game host')