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.'
44 'admin_thing_protect': {
45 'short': 'change thing protection',
46 'intro': '@ enter thing protection character:',
47 'long': 'Change protection character for thing here.'
50 'short': 'change terrain',
52 '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.'
55 'short': 'change protection character password',
56 'intro': '@ enter protection character for which you want to change the password:',
57 '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.'
60 'short': 'change protection character password',
62 '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.'
64 'control_tile_type': {
65 'short': 'change tiles protection',
66 'intro': '@ enter protection character which you want to draw:',
67 '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.'
69 'control_tile_draw': {
70 'short': 'change tiles protection',
72 '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.'
75 'short': 'annotate tile',
77 '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.'
80 'short': 'edit portal',
82 '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.'
87 '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'
92 'long': 'Enter your player name.'
94 'waiting_for_server': {
95 'short': 'waiting for server response',
96 'intro': '@ waiting for server …',
97 'long': 'Waiting for a server response.'
100 'short': 'waiting for server response',
102 'long': 'Waiting for a server response.'
105 'short': 'set world edit password',
107 '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.'
110 'short': 'become admin',
111 'intro': '@ enter admin password:',
112 'long': 'This mode allows you to become admin if you know an admin password.'
117 'long': 'This mode allows you access to actions limited to administrators.'
121 from ws4py.client import WebSocketBaseClient
122 class WebSocketClient(WebSocketBaseClient):
124 def __init__(self, recv_handler, *args, **kwargs):
125 super().__init__(*args, **kwargs)
126 self.recv_handler = recv_handler
129 def received_message(self, message):
131 message = str(message)
132 self.recv_handler(message)
135 def plom_closed(self):
136 return self.client_terminated
138 from plomrogue.io_tcp import PlomSocket
139 class PlomSocketClient(PlomSocket):
141 def __init__(self, recv_handler, url):
143 self.recv_handler = recv_handler
144 host, port = url.split(':')
145 super().__init__(socket.create_connection((host, port)))
153 for msg in self.recv():
154 if msg == 'NEED_SSL':
155 self.socket = ssl.wrap_socket(self.socket)
157 self.recv_handler(msg)
158 except BrokenSocketConnection:
159 pass # we assume socket will be known as dead by now
161 def cmd_TURN(game, n):
162 game.annotations = {}
166 game.turn_complete = False
168 cmd_TURN.argtypes = 'int:nonneg'
170 def cmd_LOGIN_OK(game):
171 game.tui.switch_mode('post_login_wait')
172 game.tui.send('GET_GAMESTATE')
173 game.tui.log_msg('@ welcome')
174 cmd_LOGIN_OK.argtypes = ''
176 def cmd_ADMIN_OK(game):
177 game.tui.is_admin = True
178 game.tui.log_msg('@ you now have admin rights')
179 game.tui.switch_mode('admin')
180 game.tui.do_refresh = True
181 cmd_ADMIN_OK.argtypes = ''
183 def cmd_REPLY(game, msg):
184 game.tui.log_msg('#MUSICPLAYER: ' + msg)
185 game.tui.do_refresh = True
186 cmd_REPLY.argtypes = 'string'
188 def cmd_CHAT(game, msg):
189 game.tui.log_msg('# ' + msg)
190 game.tui.do_refresh = True
191 cmd_CHAT.argtypes = 'string'
193 def cmd_PLAYER_ID(game, player_id):
194 game.player_id = player_id
195 cmd_PLAYER_ID.argtypes = 'int:nonneg'
197 def cmd_THING(game, yx, thing_type, protection, thing_id, portable):
198 t = game.get_thing(thing_id)
200 t = ThingBase(game, thing_id)
204 t.protection = protection
205 t.portable = portable
206 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool'
208 def cmd_THING_NAME(game, thing_id, name):
209 t = game.get_thing(thing_id)
212 cmd_THING_NAME.argtypes = 'int:nonneg string'
214 def cmd_THING_CHAR(game, thing_id, c):
215 t = game.get_thing(thing_id)
218 cmd_THING_CHAR.argtypes = 'int:nonneg char'
220 def cmd_MAP(game, geometry, size, content):
221 map_geometry_class = globals()['MapGeometry' + geometry]
222 game.map_geometry = map_geometry_class(size)
223 game.map_content = content
224 if type(game.map_geometry) == MapGeometrySquare:
225 game.tui.movement_keys = {
226 game.tui.keys['square_move_up']: 'UP',
227 game.tui.keys['square_move_left']: 'LEFT',
228 game.tui.keys['square_move_down']: 'DOWN',
229 game.tui.keys['square_move_right']: 'RIGHT',
231 elif type(game.map_geometry) == MapGeometryHex:
232 game.tui.movement_keys = {
233 game.tui.keys['hex_move_upleft']: 'UPLEFT',
234 game.tui.keys['hex_move_upright']: 'UPRIGHT',
235 game.tui.keys['hex_move_right']: 'RIGHT',
236 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
237 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
238 game.tui.keys['hex_move_left']: 'LEFT',
240 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
242 def cmd_FOV(game, content):
244 cmd_FOV.argtypes = 'string'
246 def cmd_MAP_CONTROL(game, content):
247 game.map_control_content = content
248 cmd_MAP_CONTROL.argtypes = 'string'
250 def cmd_GAME_STATE_COMPLETE(game):
251 if game.tui.mode.name == 'post_login_wait':
252 game.tui.switch_mode('play')
253 game.turn_complete = True
254 game.tui.do_refresh = True
255 game.tui.info_cached = None
256 cmd_GAME_STATE_COMPLETE.argtypes = ''
258 def cmd_PORTAL(game, position, msg):
259 game.portals[position] = msg
260 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
262 def cmd_PLAY_ERROR(game, msg):
263 game.tui.log_msg('? ' + msg)
264 game.tui.flash = True
265 game.tui.do_refresh = True
266 cmd_PLAY_ERROR.argtypes = 'string'
268 def cmd_GAME_ERROR(game, msg):
269 game.tui.log_msg('? game error: ' + msg)
270 game.tui.do_refresh = True
271 cmd_GAME_ERROR.argtypes = 'string'
273 def cmd_ARGUMENT_ERROR(game, msg):
274 game.tui.log_msg('? syntax error: ' + msg)
275 game.tui.do_refresh = True
276 cmd_ARGUMENT_ERROR.argtypes = 'string'
278 def cmd_ANNOTATION(game, position, msg):
279 game.annotations[position] = msg
280 if game.tui.mode.shows_info:
281 game.tui.do_refresh = True
282 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
284 def cmd_TASKS(game, tasks_comma_separated):
285 game.tasks = tasks_comma_separated.split(',')
286 game.tui.mode_write.legal = 'WRITE' in game.tasks
287 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
288 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
289 cmd_TASKS.argtypes = 'string'
291 def cmd_THING_TYPE(game, thing_type, symbol_hint):
292 game.thing_types[thing_type] = symbol_hint
293 cmd_THING_TYPE.argtypes = 'string char'
295 def cmd_THING_CARRYING(game, thing_id):
296 game.get_thing(thing_id).carrying = True
297 cmd_THING_CARRYING.argtypes = 'int:nonneg'
299 def cmd_TERRAIN(game, terrain_char, terrain_desc):
300 game.terrains[terrain_char] = terrain_desc
301 cmd_TERRAIN.argtypes = 'char string'
305 cmd_PONG.argtypes = ''
307 def cmd_DEFAULT_COLORS(game):
308 game.tui.set_default_colors()
309 cmd_DEFAULT_COLORS.argtypes = ''
311 def cmd_RANDOM_COLORS(game):
312 game.tui.set_random_colors()
313 cmd_RANDOM_COLORS.argtypes = ''
315 class Game(GameBase):
316 turn_complete = False
320 def __init__(self, *args, **kwargs):
321 super().__init__(*args, **kwargs)
322 self.register_command(cmd_LOGIN_OK)
323 self.register_command(cmd_ADMIN_OK)
324 self.register_command(cmd_PONG)
325 self.register_command(cmd_CHAT)
326 self.register_command(cmd_REPLY)
327 self.register_command(cmd_PLAYER_ID)
328 self.register_command(cmd_TURN)
329 self.register_command(cmd_THING)
330 self.register_command(cmd_THING_TYPE)
331 self.register_command(cmd_THING_NAME)
332 self.register_command(cmd_THING_CHAR)
333 self.register_command(cmd_THING_CARRYING)
334 self.register_command(cmd_TERRAIN)
335 self.register_command(cmd_MAP)
336 self.register_command(cmd_MAP_CONTROL)
337 self.register_command(cmd_PORTAL)
338 self.register_command(cmd_ANNOTATION)
339 self.register_command(cmd_GAME_STATE_COMPLETE)
340 self.register_command(cmd_ARGUMENT_ERROR)
341 self.register_command(cmd_GAME_ERROR)
342 self.register_command(cmd_PLAY_ERROR)
343 self.register_command(cmd_TASKS)
344 self.register_command(cmd_FOV)
345 self.register_command(cmd_DEFAULT_COLORS)
346 self.register_command(cmd_RANDOM_COLORS)
347 self.map_content = ''
349 self.annotations = {}
353 def get_string_options(self, string_option_type):
354 if string_option_type == 'map_geometry':
355 return ['Hex', 'Square']
356 elif string_option_type == 'thing_type':
357 return self.thing_types.keys()
360 def get_command(self, command_name):
361 from functools import partial
362 f = partial(self.commands[command_name], self)
363 f.argtypes = self.commands[command_name].argtypes
368 def __init__(self, name, has_input_prompt=False, shows_info=False,
369 is_intro=False, is_single_char_entry=False):
371 self.short_desc = mode_helps[name]['short']
372 self.available_modes = []
373 self.available_actions = []
374 self.has_input_prompt = has_input_prompt
375 self.shows_info = shows_info
376 self.is_intro = is_intro
377 self.help_intro = mode_helps[name]['long']
378 self.intro_msg = mode_helps[name]['intro']
379 self.is_single_char_entry = is_single_char_entry
382 def iter_available_modes(self, tui):
383 for mode_name in self.available_modes:
384 mode = getattr(tui, 'mode_' + mode_name)
387 key = tui.keys['switch_to_' + mode.name]
390 def list_available_modes(self, tui):
392 if len(self.available_modes) > 0:
393 msg = 'Other modes available from here:\n'
394 for mode, key in self.iter_available_modes(tui):
395 msg += '[%s] – %s\n' % (key, mode.short_desc)
398 def mode_switch_on_key(self, tui, key_pressed):
399 for mode, key in self.iter_available_modes(tui):
400 if key_pressed == key:
401 tui.switch_mode(mode.name)
406 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
407 mode_admin = Mode('admin')
408 mode_play = Mode('play')
409 mode_study = Mode('study', shows_info=True)
410 mode_write = Mode('write', is_single_char_entry=True)
411 mode_edit = Mode('edit')
412 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
413 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
414 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
415 mode_control_tile_draw = Mode('control_tile_draw')
416 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
417 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
418 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
419 mode_chat = Mode('chat', has_input_prompt=True)
420 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
421 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
422 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
423 mode_password = Mode('password', has_input_prompt=True)
424 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
425 mode_command_thing = Mode('command_thing', has_input_prompt=True)
426 mode_take_thing = Mode('take_thing', has_input_prompt=True)
430 def __init__(self, host):
433 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
434 "command_thing", "take_thing"]
435 self.mode_play.available_actions = ["move", "drop_thing",
436 "teleport", "door", "consume",
438 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
439 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
440 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
441 "control_tile_type", "chat",
442 "study", "play", "edit"]
443 self.mode_admin.available_actions = ["move"]
444 self.mode_control_tile_draw.available_modes = ["admin_enter"]
445 self.mode_control_tile_draw.available_actions = ["move_explorer",
447 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
448 "password", "chat", "study", "play",
450 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
455 self.parser = Parser(self.game)
457 self.do_refresh = True
458 self.queue = queue.Queue()
459 self.login_name = None
460 self.map_mode = 'terrain + things'
461 self.password = 'foo'
462 self.switch_mode('waiting_for_server')
464 'switch_to_chat': 't',
465 'switch_to_play': 'p',
466 'switch_to_password': 'P',
467 'switch_to_annotate': 'M',
468 'switch_to_portal': 'T',
469 'switch_to_study': '?',
470 'switch_to_edit': 'E',
471 'switch_to_write': 'm',
472 'switch_to_name_thing': 'N',
473 'switch_to_command_thing': 'O',
474 'switch_to_admin_enter': 'A',
475 'switch_to_control_pw_type': 'C',
476 'switch_to_control_tile_type': 'Q',
477 'switch_to_admin_thing_protect': 'T',
479 'switch_to_take_thing': 'z',
486 'toggle_map_mode': 'L',
487 'toggle_tile_draw': 'm',
488 'hex_move_upleft': 'w',
489 'hex_move_upright': 'e',
490 'hex_move_right': 'd',
491 'hex_move_downright': 'x',
492 'hex_move_downleft': 'y',
493 'hex_move_left': 'a',
494 'square_move_up': 'w',
495 'square_move_left': 'a',
496 'square_move_down': 's',
497 'square_move_right': 'd',
499 if os.path.isfile('config.json'):
500 with open('config.json', 'r') as f:
501 keys_conf = json.loads(f.read())
503 self.keys[k] = keys_conf[k]
504 self.show_help = False
505 self.disconnected = True
506 self.force_instant_connect = True
507 self.input_lines = []
511 self.offset = YX(0,0)
512 curses.wrapper(self.loop)
516 def handle_recv(msg):
522 self.log_msg('@ attempting connect')
523 socket_client_class = PlomSocketClient
524 if self.host.startswith('ws://') or self.host.startswith('wss://'):
525 socket_client_class = WebSocketClient
527 self.socket = socket_client_class(handle_recv, self.host)
528 self.socket_thread = threading.Thread(target=self.socket.run)
529 self.socket_thread.start()
530 self.disconnected = False
531 self.game.thing_types = {}
532 self.game.terrains = {}
533 time.sleep(0.1) # give potential SSL negotation some time …
534 self.socket.send('TASKS')
535 self.socket.send('TERRAINS')
536 self.socket.send('THING_TYPES')
537 self.switch_mode('login')
538 except ConnectionRefusedError:
539 self.log_msg('@ server connect failure')
540 self.disconnected = True
541 self.switch_mode('waiting_for_server')
542 self.do_refresh = True
545 self.log_msg('@ attempting reconnect')
547 # necessitated by some strange SSL race conditions with ws4py
548 time.sleep(0.1) # FIXME find out why exactly necessary
549 self.switch_mode('waiting_for_server')
554 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
555 raise BrokenSocketConnection
556 self.socket.send(msg)
557 except (BrokenPipeError, BrokenSocketConnection):
558 self.log_msg('@ server disconnected :(')
559 self.disconnected = True
560 self.force_instant_connect = True
561 self.do_refresh = True
563 def log_msg(self, msg):
565 if len(self.log) > 100:
566 self.log = self.log[-100:]
568 def restore_input_values(self):
569 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
570 self.input_ = self.game.annotations[self.explorer]
571 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
572 self.input_ = self.game.portals[self.explorer]
573 elif self.mode.name == 'password':
574 self.input_ = self.password
575 elif self.mode.name == 'name_thing':
576 if hasattr(self.thing_selected, 'name'):
577 self.input_ = self.thing_selected.name
578 elif self.mode.name == 'admin_thing_protect':
579 if hasattr(self.thing_selected, 'protection'):
580 self.input_ = self.thing_selected.protection
582 def send_tile_control_command(self):
583 self.send('SET_TILE_CONTROL %s %s' %
584 (self.explorer, quote(self.tile_control_char)))
586 def toggle_map_mode(self):
587 if self.map_mode == 'terrain only':
588 self.map_mode = 'terrain + annotations'
589 elif self.map_mode == 'terrain + annotations':
590 self.map_mode = 'terrain + things'
591 elif self.map_mode == 'terrain + things':
592 self.map_mode = 'protections'
593 elif self.map_mode == 'protections':
594 self.map_mode = 'terrain only'
596 def switch_mode(self, mode_name):
597 if self.mode and self.mode.name == 'control_tile_draw':
598 self.log_msg('@ finished tile protection drawing.')
599 self.tile_draw = False
600 if mode_name == 'admin_enter' and self.is_admin:
602 elif mode_name in {'name_thing', 'admin_thing_protect'}:
603 player = self.game.get_thing(self.game.player_id)
605 for t in [t for t in self.game.things if t.position == player.position
606 and t.id_ != player.id_]:
611 self.log_msg('? not standing over thing')
614 self.thing_selected = thing
615 self.mode = getattr(self, 'mode_' + mode_name)
616 if self.mode.name in {'control_tile_draw', 'control_tile_type',
618 self.map_mode = 'protections'
619 elif self.mode.name != 'edit':
620 self.map_mode = 'terrain + things'
621 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
622 player = self.game.get_thing(self.game.player_id)
623 self.explorer = YX(player.position.y, player.position.x)
624 if self.mode.is_single_char_entry:
625 self.show_help = True
626 if len(self.mode.intro_msg) > 0:
627 self.log_msg(self.mode.intro_msg)
628 if self.mode.name == 'login':
630 self.send('LOGIN ' + quote(self.login_name))
632 self.log_msg('@ enter username')
633 elif self.mode.name == 'take_thing':
634 self.log_msg('Portable things in reach for pick-up:')
635 player = self.game.get_thing(self.game.player_id)
636 select_range = [player.position,
637 player.position + YX(0,-1),
638 player.position + YX(0, 1),
639 player.position + YX(-1, 0),
640 player.position + YX(1, 0)]
641 if type(self.game.map_geometry) == MapGeometryHex:
642 if player.position.y % 2:
643 select_range += [player.position + YX(-1, 1),
644 player.position + YX(1, 1)]
646 select_range += [player.position + YX(-1, -1),
647 player.position + YX(1, -1)]
648 self.selectables = [t for t in self.game.things
649 if t.portable and t.position in select_range]
650 if len(self.selectables) == 0:
653 for i in range(len(self.selectables)):
654 t = self.selectables[i]
655 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
656 elif self.mode.name == 'command_thing':
657 self.send('TASK:COMMAND ' + quote('HELP'))
658 elif self.mode.name == 'control_pw_pw':
659 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
660 elif self.mode.name == 'control_tile_draw':
661 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']))
663 self.restore_input_values()
665 def set_default_colors(self):
666 curses.init_color(1, 1000, 1000, 1000)
667 curses.init_color(2, 0, 0, 0)
668 self.do_refresh = True
670 def set_random_colors(self):
674 return int(offset + random.random()*375)
676 curses.init_color(1, rand(625), rand(625), rand(625))
677 curses.init_color(2, rand(0), rand(0), rand(0))
678 self.do_refresh = True
682 return self.info_cached
683 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
685 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
686 info_to_cache += 'outside field of view'
688 terrain_char = self.game.map_content[pos_i]
690 if terrain_char in self.game.terrains:
691 terrain_desc = self.game.terrains[terrain_char]
692 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
694 protection = self.game.map_control_content[pos_i]
695 if protection == '.':
696 protection = 'unprotected'
697 info_to_cache += 'PROTECTION: %s\n' % protection
698 for t in self.game.things:
699 if t.position == self.explorer:
700 info_to_cache += 'THING: %s' % self.get_thing_info(t)
701 protection = t.protection
702 if protection == '.':
704 info_to_cache += ' / protection: %s\n' % protection
705 if self.explorer in self.game.portals:
706 info_to_cache += 'PORTAL: ' +\
707 self.game.portals[self.explorer] + '\n'
709 info_to_cache += 'PORTAL: (none)\n'
710 if self.explorer in self.game.annotations:
711 info_to_cache += 'ANNOTATION: ' +\
712 self.game.annotations[self.explorer]
713 self.info_cached = info_to_cache
714 return self.info_cached
716 def get_thing_info(self, t):
718 (t.type_, self.game.thing_types[t.type_])
719 if hasattr(t, 'thing_char'):
721 if hasattr(t, 'name'):
722 info += ' (%s)' % t.name
725 def loop(self, stdscr):
728 def safe_addstr(y, x, line):
729 if y < self.size.y - 1 or x + len(line) < self.size.x:
730 stdscr.addstr(y, x, line, curses.color_pair(1))
731 else: # workaround to <https://stackoverflow.com/q/7063128>
732 cut_i = self.size.x - x - 1
734 last_char = line[cut_i]
735 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
736 stdscr.insstr(y, self.size.x - 2, ' ')
737 stdscr.addstr(y, x, cut, curses.color_pair(1))
739 def handle_input(msg):
740 command, args = self.parser.parse(msg)
743 def task_action_on(action):
744 return action_tasks[action] in self.game.tasks
746 def msg_into_lines_of_width(msg, width):
750 for i in range(len(msg)):
751 if x >= width or msg[i] == "\n":
763 def reset_screen_size():
764 self.size = YX(*stdscr.getmaxyx())
765 self.size = self.size - YX(self.size.y % 4, 0)
766 self.size = self.size - YX(0, self.size.x % 4)
767 self.window_width = int(self.size.x / 2)
769 def recalc_input_lines():
770 if not self.mode.has_input_prompt:
771 self.input_lines = []
773 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
776 def move_explorer(direction):
777 target = self.game.map_geometry.move_yx(self.explorer, direction)
779 self.info_cached = None
780 self.explorer = target
782 self.send_tile_control_command()
788 for line in self.log:
789 lines += msg_into_lines_of_width(line, self.window_width)
792 max_y = self.size.y - len(self.input_lines)
793 for i in range(len(lines)):
794 if (i >= max_y - height_header):
796 safe_addstr(max_y - i - 1, self.window_width, lines[i])
799 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
800 lines = msg_into_lines_of_width(info, self.window_width)
802 for i in range(len(lines)):
803 y = height_header + i
804 if y >= self.size.y - len(self.input_lines):
806 safe_addstr(y, self.window_width, lines[i])
809 y = self.size.y - len(self.input_lines)
810 for i in range(len(self.input_lines)):
811 safe_addstr(y, self.window_width, self.input_lines[i])
815 if not self.game.turn_complete:
817 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
820 help = "hit [%s] for help" % self.keys['help']
821 if self.mode.has_input_prompt:
822 help = "enter /help for help"
823 safe_addstr(1, self.window_width,
824 'MODE: %s – %s' % (self.mode.short_desc, help))
827 if not self.game.turn_complete and len(self.map_lines) == 0:
829 if self.game.turn_complete:
831 for y in range(self.game.map_geometry.size.y):
832 start = self.game.map_geometry.size.x * y
833 end = start + self.game.map_geometry.size.x
834 if self.map_mode == 'protections':
835 map_lines_split += [[c + ' ' for c
836 in self.game.map_control_content[start:end]]]
838 map_lines_split += [[c + ' ' for c
839 in self.game.map_content[start:end]]]
840 if self.map_mode == 'terrain + annotations':
841 for p in self.game.annotations:
842 map_lines_split[p.y][p.x] = 'A '
843 elif self.map_mode == 'terrain + things':
844 for p in self.game.portals.keys():
845 original = map_lines_split[p.y][p.x]
846 map_lines_split[p.y][p.x] = original[0] + 'P'
849 def draw_thing(t, used_positions):
850 symbol = self.game.thing_types[t.type_]
852 if hasattr(t, 'thing_char'):
853 meta_char = t.thing_char
854 if t.position in used_positions:
856 if hasattr(t, 'carrying') and t.carrying:
858 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
859 used_positions += [t.position]
861 for t in [t for t in self.game.things if t.type_ != 'Player']:
862 draw_thing(t, used_positions)
863 for t in [t for t in self.game.things if t.type_ == 'Player']:
864 draw_thing(t, used_positions)
865 player = self.game.get_thing(self.game.player_id)
866 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
867 map_lines_split[self.explorer.y][self.explorer.x] = '??'
868 elif self.map_mode != 'terrain + things':
869 map_lines_split[player.position.y][player.position.x] = '??'
871 if type(self.game.map_geometry) == MapGeometryHex:
873 for line in map_lines_split:
874 self.map_lines += [indent * ' ' + ''.join(line)]
875 indent = 0 if indent else 1
877 for line in map_lines_split:
878 self.map_lines += [''.join(line)]
879 window_center = YX(int(self.size.y / 2),
880 int(self.window_width / 2))
881 center = player.position
882 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
883 center = self.explorer
884 center = YX(center.y, center.x * 2)
885 self.offset = center - window_center
886 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
887 self.offset += YX(0, 1)
888 term_y = max(0, -self.offset.y)
889 term_x = max(0, -self.offset.x)
890 map_y = max(0, self.offset.y)
891 map_x = max(0, self.offset.x)
892 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
893 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
894 safe_addstr(term_y, term_x, to_draw)
899 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
900 self.mode.help_intro)
901 if len(self.mode.available_actions) > 0:
902 content += "Available actions:\n"
903 for action in self.mode.available_actions:
904 if action in action_tasks:
905 if action_tasks[action] not in self.game.tasks:
907 if action == 'move_explorer':
910 key = ','.join(self.movement_keys)
912 key = self.keys[action]
913 content += '[%s] – %s\n' % (key, action_descriptions[action])
915 content += self.mode.list_available_modes(self)
916 for i in range(self.size.y):
918 self.window_width * (not self.mode.has_input_prompt),
919 ' ' * self.window_width)
921 for line in content.split('\n'):
922 lines += msg_into_lines_of_width(line, self.window_width)
923 for i in range(len(lines)):
927 self.window_width * (not self.mode.has_input_prompt),
932 stdscr.bkgd(' ', curses.color_pair(1))
934 if self.mode.has_input_prompt:
936 if self.mode.shows_info:
941 if not self.mode.is_intro:
947 action_descriptions = {
949 'flatten': 'flatten surroundings',
950 'teleport': 'teleport',
951 'take_thing': 'pick up thing',
952 'drop_thing': 'drop thing',
953 'toggle_map_mode': 'toggle map view',
954 'toggle_tile_draw': 'toggle protection character drawing',
955 'install': 'install',
956 'door': 'open/close',
957 'consume': 'consume',
961 'flatten': 'FLATTEN_SURROUNDINGS',
962 'take_thing': 'PICK_UP',
963 'drop_thing': 'DROP',
965 'install': 'INSTALL',
967 'command': 'COMMAND',
968 'consume': 'INTOXICATE',
971 curses.curs_set(False) # hide cursor
973 self.set_default_colors()
974 curses.init_pair(1, 1, 2)
977 self.explorer = YX(0, 0)
980 interval = datetime.timedelta(seconds=5)
981 last_ping = datetime.datetime.now() - interval
983 if self.disconnected and self.force_instant_connect:
984 self.force_instant_connect = False
986 now = datetime.datetime.now()
987 if now - last_ping > interval:
988 if self.disconnected:
998 self.do_refresh = False
1001 msg = self.queue.get(block=False)
1006 key = stdscr.getkey()
1007 self.do_refresh = True
1008 except curses.error:
1010 self.show_help = False
1011 if key == 'KEY_RESIZE':
1013 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1014 self.input_ = self.input_[:-1]
1015 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1016 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1018 if self.mode.name != 'chat':
1019 self.log_msg('@ aborted')
1020 self.switch_mode('play')
1021 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1022 self.show_help = True
1024 self.restore_input_values()
1025 elif self.mode.has_input_prompt and key != '\n': # Return key
1027 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1028 if len(self.input_) > max_length:
1029 self.input_ = self.input_[:max_length]
1030 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1031 self.show_help = True
1032 elif self.mode.name == 'login' and key == '\n':
1033 self.login_name = self.input_
1034 self.send('LOGIN ' + quote(self.input_))
1036 elif self.mode.name == 'take_thing' and key == '\n':
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:PICK_UP %s' % self.selectables[i].id_)
1044 self.log_msg('? invalid index, aborted')
1046 self.switch_mode('play')
1047 elif self.mode.name == 'command_thing' and key == '\n':
1048 if task_action_on('command'):
1049 self.send('TASK:COMMAND ' + quote(self.input_))
1051 elif self.mode.name == 'control_pw_pw' and key == '\n':
1052 if self.input_ == '':
1053 self.log_msg('@ aborted')
1055 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1056 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1057 self.switch_mode('admin')
1058 elif self.mode.name == 'password' and key == '\n':
1059 if self.input_ == '':
1061 self.password = self.input_
1062 self.switch_mode('edit')
1063 elif self.mode.name == 'admin_enter' and key == '\n':
1064 self.send('BECOME_ADMIN ' + quote(self.input_))
1065 self.switch_mode('play')
1066 elif self.mode.name == 'control_pw_type' and key == '\n':
1067 if len(self.input_) != 1:
1068 self.log_msg('@ entered non-single-char, therefore aborted')
1069 self.switch_mode('admin')
1071 self.tile_control_char = self.input_
1072 self.switch_mode('control_pw_pw')
1073 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1074 if len(self.input_) != 1:
1075 self.log_msg('@ entered non-single-char, therefore aborted')
1077 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1078 quote(self.input_)))
1079 self.log_msg('@ sent new protection character for thing')
1080 self.switch_mode('admin')
1081 elif self.mode.name == 'control_tile_type' and key == '\n':
1082 if len(self.input_) != 1:
1083 self.log_msg('@ entered non-single-char, therefore aborted')
1084 self.switch_mode('admin')
1086 self.tile_control_char = self.input_
1087 self.switch_mode('control_tile_draw')
1088 elif self.mode.name == 'chat' and key == '\n':
1089 if self.input_ == '':
1091 if self.input_[0] == '/':
1092 if self.input_.startswith('/nick'):
1093 tokens = self.input_.split(maxsplit=1)
1094 if len(tokens) == 2:
1095 self.send('NICK ' + quote(tokens[1]))
1097 self.log_msg('? need login name')
1099 self.log_msg('? unknown command')
1101 self.send('ALL ' + quote(self.input_))
1103 elif self.mode.name == 'name_thing' and key == '\n':
1104 if self.input_ == '':
1106 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1108 quote(self.password)))
1109 self.switch_mode('edit')
1110 elif self.mode.name == 'annotate' and key == '\n':
1111 if self.input_ == '':
1113 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1114 quote(self.password)))
1115 self.switch_mode('edit')
1116 elif self.mode.name == 'portal' and key == '\n':
1117 if self.input_ == '':
1119 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1120 quote(self.password)))
1121 self.switch_mode('edit')
1122 elif self.mode.name == 'study':
1123 if self.mode.mode_switch_on_key(self, key):
1125 elif key == self.keys['toggle_map_mode']:
1126 self.toggle_map_mode()
1127 elif key in self.movement_keys:
1128 move_explorer(self.movement_keys[key])
1129 elif self.mode.name == 'play':
1130 if self.mode.mode_switch_on_key(self, key):
1132 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1133 self.send('TASK:DROP')
1134 elif key == self.keys['door'] and task_action_on('door'):
1135 self.send('TASK:DOOR')
1136 elif key == self.keys['consume'] and task_action_on('consume'):
1137 self.send('TASK:INTOXICATE')
1138 elif key == self.keys['install'] and task_action_on('install'):
1139 self.send('TASK:INSTALL')
1140 elif key == self.keys['teleport']:
1141 player = self.game.get_thing(self.game.player_id)
1142 if player.position in self.game.portals:
1143 self.host = self.game.portals[player.position]
1147 self.log_msg('? not standing on portal')
1148 elif key in self.movement_keys and task_action_on('move'):
1149 self.send('TASK:MOVE ' + self.movement_keys[key])
1150 elif self.mode.name == 'write':
1151 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1152 self.switch_mode('edit')
1153 elif self.mode.name == 'control_tile_draw':
1154 if self.mode.mode_switch_on_key(self, key):
1156 elif key in self.movement_keys:
1157 move_explorer(self.movement_keys[key])
1158 elif key == self.keys['toggle_tile_draw']:
1159 self.tile_draw = False if self.tile_draw else True
1160 elif self.mode.name == 'admin':
1161 if self.mode.mode_switch_on_key(self, key):
1163 elif key in self.movement_keys and task_action_on('move'):
1164 self.send('TASK:MOVE ' + self.movement_keys[key])
1165 elif self.mode.name == 'edit':
1166 if self.mode.mode_switch_on_key(self, key):
1168 elif key == self.keys['flatten'] and task_action_on('flatten'):
1169 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1170 elif key == self.keys['toggle_map_mode']:
1171 self.toggle_map_mode()
1172 elif key in self.movement_keys and task_action_on('move'):
1173 self.send('TASK:MOVE ' + self.movement_keys[key])
1175 if len(sys.argv) != 2:
1176 raise ArgError('wrong number of arguments, need game host')