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_CHATFACE(game, thing_id):
204 game.tui.draw_face = thing_id
205 cmd_CHATFACE.argtypes = 'int:pos'
207 def cmd_PLAYER_ID(game, player_id):
208 game.player_id = player_id
209 cmd_PLAYER_ID.argtypes = 'int:nonneg'
211 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
212 t = game.get_thing(thing_id)
214 t = ThingBase(game, thing_id)
218 t.protection = protection
219 t.portable = portable
220 t.commandable = commandable
221 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
223 def cmd_THING_NAME(game, thing_id, name):
224 t = game.get_thing(thing_id)
226 cmd_THING_NAME.argtypes = 'int:pos string'
228 def cmd_THING_FACE(game, thing_id, face):
229 t = game.get_thing(thing_id)
231 cmd_THING_FACE.argtypes = 'int:pos string'
233 def cmd_THING_HAT(game, thing_id, hat):
234 t = game.get_thing(thing_id)
236 cmd_THING_HAT.argtypes = 'int:pos string'
238 def cmd_THING_CHAR(game, thing_id, c):
239 t = game.get_thing(thing_id)
241 cmd_THING_CHAR.argtypes = 'int:pos char'
243 def cmd_MAP(game, geometry, size, content):
244 map_geometry_class = globals()['MapGeometry' + geometry]
245 game.map_geometry = map_geometry_class(size)
246 game.map_content = content
247 if type(game.map_geometry) == MapGeometrySquare:
248 game.tui.movement_keys = {
249 game.tui.keys['square_move_up']: 'UP',
250 game.tui.keys['square_move_left']: 'LEFT',
251 game.tui.keys['square_move_down']: 'DOWN',
252 game.tui.keys['square_move_right']: 'RIGHT',
254 elif type(game.map_geometry) == MapGeometryHex:
255 game.tui.movement_keys = {
256 game.tui.keys['hex_move_upleft']: 'UPLEFT',
257 game.tui.keys['hex_move_upright']: 'UPRIGHT',
258 game.tui.keys['hex_move_right']: 'RIGHT',
259 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
260 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
261 game.tui.keys['hex_move_left']: 'LEFT',
263 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
265 def cmd_FOV(game, content):
267 cmd_FOV.argtypes = 'string'
269 def cmd_MAP_CONTROL(game, content):
270 game.map_control_content = content
271 cmd_MAP_CONTROL.argtypes = 'string'
273 def cmd_GAME_STATE_COMPLETE(game):
274 game.turn_complete = True
275 game.tui.do_refresh = True
276 game.tui.info_cached = None
277 game.player = game.get_thing(game.player_id)
278 if game.tui.mode.name == 'post_login_wait':
279 game.tui.switch_mode('play')
280 cmd_GAME_STATE_COMPLETE.argtypes = ''
282 def cmd_PORTAL(game, position, msg):
283 game.portals[position] = msg
284 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
286 def cmd_PLAY_ERROR(game, msg):
287 game.tui.log_msg('? ' + msg)
288 game.tui.flash = True
289 game.tui.do_refresh = True
290 cmd_PLAY_ERROR.argtypes = 'string'
292 def cmd_GAME_ERROR(game, msg):
293 game.tui.log_msg('? game error: ' + msg)
294 game.tui.do_refresh = True
295 cmd_GAME_ERROR.argtypes = 'string'
297 def cmd_ARGUMENT_ERROR(game, msg):
298 game.tui.log_msg('? syntax error: ' + msg)
299 game.tui.do_refresh = True
300 cmd_ARGUMENT_ERROR.argtypes = 'string'
302 def cmd_ANNOTATION(game, position, msg):
303 game.annotations[position] = msg
304 if game.tui.mode.shows_info:
305 game.tui.do_refresh = True
306 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
308 def cmd_TASKS(game, tasks_comma_separated):
309 game.tasks = tasks_comma_separated.split(',')
310 game.tui.mode_write.legal = 'WRITE' in game.tasks
311 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
312 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
313 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
314 cmd_TASKS.argtypes = 'string'
316 def cmd_THING_TYPE(game, thing_type, symbol_hint):
317 game.thing_types[thing_type] = symbol_hint
318 cmd_THING_TYPE.argtypes = 'string char'
320 def cmd_THING_INSTALLED(game, thing_id):
321 game.get_thing(thing_id).installed = True
322 cmd_THING_INSTALLED.argtypes = 'int:pos'
324 def cmd_THING_CARRYING(game, thing_id, carried_id):
325 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
326 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
328 def cmd_TERRAIN(game, terrain_char, terrain_desc):
329 game.terrains[terrain_char] = terrain_desc
330 cmd_TERRAIN.argtypes = 'char string'
334 cmd_PONG.argtypes = ''
336 def cmd_DEFAULT_COLORS(game):
337 game.tui.set_default_colors()
338 cmd_DEFAULT_COLORS.argtypes = ''
340 def cmd_RANDOM_COLORS(game):
341 game.tui.set_random_colors()
342 cmd_RANDOM_COLORS.argtypes = ''
344 class Game(GameBase):
345 turn_complete = False
349 def __init__(self, *args, **kwargs):
350 super().__init__(*args, **kwargs)
351 self.register_command(cmd_LOGIN_OK)
352 self.register_command(cmd_ADMIN_OK)
353 self.register_command(cmd_PONG)
354 self.register_command(cmd_CHAT)
355 self.register_command(cmd_CHATFACE)
356 self.register_command(cmd_REPLY)
357 self.register_command(cmd_PLAYER_ID)
358 self.register_command(cmd_TURN)
359 self.register_command(cmd_THING)
360 self.register_command(cmd_THING_TYPE)
361 self.register_command(cmd_THING_NAME)
362 self.register_command(cmd_THING_CHAR)
363 self.register_command(cmd_THING_FACE)
364 self.register_command(cmd_THING_HAT)
365 self.register_command(cmd_THING_CARRYING)
366 self.register_command(cmd_THING_INSTALLED)
367 self.register_command(cmd_TERRAIN)
368 self.register_command(cmd_MAP)
369 self.register_command(cmd_MAP_CONTROL)
370 self.register_command(cmd_PORTAL)
371 self.register_command(cmd_ANNOTATION)
372 self.register_command(cmd_GAME_STATE_COMPLETE)
373 self.register_command(cmd_ARGUMENT_ERROR)
374 self.register_command(cmd_GAME_ERROR)
375 self.register_command(cmd_PLAY_ERROR)
376 self.register_command(cmd_TASKS)
377 self.register_command(cmd_FOV)
378 self.register_command(cmd_DEFAULT_COLORS)
379 self.register_command(cmd_RANDOM_COLORS)
380 self.map_content = ''
382 self.annotations = {}
387 def get_string_options(self, string_option_type):
388 if string_option_type == 'map_geometry':
389 return ['Hex', 'Square']
390 elif string_option_type == 'thing_type':
391 return self.thing_types.keys()
394 def get_command(self, command_name):
395 from functools import partial
396 f = partial(self.commands[command_name], self)
397 f.argtypes = self.commands[command_name].argtypes
402 def __init__(self, name, has_input_prompt=False, shows_info=False,
403 is_intro=False, is_single_char_entry=False):
405 self.short_desc = mode_helps[name]['short']
406 self.available_modes = []
407 self.available_actions = []
408 self.has_input_prompt = has_input_prompt
409 self.shows_info = shows_info
410 self.is_intro = is_intro
411 self.help_intro = mode_helps[name]['long']
412 self.intro_msg = mode_helps[name]['intro']
413 self.is_single_char_entry = is_single_char_entry
416 def iter_available_modes(self, tui):
417 for mode_name in self.available_modes:
418 mode = getattr(tui, 'mode_' + mode_name)
421 key = tui.keys['switch_to_' + mode.name]
424 def list_available_modes(self, tui):
426 if len(self.available_modes) > 0:
427 msg = 'Other modes available from here:\n'
428 for mode, key in self.iter_available_modes(tui):
429 msg += '[%s] – %s\n' % (key, mode.short_desc)
432 def mode_switch_on_key(self, tui, key_pressed):
433 for mode, key in self.iter_available_modes(tui):
434 if key_pressed == key:
435 tui.switch_mode(mode.name)
440 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
441 mode_admin = Mode('admin')
442 mode_play = Mode('play')
443 mode_study = Mode('study', shows_info=True)
444 mode_write = Mode('write', is_single_char_entry=True)
445 mode_edit = Mode('edit')
446 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
447 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
448 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
449 mode_control_tile_draw = Mode('control_tile_draw')
450 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
451 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
452 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
453 mode_chat = Mode('chat', has_input_prompt=True)
454 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
455 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
456 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
457 mode_password = Mode('password', has_input_prompt=True)
458 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
459 mode_command_thing = Mode('command_thing', has_input_prompt=True)
460 mode_take_thing = Mode('take_thing', has_input_prompt=True)
461 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
462 mode_enter_face = Mode('enter_face', has_input_prompt=True)
466 def __init__(self, host):
469 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
470 "command_thing", "take_thing",
472 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
473 "install", "wear", "spin"]
474 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
475 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
476 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
477 "control_tile_type", "chat",
478 "study", "play", "edit"]
479 self.mode_admin.available_actions = ["move"]
480 self.mode_control_tile_draw.available_modes = ["admin_enter"]
481 self.mode_control_tile_draw.available_actions = ["move_explorer",
483 self.mode_edit.available_modes = ["write", "annotate", "portal",
484 "name_thing", "enter_face", "password",
485 "chat", "study", "play", "admin_enter"]
486 self.mode_edit.available_actions = ["move", "flatten", "install",
492 self.parser = Parser(self.game)
494 self.do_refresh = True
495 self.queue = queue.Queue()
496 self.login_name = None
497 self.map_mode = 'terrain + things'
498 self.password = 'foo'
499 self.switch_mode('waiting_for_server')
501 'switch_to_chat': 't',
502 'switch_to_play': 'p',
503 'switch_to_password': 'P',
504 'switch_to_annotate': 'M',
505 'switch_to_portal': 'T',
506 'switch_to_study': '?',
507 'switch_to_edit': 'E',
508 'switch_to_write': 'm',
509 'switch_to_name_thing': 'N',
510 'switch_to_command_thing': 'O',
511 'switch_to_admin_enter': 'A',
512 'switch_to_control_pw_type': 'C',
513 'switch_to_control_tile_type': 'Q',
514 'switch_to_admin_thing_protect': 'T',
516 'switch_to_enter_face': 'f',
517 'switch_to_take_thing': 'z',
518 'switch_to_drop_thing': 'u',
526 'toggle_map_mode': 'L',
527 'toggle_tile_draw': 'm',
528 'hex_move_upleft': 'w',
529 'hex_move_upright': 'e',
530 'hex_move_right': 'd',
531 'hex_move_downright': 'x',
532 'hex_move_downleft': 'y',
533 'hex_move_left': 'a',
534 'square_move_up': 'w',
535 'square_move_left': 'a',
536 'square_move_down': 's',
537 'square_move_right': 'd',
539 if os.path.isfile('config.json'):
540 with open('config.json', 'r') as f:
541 keys_conf = json.loads(f.read())
543 self.keys[k] = keys_conf[k]
544 self.show_help = False
545 self.disconnected = True
546 self.force_instant_connect = True
547 self.input_lines = []
551 self.draw_face = False
552 self.offset = YX(0,0)
553 curses.wrapper(self.loop)
557 def handle_recv(msg):
563 self.log_msg('@ attempting connect')
564 socket_client_class = PlomSocketClient
565 if self.host.startswith('ws://') or self.host.startswith('wss://'):
566 socket_client_class = WebSocketClient
568 self.socket = socket_client_class(handle_recv, self.host)
569 self.socket_thread = threading.Thread(target=self.socket.run)
570 self.socket_thread.start()
571 self.disconnected = False
572 self.game.thing_types = {}
573 self.game.terrains = {}
574 time.sleep(0.1) # give potential SSL negotation some time …
575 self.socket.send('TASKS')
576 self.socket.send('TERRAINS')
577 self.socket.send('THING_TYPES')
578 self.switch_mode('login')
579 except ConnectionRefusedError:
580 self.log_msg('@ server connect failure')
581 self.disconnected = True
582 self.switch_mode('waiting_for_server')
583 self.do_refresh = True
586 self.log_msg('@ attempting reconnect')
588 # necessitated by some strange SSL race conditions with ws4py
589 time.sleep(0.1) # FIXME find out why exactly necessary
590 self.switch_mode('waiting_for_server')
595 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
596 raise BrokenSocketConnection
597 self.socket.send(msg)
598 except (BrokenPipeError, BrokenSocketConnection):
599 self.log_msg('@ server disconnected :(')
600 self.disconnected = True
601 self.force_instant_connect = True
602 self.do_refresh = True
604 def log_msg(self, msg):
606 if len(self.log) > 100:
607 self.log = self.log[-100:]
609 def restore_input_values(self):
610 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
611 self.input_ = self.game.annotations[self.explorer]
612 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
613 self.input_ = self.game.portals[self.explorer]
614 elif self.mode.name == 'password':
615 self.input_ = self.password
616 elif self.mode.name == 'name_thing':
617 if hasattr(self.thing_selected, 'name'):
618 self.input_ = self.thing_selected.name
619 elif self.mode.name == 'admin_thing_protect':
620 if hasattr(self.thing_selected, 'protection'):
621 self.input_ = self.thing_selected.protection
623 def send_tile_control_command(self):
624 self.send('SET_TILE_CONTROL %s %s' %
625 (self.explorer, quote(self.tile_control_char)))
627 def toggle_map_mode(self):
628 if self.map_mode == 'terrain only':
629 self.map_mode = 'terrain + annotations'
630 elif self.map_mode == 'terrain + annotations':
631 self.map_mode = 'terrain + things'
632 elif self.map_mode == 'terrain + things':
633 self.map_mode = 'protections'
634 elif self.map_mode == 'protections':
635 self.map_mode = 'terrain only'
637 def switch_mode(self, mode_name):
639 def fail(msg, return_mode='play'):
640 self.log_msg('? ' + msg)
642 self.switch_mode(return_mode)
644 if self.mode and self.mode.name == 'control_tile_draw':
645 self.log_msg('@ finished tile protection drawing.')
646 self.tile_draw = False
647 if mode_name == 'command_thing' and\
648 (not self.game.player.carrying or
649 not self.game.player.carrying.commandable):
650 return fail('not carrying anything commandable')
651 if mode_name == 'take_thing' and self.game.player.carrying:
652 return fail('already carrying something')
653 if mode_name == 'drop_thing' and not self.game.player.carrying:
654 return fail('not carrying anything droppable')
655 if mode_name == 'admin_enter' and self.is_admin:
657 elif mode_name in {'name_thing', 'admin_thing_protect'}:
659 for t in [t for t in self.game.things
660 if t.position == self.game.player.position
661 and t.id_ != self.game.player.id_]:
665 return fail('not standing over thing', 'edit')
667 self.thing_selected = thing
668 self.mode = getattr(self, 'mode_' + mode_name)
669 if self.mode.name in {'control_tile_draw', 'control_tile_type',
671 self.map_mode = 'protections'
672 elif self.mode.name != 'edit':
673 self.map_mode = 'terrain + things'
674 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
675 self.explorer = YX(self.game.player.position.y,
676 self.game.player.position.x)
677 if self.mode.is_single_char_entry:
678 self.show_help = True
679 if len(self.mode.intro_msg) > 0:
680 self.log_msg(self.mode.intro_msg)
681 if self.mode.name == 'login':
683 self.send('LOGIN ' + quote(self.login_name))
685 self.log_msg('@ enter username')
686 elif self.mode.name == 'take_thing':
687 self.log_msg('Portable things in reach for pick-up:')
688 select_range = [self.game.player.position,
689 self.game.player.position + YX(0,-1),
690 self.game.player.position + YX(0, 1),
691 self.game.player.position + YX(-1, 0),
692 self.game.player.position + YX(1, 0)]
693 if type(self.game.map_geometry) == MapGeometryHex:
694 if self.game.player.position.y % 2:
695 select_range += [self.game.player.position + YX(-1, 1),
696 self.game.player.position + YX(1, 1)]
698 select_range += [self.game.player.position + YX(-1, -1),
699 self.game.player.position + YX(1, -1)]
700 self.selectables = [t.id_ for t in self.game.things
701 if t.portable and t.position in select_range]
702 if len(self.selectables) == 0:
703 return fail('nothing to pick-up')
705 for i in range(len(self.selectables)):
706 t = self.game.get_thing(self.selectables[i])
707 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
708 elif self.mode.name == 'drop_thing':
709 self.log_msg('Direction to drop thing to:')
711 ['HERE'] + list(self.game.tui.movement_keys.values())
712 for i in range(len(self.selectables)):
713 self.log_msg(str(i) + ': ' + self.selectables[i])
714 elif self.mode.name == 'command_thing':
715 self.send('TASK:COMMAND ' + quote('HELP'))
716 elif self.mode.name == 'control_pw_pw':
717 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
718 elif self.mode.name == 'control_tile_draw':
719 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']))
721 self.restore_input_values()
723 def set_default_colors(self):
724 curses.init_color(1, 1000, 1000, 1000)
725 curses.init_color(2, 0, 0, 0)
726 self.do_refresh = True
728 def set_random_colors(self):
732 return int(offset + random.random()*375)
734 curses.init_color(1, rand(625), rand(625), rand(625))
735 curses.init_color(2, rand(0), rand(0), rand(0))
736 self.do_refresh = True
740 return self.info_cached
741 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
743 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
744 info_to_cache += 'outside field of view'
746 for t in self.game.things:
747 if t.position == self.explorer:
748 info_to_cache += 'THING: %s' % self.get_thing_info(t)
749 protection = t.protection
750 if protection == '.':
752 info_to_cache += ' / protection: %s\n' % protection
753 if hasattr(t, 'hat'):
754 info_to_cache += t.hat[0:6] + '\n'
755 info_to_cache += t.hat[6:12] + '\n'
756 info_to_cache += t.hat[12:18] + '\n'
757 if hasattr(t, 'face'):
758 info_to_cache += t.face[0:6] + '\n'
759 info_to_cache += t.face[6:12] + '\n'
760 info_to_cache += t.face[12:18] + '\n'
761 terrain_char = self.game.map_content[pos_i]
763 if terrain_char in self.game.terrains:
764 terrain_desc = self.game.terrains[terrain_char]
765 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
767 protection = self.game.map_control_content[pos_i]
768 if protection == '.':
769 protection = 'unprotected'
770 info_to_cache += 'PROTECTION: %s\n' % protection
771 if self.explorer in self.game.portals:
772 info_to_cache += 'PORTAL: ' +\
773 self.game.portals[self.explorer] + '\n'
775 info_to_cache += 'PORTAL: (none)\n'
776 if self.explorer in self.game.annotations:
777 info_to_cache += 'ANNOTATION: ' +\
778 self.game.annotations[self.explorer]
779 self.info_cached = info_to_cache
780 return self.info_cached
782 def get_thing_info(self, t):
784 (t.type_, self.game.thing_types[t.type_])
785 if hasattr(t, 'thing_char'):
787 if hasattr(t, 'name'):
788 info += ' (%s)' % t.name
789 if hasattr(t, 'installed'):
790 info += ' / installed'
793 def loop(self, stdscr):
796 def safe_addstr(y, x, line):
797 if y < self.size.y - 1 or x + len(line) < self.size.x:
798 stdscr.addstr(y, x, line, curses.color_pair(1))
799 else: # workaround to <https://stackoverflow.com/q/7063128>
800 cut_i = self.size.x - x - 1
802 last_char = line[cut_i]
803 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
804 stdscr.insstr(y, self.size.x - 2, ' ')
805 stdscr.addstr(y, x, cut, curses.color_pair(1))
807 def handle_input(msg):
808 command, args = self.parser.parse(msg)
811 def task_action_on(action):
812 return action_tasks[action] in self.game.tasks
814 def msg_into_lines_of_width(msg, width):
818 for i in range(len(msg)):
819 if x >= width or msg[i] == "\n":
831 def reset_screen_size():
832 self.size = YX(*stdscr.getmaxyx())
833 self.size = self.size - YX(self.size.y % 4, 0)
834 self.size = self.size - YX(0, self.size.x % 4)
835 self.window_width = int(self.size.x / 2)
837 def recalc_input_lines():
838 if not self.mode.has_input_prompt:
839 self.input_lines = []
841 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
844 def move_explorer(direction):
845 target = self.game.map_geometry.move_yx(self.explorer, direction)
847 self.info_cached = None
848 self.explorer = target
850 self.send_tile_control_command()
856 for line in self.log:
857 lines += msg_into_lines_of_width(line, self.window_width)
860 max_y = self.size.y - len(self.input_lines)
861 for i in range(len(lines)):
862 if (i >= max_y - height_header):
864 safe_addstr(max_y - i - 1, self.window_width, lines[i])
867 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
868 lines = msg_into_lines_of_width(info, self.window_width)
870 for i in range(len(lines)):
871 y = height_header + i
872 if y >= self.size.y - len(self.input_lines):
874 safe_addstr(y, self.window_width, lines[i])
877 y = self.size.y - len(self.input_lines)
878 for i in range(len(self.input_lines)):
879 safe_addstr(y, self.window_width, self.input_lines[i])
883 if not self.game.turn_complete:
885 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
888 help = "hit [%s] for help" % self.keys['help']
889 if self.mode.has_input_prompt:
890 help = "enter /help for help"
891 safe_addstr(1, self.window_width,
892 'MODE: %s – %s' % (self.mode.short_desc, help))
895 if not self.game.turn_complete and len(self.map_lines) == 0:
897 if self.game.turn_complete:
899 for y in range(self.game.map_geometry.size.y):
900 start = self.game.map_geometry.size.x * y
901 end = start + self.game.map_geometry.size.x
902 if self.map_mode == 'protections':
903 map_lines_split += [[c + ' ' for c
904 in self.game.map_control_content[start:end]]]
906 map_lines_split += [[c + ' ' for c
907 in self.game.map_content[start:end]]]
908 if self.map_mode == 'terrain + annotations':
909 for p in self.game.annotations:
910 map_lines_split[p.y][p.x] = 'A '
911 elif self.map_mode == 'terrain + things':
912 for p in self.game.portals.keys():
913 original = map_lines_split[p.y][p.x]
914 map_lines_split[p.y][p.x] = original[0] + 'P'
917 def draw_thing(t, used_positions):
918 symbol = self.game.thing_types[t.type_]
920 if hasattr(t, 'thing_char'):
921 meta_char = t.thing_char
922 if t.position in used_positions:
924 if hasattr(t, 'carrying') and t.carrying:
926 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
927 used_positions += [t.position]
929 for t in [t for t in self.game.things if t.type_ != 'Player']:
930 draw_thing(t, used_positions)
931 for t in [t for t in self.game.things if t.type_ == 'Player']:
932 draw_thing(t, used_positions)
933 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
934 map_lines_split[self.explorer.y][self.explorer.x] = '??'
935 elif self.map_mode != 'terrain + things':
936 map_lines_split[self.game.player.position.y]\
937 [self.game.player.position.x] = '??'
939 if type(self.game.map_geometry) == MapGeometryHex:
941 for line in map_lines_split:
942 self.map_lines += [indent * ' ' + ''.join(line)]
943 indent = 0 if indent else 1
945 for line in map_lines_split:
946 self.map_lines += [''.join(line)]
947 window_center = YX(int(self.size.y / 2),
948 int(self.window_width / 2))
949 center = self.game.player.position
950 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
951 center = self.explorer
952 center = YX(center.y, center.x * 2)
953 self.offset = center - window_center
954 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
955 self.offset += YX(0, 1)
956 term_y = max(0, -self.offset.y)
957 term_x = max(0, -self.offset.x)
958 map_y = max(0, self.offset.y)
959 map_x = max(0, self.offset.x)
960 while term_y < self.size.y and map_y < len(self.map_lines):
961 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
962 safe_addstr(term_y, term_x, to_draw)
966 def draw_face_popup():
967 t = self.game.get_thing(self.draw_face)
969 self.draw_face = False
972 def draw_body_part(body_part, end_y):
973 start_x = self.window_width - 10
974 safe_addstr(end_y - 4, start_x, '+--------+')
975 safe_addstr(end_y - 3, start_x, '| |')
976 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
977 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
978 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
980 if hasattr(t, 'face'):
981 draw_body_part(t.face, self.size.y - 1)
982 if hasattr(t, 'hat'):
983 draw_body_part(t.hat, self.size.y - 4)
986 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
987 self.mode.help_intro)
988 if len(self.mode.available_actions) > 0:
989 content += "Available actions:\n"
990 for action in self.mode.available_actions:
991 if action in action_tasks:
992 if action_tasks[action] not in self.game.tasks:
994 if action == 'move_explorer':
997 key = ','.join(self.movement_keys)
999 key = self.keys[action]
1000 content += '[%s] – %s\n' % (key, action_descriptions[action])
1002 content += self.mode.list_available_modes(self)
1003 for i in range(self.size.y):
1005 self.window_width * (not self.mode.has_input_prompt),
1006 ' ' * self.window_width)
1008 for line in content.split('\n'):
1009 lines += msg_into_lines_of_width(line, self.window_width)
1010 for i in range(len(lines)):
1011 if i >= self.size.y:
1014 self.window_width * (not self.mode.has_input_prompt),
1019 stdscr.bkgd(' ', curses.color_pair(1))
1020 recalc_input_lines()
1021 if self.mode.has_input_prompt:
1023 if self.mode.shows_info:
1028 if not self.mode.is_intro:
1033 if self.draw_face and self.mode.name in {'chat', 'play'}:
1036 def pick_selectable(task_name):
1038 i = int(self.input_)
1039 if i < 0 or i >= len(self.selectables):
1040 self.log_msg('? invalid index, aborted')
1042 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1044 self.log_msg('? invalid index, aborted')
1046 self.switch_mode('play')
1048 action_descriptions = {
1050 'flatten': 'flatten surroundings',
1051 'teleport': 'teleport',
1052 'take_thing': 'pick up thing',
1053 'drop_thing': 'drop thing',
1054 'toggle_map_mode': 'toggle map view',
1055 'toggle_tile_draw': 'toggle protection character drawing',
1056 'install': '(un-)install',
1057 'wear': '(un-)wear',
1058 'door': 'open/close',
1059 'consume': 'consume',
1064 'flatten': 'FLATTEN_SURROUNDINGS',
1065 'take_thing': 'PICK_UP',
1066 'drop_thing': 'DROP',
1068 'install': 'INSTALL',
1071 'command': 'COMMAND',
1072 'consume': 'INTOXICATE',
1076 curses.curs_set(False) # hide cursor
1077 curses.start_color()
1078 self.set_default_colors()
1079 curses.init_pair(1, 1, 2)
1082 self.explorer = YX(0, 0)
1085 interval = datetime.timedelta(seconds=5)
1086 last_ping = datetime.datetime.now() - interval
1088 if self.disconnected and self.force_instant_connect:
1089 self.force_instant_connect = False
1091 now = datetime.datetime.now()
1092 if now - last_ping > interval:
1093 if self.disconnected:
1103 self.do_refresh = False
1106 msg = self.queue.get(block=False)
1111 key = stdscr.getkey()
1112 self.do_refresh = True
1113 except curses.error:
1118 self.show_help = False
1119 self.draw_face = False
1120 if key == 'KEY_RESIZE':
1122 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1123 self.input_ = self.input_[:-1]
1124 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1125 or (self.mode.has_input_prompt and key == '\n'
1126 and self.input_ == ''\
1127 and self.mode.name in {'chat', 'command_thing',
1128 'take_thing', 'drop_thing',
1130 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1131 self.log_msg('@ aborted')
1132 self.switch_mode('play')
1133 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1134 self.show_help = True
1136 self.restore_input_values()
1137 elif self.mode.has_input_prompt and key != '\n': # Return key
1139 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1140 if len(self.input_) > max_length:
1141 self.input_ = self.input_[:max_length]
1142 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1143 self.show_help = True
1144 elif self.mode.name == 'login' and key == '\n':
1145 self.login_name = self.input_
1146 self.send('LOGIN ' + quote(self.input_))
1148 elif self.mode.name == 'enter_face' and key == '\n':
1149 if len(self.input_) != 18:
1150 self.log_msg('? wrong input length, aborting')
1152 self.send('PLAYER_FACE %s' % quote(self.input_))
1154 self.switch_mode('edit')
1155 elif self.mode.name == 'take_thing' and key == '\n':
1156 pick_selectable('PICK_UP')
1157 elif self.mode.name == 'drop_thing' and key == '\n':
1158 pick_selectable('DROP')
1159 elif self.mode.name == 'command_thing' and key == '\n':
1160 self.send('TASK:COMMAND ' + quote(self.input_))
1162 elif self.mode.name == 'control_pw_pw' and key == '\n':
1163 if self.input_ == '':
1164 self.log_msg('@ aborted')
1166 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1167 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1168 self.switch_mode('admin')
1169 elif self.mode.name == 'password' and key == '\n':
1170 if self.input_ == '':
1172 self.password = self.input_
1173 self.switch_mode('edit')
1174 elif self.mode.name == 'admin_enter' and key == '\n':
1175 self.send('BECOME_ADMIN ' + quote(self.input_))
1176 self.switch_mode('play')
1177 elif self.mode.name == 'control_pw_type' and key == '\n':
1178 if len(self.input_) != 1:
1179 self.log_msg('@ entered non-single-char, therefore aborted')
1180 self.switch_mode('admin')
1182 self.tile_control_char = self.input_
1183 self.switch_mode('control_pw_pw')
1184 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1185 if len(self.input_) != 1:
1186 self.log_msg('@ entered non-single-char, therefore aborted')
1188 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1189 quote(self.input_)))
1190 self.log_msg('@ sent new protection character for thing')
1191 self.switch_mode('admin')
1192 elif self.mode.name == 'control_tile_type' and key == '\n':
1193 if len(self.input_) != 1:
1194 self.log_msg('@ entered non-single-char, therefore aborted')
1195 self.switch_mode('admin')
1197 self.tile_control_char = self.input_
1198 self.switch_mode('control_tile_draw')
1199 elif self.mode.name == 'chat' and key == '\n':
1200 if self.input_ == '':
1202 if self.input_[0] == '/':
1203 if self.input_.startswith('/nick'):
1204 tokens = self.input_.split(maxsplit=1)
1205 if len(tokens) == 2:
1206 self.send('NICK ' + quote(tokens[1]))
1208 self.log_msg('? need login name')
1210 self.log_msg('? unknown command')
1212 self.send('ALL ' + quote(self.input_))
1214 elif self.mode.name == 'name_thing' and key == '\n':
1215 if self.input_ == '':
1217 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1219 quote(self.password)))
1220 self.switch_mode('edit')
1221 elif self.mode.name == 'annotate' and key == '\n':
1222 if self.input_ == '':
1224 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1225 quote(self.password)))
1226 self.switch_mode('edit')
1227 elif self.mode.name == 'portal' and key == '\n':
1228 if self.input_ == '':
1230 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1231 quote(self.password)))
1232 self.switch_mode('edit')
1233 elif self.mode.name == 'study':
1234 if self.mode.mode_switch_on_key(self, key):
1236 elif key == self.keys['toggle_map_mode']:
1237 self.toggle_map_mode()
1238 elif key in self.movement_keys:
1239 move_explorer(self.movement_keys[key])
1240 elif self.mode.name == 'play':
1241 if self.mode.mode_switch_on_key(self, key):
1243 elif key == self.keys['door'] and task_action_on('door'):
1244 self.send('TASK:DOOR')
1245 elif key == self.keys['consume'] and task_action_on('consume'):
1246 self.send('TASK:INTOXICATE')
1247 elif key == self.keys['wear'] and task_action_on('wear'):
1248 self.send('TASK:WEAR')
1249 elif key == self.keys['spin'] and task_action_on('spin'):
1250 self.send('TASK:SPIN')
1251 elif key == self.keys['teleport']:
1252 if self.game.player.position in self.game.portals:
1253 self.host = self.game.portals[self.game.player.position]
1257 self.log_msg('? not standing on portal')
1258 elif key in self.movement_keys and task_action_on('move'):
1259 self.send('TASK:MOVE ' + self.movement_keys[key])
1260 elif self.mode.name == 'write':
1261 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1262 self.switch_mode('edit')
1263 elif self.mode.name == 'control_tile_draw':
1264 if self.mode.mode_switch_on_key(self, key):
1266 elif key in self.movement_keys:
1267 move_explorer(self.movement_keys[key])
1268 elif key == self.keys['toggle_tile_draw']:
1269 self.tile_draw = False if self.tile_draw else True
1270 elif self.mode.name == 'admin':
1271 if self.mode.mode_switch_on_key(self, key):
1273 elif key in self.movement_keys and task_action_on('move'):
1274 self.send('TASK:MOVE ' + self.movement_keys[key])
1275 elif self.mode.name == 'edit':
1276 if self.mode.mode_switch_on_key(self, key):
1278 elif key == self.keys['flatten'] and task_action_on('flatten'):
1279 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1280 elif key == self.keys['install'] and task_action_on('install'):
1281 self.send('TASK:INSTALL %s' % quote(self.password))
1282 elif key == self.keys['toggle_map_mode']:
1283 self.toggle_map_mode()
1284 elif key in self.movement_keys and task_action_on('move'):
1285 self.send('TASK:MOVE ' + self.movement_keys[key])
1287 if len(sys.argv) != 2:
1288 raise ArgError('wrong number of arguments, need game host')