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):
198 t = game.get_thing(thing_id)
200 t = ThingBase(game, thing_id)
204 t.protection = protection
205 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg'
207 def cmd_THING_NAME(game, thing_id, name):
208 t = game.get_thing(thing_id)
211 cmd_THING_NAME.argtypes = 'int:nonneg string'
213 def cmd_THING_CHAR(game, thing_id, c):
214 t = game.get_thing(thing_id)
217 cmd_THING_CHAR.argtypes = 'int:nonneg char'
219 def cmd_MAP(game, geometry, size, content):
220 map_geometry_class = globals()['MapGeometry' + geometry]
221 game.map_geometry = map_geometry_class(size)
222 game.map_content = content
223 if type(game.map_geometry) == MapGeometrySquare:
224 game.tui.movement_keys = {
225 game.tui.keys['square_move_up']: 'UP',
226 game.tui.keys['square_move_left']: 'LEFT',
227 game.tui.keys['square_move_down']: 'DOWN',
228 game.tui.keys['square_move_right']: 'RIGHT',
230 elif type(game.map_geometry) == MapGeometryHex:
231 game.tui.movement_keys = {
232 game.tui.keys['hex_move_upleft']: 'UPLEFT',
233 game.tui.keys['hex_move_upright']: 'UPRIGHT',
234 game.tui.keys['hex_move_right']: 'RIGHT',
235 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
236 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
237 game.tui.keys['hex_move_left']: 'LEFT',
239 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
241 def cmd_FOV(game, content):
243 cmd_FOV.argtypes = 'string'
245 def cmd_MAP_CONTROL(game, content):
246 game.map_control_content = content
247 cmd_MAP_CONTROL.argtypes = 'string'
249 def cmd_GAME_STATE_COMPLETE(game):
250 if game.tui.mode.name == 'post_login_wait':
251 game.tui.switch_mode('play')
252 game.turn_complete = True
253 game.tui.do_refresh = True
254 game.tui.info_cached = None
255 cmd_GAME_STATE_COMPLETE.argtypes = ''
257 def cmd_PORTAL(game, position, msg):
258 game.portals[position] = msg
259 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
261 def cmd_PLAY_ERROR(game, msg):
262 game.tui.log_msg('? ' + msg)
263 game.tui.flash = True
264 game.tui.do_refresh = True
265 cmd_PLAY_ERROR.argtypes = 'string'
267 def cmd_GAME_ERROR(game, msg):
268 game.tui.log_msg('? game error: ' + msg)
269 game.tui.do_refresh = True
270 cmd_GAME_ERROR.argtypes = 'string'
272 def cmd_ARGUMENT_ERROR(game, msg):
273 game.tui.log_msg('? syntax error: ' + msg)
274 game.tui.do_refresh = True
275 cmd_ARGUMENT_ERROR.argtypes = 'string'
277 def cmd_ANNOTATION(game, position, msg):
278 game.annotations[position] = msg
279 if game.tui.mode.shows_info:
280 game.tui.do_refresh = True
281 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
283 def cmd_TASKS(game, tasks_comma_separated):
284 game.tasks = tasks_comma_separated.split(',')
285 game.tui.mode_write.legal = 'WRITE' in game.tasks
286 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
287 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
288 cmd_TASKS.argtypes = 'string'
290 def cmd_THING_TYPE(game, thing_type, symbol_hint):
291 game.thing_types[thing_type] = symbol_hint
292 cmd_THING_TYPE.argtypes = 'string char'
294 def cmd_THING_CARRYING(game, thing_id):
295 game.get_thing(thing_id).carrying = True
296 cmd_THING_CARRYING.argtypes = 'int:nonneg'
298 def cmd_TERRAIN(game, terrain_char, terrain_desc):
299 game.terrains[terrain_char] = terrain_desc
300 cmd_TERRAIN.argtypes = 'char string'
304 cmd_PONG.argtypes = ''
306 def cmd_DEFAULT_COLORS(game):
307 game.tui.set_default_colors()
308 cmd_DEFAULT_COLORS.argtypes = ''
310 def cmd_RANDOM_COLORS(game):
311 game.tui.set_random_colors()
312 cmd_RANDOM_COLORS.argtypes = ''
314 class Game(GameBase):
315 turn_complete = False
319 def __init__(self, *args, **kwargs):
320 super().__init__(*args, **kwargs)
321 self.register_command(cmd_LOGIN_OK)
322 self.register_command(cmd_ADMIN_OK)
323 self.register_command(cmd_PONG)
324 self.register_command(cmd_CHAT)
325 self.register_command(cmd_REPLY)
326 self.register_command(cmd_PLAYER_ID)
327 self.register_command(cmd_TURN)
328 self.register_command(cmd_THING)
329 self.register_command(cmd_THING_TYPE)
330 self.register_command(cmd_THING_NAME)
331 self.register_command(cmd_THING_CHAR)
332 self.register_command(cmd_THING_CARRYING)
333 self.register_command(cmd_TERRAIN)
334 self.register_command(cmd_MAP)
335 self.register_command(cmd_MAP_CONTROL)
336 self.register_command(cmd_PORTAL)
337 self.register_command(cmd_ANNOTATION)
338 self.register_command(cmd_GAME_STATE_COMPLETE)
339 self.register_command(cmd_ARGUMENT_ERROR)
340 self.register_command(cmd_GAME_ERROR)
341 self.register_command(cmd_PLAY_ERROR)
342 self.register_command(cmd_TASKS)
343 self.register_command(cmd_FOV)
344 self.register_command(cmd_DEFAULT_COLORS)
345 self.register_command(cmd_RANDOM_COLORS)
346 self.map_content = ''
348 self.annotations = {}
352 def get_string_options(self, string_option_type):
353 if string_option_type == 'map_geometry':
354 return ['Hex', 'Square']
355 elif string_option_type == 'thing_type':
356 return self.thing_types.keys()
359 def get_command(self, command_name):
360 from functools import partial
361 f = partial(self.commands[command_name], self)
362 f.argtypes = self.commands[command_name].argtypes
367 def __init__(self, name, has_input_prompt=False, shows_info=False,
368 is_intro=False, is_single_char_entry=False):
370 self.short_desc = mode_helps[name]['short']
371 self.available_modes = []
372 self.available_actions = []
373 self.has_input_prompt = has_input_prompt
374 self.shows_info = shows_info
375 self.is_intro = is_intro
376 self.help_intro = mode_helps[name]['long']
377 self.intro_msg = mode_helps[name]['intro']
378 self.is_single_char_entry = is_single_char_entry
381 def iter_available_modes(self, tui):
382 for mode_name in self.available_modes:
383 mode = getattr(tui, 'mode_' + mode_name)
386 key = tui.keys['switch_to_' + mode.name]
389 def list_available_modes(self, tui):
391 if len(self.available_modes) > 0:
392 msg = 'Other modes available from here:\n'
393 for mode, key in self.iter_available_modes(tui):
394 msg += '[%s] – %s\n' % (key, mode.short_desc)
397 def mode_switch_on_key(self, tui, key_pressed):
398 for mode, key in self.iter_available_modes(tui):
399 if key_pressed == key:
400 tui.switch_mode(mode.name)
405 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
406 mode_admin = Mode('admin')
407 mode_play = Mode('play')
408 mode_study = Mode('study', shows_info=True)
409 mode_write = Mode('write', is_single_char_entry=True)
410 mode_edit = Mode('edit')
411 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
412 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
413 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
414 mode_control_tile_draw = Mode('control_tile_draw')
415 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
416 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
417 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
418 mode_chat = Mode('chat', has_input_prompt=True)
419 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
420 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
421 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
422 mode_password = Mode('password', has_input_prompt=True)
423 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
424 mode_command_thing = Mode('command_thing', has_input_prompt=True)
425 mode_take_thing = Mode('take_thing', has_input_prompt=True)
429 def __init__(self, host):
432 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
433 "command_thing", "take_thing"]
434 self.mode_play.available_actions = ["move", "drop_thing",
435 "teleport", "door", "consume"]
436 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
437 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
438 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
439 "control_tile_type", "chat",
440 "study", "play", "edit"]
441 self.mode_admin.available_actions = ["move"]
442 self.mode_control_tile_draw.available_modes = ["admin_enter"]
443 self.mode_control_tile_draw.available_actions = ["move_explorer",
445 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
446 "password", "chat", "study", "play",
448 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
453 self.parser = Parser(self.game)
455 self.do_refresh = True
456 self.queue = queue.Queue()
457 self.login_name = None
458 self.map_mode = 'terrain + things'
459 self.password = 'foo'
460 self.switch_mode('waiting_for_server')
462 'switch_to_chat': 't',
463 'switch_to_play': 'p',
464 'switch_to_password': 'P',
465 'switch_to_annotate': 'M',
466 'switch_to_portal': 'T',
467 'switch_to_study': '?',
468 'switch_to_edit': 'E',
469 'switch_to_write': 'm',
470 'switch_to_name_thing': 'N',
471 'switch_to_command_thing': 'O',
472 'switch_to_admin_enter': 'A',
473 'switch_to_control_pw_type': 'C',
474 'switch_to_control_tile_type': 'Q',
475 'switch_to_admin_thing_protect': 'T',
477 'switch_to_take_thing': 'z',
483 'toggle_map_mode': 'L',
484 'toggle_tile_draw': 'm',
485 'hex_move_upleft': 'w',
486 'hex_move_upright': 'e',
487 'hex_move_right': 'd',
488 'hex_move_downright': 'x',
489 'hex_move_downleft': 'y',
490 'hex_move_left': 'a',
491 'square_move_up': 'w',
492 'square_move_left': 'a',
493 'square_move_down': 's',
494 'square_move_right': 'd',
496 if os.path.isfile('config.json'):
497 with open('config.json', 'r') as f:
498 keys_conf = json.loads(f.read())
500 self.keys[k] = keys_conf[k]
501 self.show_help = False
502 self.disconnected = True
503 self.force_instant_connect = True
504 self.input_lines = []
508 self.offset = YX(0,0)
509 curses.wrapper(self.loop)
513 def handle_recv(msg):
519 self.log_msg('@ attempting connect')
520 socket_client_class = PlomSocketClient
521 if self.host.startswith('ws://') or self.host.startswith('wss://'):
522 socket_client_class = WebSocketClient
524 self.socket = socket_client_class(handle_recv, self.host)
525 self.socket_thread = threading.Thread(target=self.socket.run)
526 self.socket_thread.start()
527 self.disconnected = False
528 self.game.thing_types = {}
529 self.game.terrains = {}
530 time.sleep(0.1) # give potential SSL negotation some time …
531 self.socket.send('TASKS')
532 self.socket.send('TERRAINS')
533 self.socket.send('THING_TYPES')
534 self.switch_mode('login')
535 except ConnectionRefusedError:
536 self.log_msg('@ server connect failure')
537 self.disconnected = True
538 self.switch_mode('waiting_for_server')
539 self.do_refresh = True
542 self.log_msg('@ attempting reconnect')
544 # necessitated by some strange SSL race conditions with ws4py
545 time.sleep(0.1) # FIXME find out why exactly necessary
546 self.switch_mode('waiting_for_server')
551 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
552 raise BrokenSocketConnection
553 self.socket.send(msg)
554 except (BrokenPipeError, BrokenSocketConnection):
555 self.log_msg('@ server disconnected :(')
556 self.disconnected = True
557 self.force_instant_connect = True
558 self.do_refresh = True
560 def log_msg(self, msg):
562 if len(self.log) > 100:
563 self.log = self.log[-100:]
565 def restore_input_values(self):
566 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
567 self.input_ = self.game.annotations[self.explorer]
568 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
569 self.input_ = self.game.portals[self.explorer]
570 elif self.mode.name == 'password':
571 self.input_ = self.password
572 elif self.mode.name == 'name_thing':
573 if hasattr(self.thing_selected, 'name'):
574 self.input_ = self.thing_selected.name
575 elif self.mode.name == 'admin_thing_protect':
576 if hasattr(self.thing_selected, 'protection'):
577 self.input_ = self.thing_selected.protection
579 def send_tile_control_command(self):
580 self.send('SET_TILE_CONTROL %s %s' %
581 (self.explorer, quote(self.tile_control_char)))
583 def toggle_map_mode(self):
584 if self.map_mode == 'terrain only':
585 self.map_mode = 'terrain + annotations'
586 elif self.map_mode == 'terrain + annotations':
587 self.map_mode = 'terrain + things'
588 elif self.map_mode == 'terrain + things':
589 self.map_mode = 'protections'
590 elif self.map_mode == 'protections':
591 self.map_mode = 'terrain only'
593 def switch_mode(self, mode_name):
594 if self.mode and self.mode.name == 'control_tile_draw':
595 self.log_msg('@ finished tile protection drawing.')
596 self.tile_draw = False
597 if mode_name == 'admin_enter' and self.is_admin:
599 elif mode_name in {'name_thing', 'admin_thing_protect'}:
600 player = self.game.get_thing(self.game.player_id)
602 for t in [t for t in self.game.things if t.position == player.position
603 and t.id_ != player.id_]:
608 self.log_msg('? not standing over thing')
611 self.thing_selected = thing
612 self.mode = getattr(self, 'mode_' + mode_name)
613 if self.mode.name in {'control_tile_draw', 'control_tile_type',
615 self.map_mode = 'protections'
616 elif self.mode.name != 'edit':
617 self.map_mode = 'terrain + things'
618 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
619 player = self.game.get_thing(self.game.player_id)
620 self.explorer = YX(player.position.y, player.position.x)
621 if self.mode.is_single_char_entry:
622 self.show_help = True
623 if len(self.mode.intro_msg) > 0:
624 self.log_msg(self.mode.intro_msg)
625 if self.mode.name == 'login':
627 self.send('LOGIN ' + quote(self.login_name))
629 self.log_msg('@ enter username')
630 elif self.mode.name == 'take_thing':
631 self.log_msg('Things in reach for pick-up:')
632 player = self.game.get_thing(self.game.player_id)
633 select_range = [player.position,
634 player.position + YX(0,-1),
635 player.position + YX(0, 1),
636 player.position + YX(-1, 0),
637 player.position + YX(1, 0)]
638 if type(self.game.map_geometry) == MapGeometryHex:
639 if player.position.y % 2:
640 select_range += [player.position + YX(-1, 1),
641 player.position + YX(1, 1)]
643 select_range += [player.position + YX(-1, -1),
644 player.position + YX(1, -1)]
645 self.selectables = [t for t in self.game.things
646 if t != player and t.type_ != 'Player'
647 and t.position in select_range]
648 if len(self.selectables) == 0:
651 for i in range(len(self.selectables)):
652 t = self.selectables[i]
653 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
654 elif self.mode.name == 'command_thing':
655 self.send('TASK:COMMAND ' + quote('HELP'))
656 elif self.mode.name == 'control_pw_pw':
657 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
658 elif self.mode.name == 'control_tile_draw':
659 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']))
661 self.restore_input_values()
663 def set_default_colors(self):
664 curses.init_color(1, 1000, 1000, 1000)
665 curses.init_color(2, 0, 0, 0)
666 self.do_refresh = True
668 def set_random_colors(self):
672 return int(offset + random.random()*375)
674 curses.init_color(1, rand(625), rand(625), rand(625))
675 curses.init_color(2, rand(0), rand(0), rand(0))
676 self.do_refresh = True
680 return self.info_cached
681 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
683 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
684 info_to_cache += 'outside field of view'
686 terrain_char = self.game.map_content[pos_i]
688 if terrain_char in self.game.terrains:
689 terrain_desc = self.game.terrains[terrain_char]
690 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
692 protection = self.game.map_control_content[pos_i]
693 if protection == '.':
694 protection = 'unprotected'
695 info_to_cache += 'PROTECTION: %s\n' % protection
696 for t in self.game.things:
697 if t.position == self.explorer:
698 info_to_cache += 'THING: %s' % self.get_thing_info(t)
699 protection = t.protection
700 if protection == '.':
702 info_to_cache += ' / protection: %s\n' % protection
703 if self.explorer in self.game.portals:
704 info_to_cache += 'PORTAL: ' +\
705 self.game.portals[self.explorer] + '\n'
707 info_to_cache += 'PORTAL: (none)\n'
708 if self.explorer in self.game.annotations:
709 info_to_cache += 'ANNOTATION: ' +\
710 self.game.annotations[self.explorer]
711 self.info_cached = info_to_cache
712 return self.info_cached
714 def get_thing_info(self, t):
716 (t.type_, self.game.thing_types[t.type_])
717 if hasattr(t, 'thing_char'):
719 if hasattr(t, 'name'):
720 info += ' (%s)' % t.name
723 def loop(self, stdscr):
726 def safe_addstr(y, x, line):
727 if y < self.size.y - 1 or x + len(line) < self.size.x:
728 stdscr.addstr(y, x, line, curses.color_pair(1))
729 else: # workaround to <https://stackoverflow.com/q/7063128>
730 cut_i = self.size.x - x - 1
732 last_char = line[cut_i]
733 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
734 stdscr.insstr(y, self.size.x - 2, ' ')
735 stdscr.addstr(y, x, cut, curses.color_pair(1))
737 def handle_input(msg):
738 command, args = self.parser.parse(msg)
741 def task_action_on(action):
742 return action_tasks[action] in self.game.tasks
744 def msg_into_lines_of_width(msg, width):
748 for i in range(len(msg)):
749 if x >= width or msg[i] == "\n":
761 def reset_screen_size():
762 self.size = YX(*stdscr.getmaxyx())
763 self.size = self.size - YX(self.size.y % 4, 0)
764 self.size = self.size - YX(0, self.size.x % 4)
765 self.window_width = int(self.size.x / 2)
767 def recalc_input_lines():
768 if not self.mode.has_input_prompt:
769 self.input_lines = []
771 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
774 def move_explorer(direction):
775 target = self.game.map_geometry.move_yx(self.explorer, direction)
777 self.info_cached = None
778 self.explorer = target
780 self.send_tile_control_command()
786 for line in self.log:
787 lines += msg_into_lines_of_width(line, self.window_width)
790 max_y = self.size.y - len(self.input_lines)
791 for i in range(len(lines)):
792 if (i >= max_y - height_header):
794 safe_addstr(max_y - i - 1, self.window_width, lines[i])
797 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
798 lines = msg_into_lines_of_width(info, self.window_width)
800 for i in range(len(lines)):
801 y = height_header + i
802 if y >= self.size.y - len(self.input_lines):
804 safe_addstr(y, self.window_width, lines[i])
807 y = self.size.y - len(self.input_lines)
808 for i in range(len(self.input_lines)):
809 safe_addstr(y, self.window_width, self.input_lines[i])
813 if not self.game.turn_complete:
815 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
818 help = "hit [%s] for help" % self.keys['help']
819 if self.mode.has_input_prompt:
820 help = "enter /help for help"
821 safe_addstr(1, self.window_width,
822 'MODE: %s – %s' % (self.mode.short_desc, help))
825 if not self.game.turn_complete and len(self.map_lines) == 0:
827 if self.game.turn_complete:
829 for y in range(self.game.map_geometry.size.y):
830 start = self.game.map_geometry.size.x * y
831 end = start + self.game.map_geometry.size.x
832 if self.map_mode == 'protections':
833 map_lines_split += [[c + ' ' for c
834 in self.game.map_control_content[start:end]]]
836 map_lines_split += [[c + ' ' for c
837 in self.game.map_content[start:end]]]
838 if self.map_mode == 'terrain + annotations':
839 for p in self.game.annotations:
840 map_lines_split[p.y][p.x] = 'A '
841 elif self.map_mode == 'terrain + things':
842 for p in self.game.portals.keys():
843 original = map_lines_split[p.y][p.x]
844 map_lines_split[p.y][p.x] = original[0] + 'P'
847 def draw_thing(t, used_positions):
848 symbol = self.game.thing_types[t.type_]
850 if hasattr(t, 'thing_char'):
851 meta_char = t.thing_char
852 if t.position in used_positions:
854 if hasattr(t, 'carrying') and t.carrying:
856 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
857 used_positions += [t.position]
859 for t in [t for t in self.game.things if t.type_ != 'Player']:
860 draw_thing(t, used_positions)
861 for t in [t for t in self.game.things if t.type_ == 'Player']:
862 draw_thing(t, used_positions)
863 player = self.game.get_thing(self.game.player_id)
864 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
865 map_lines_split[self.explorer.y][self.explorer.x] = '??'
866 elif self.map_mode != 'terrain + things':
867 map_lines_split[player.position.y][player.position.x] = '??'
869 if type(self.game.map_geometry) == MapGeometryHex:
871 for line in map_lines_split:
872 self.map_lines += [indent * ' ' + ''.join(line)]
873 indent = 0 if indent else 1
875 for line in map_lines_split:
876 self.map_lines += [''.join(line)]
877 window_center = YX(int(self.size.y / 2),
878 int(self.window_width / 2))
879 center = player.position
880 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
881 center = self.explorer
882 center = YX(center.y, center.x * 2)
883 self.offset = center - window_center
884 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
885 self.offset += YX(0, 1)
886 term_y = max(0, -self.offset.y)
887 term_x = max(0, -self.offset.x)
888 map_y = max(0, self.offset.y)
889 map_x = max(0, self.offset.x)
890 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
891 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
892 safe_addstr(term_y, term_x, to_draw)
897 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
898 self.mode.help_intro)
899 if len(self.mode.available_actions) > 0:
900 content += "Available actions:\n"
901 for action in self.mode.available_actions:
902 if action in action_tasks:
903 if action_tasks[action] not in self.game.tasks:
905 if action == 'move_explorer':
908 key = ','.join(self.movement_keys)
910 key = self.keys[action]
911 content += '[%s] – %s\n' % (key, action_descriptions[action])
913 content += self.mode.list_available_modes(self)
914 for i in range(self.size.y):
916 self.window_width * (not self.mode.has_input_prompt),
917 ' ' * self.window_width)
919 for line in content.split('\n'):
920 lines += msg_into_lines_of_width(line, self.window_width)
921 for i in range(len(lines)):
925 self.window_width * (not self.mode.has_input_prompt),
930 stdscr.bkgd(' ', curses.color_pair(1))
932 if self.mode.has_input_prompt:
934 if self.mode.shows_info:
939 if not self.mode.is_intro:
945 action_descriptions = {
947 'flatten': 'flatten surroundings',
948 'teleport': 'teleport',
949 'take_thing': 'pick up thing',
950 'drop_thing': 'drop thing',
951 'toggle_map_mode': 'toggle map view',
952 'toggle_tile_draw': 'toggle protection character drawing',
953 'door': 'open/close',
954 'consume': 'consume',
958 'flatten': 'FLATTEN_SURROUNDINGS',
959 'take_thing': 'PICK_UP',
960 'drop_thing': 'DROP',
963 'command': 'COMMAND',
964 'consume': 'INTOXICATE',
967 curses.curs_set(False) # hide cursor
969 self.set_default_colors()
970 curses.init_pair(1, 1, 2)
973 self.explorer = YX(0, 0)
976 interval = datetime.timedelta(seconds=5)
977 last_ping = datetime.datetime.now() - interval
979 if self.disconnected and self.force_instant_connect:
980 self.force_instant_connect = False
982 now = datetime.datetime.now()
983 if now - last_ping > interval:
984 if self.disconnected:
994 self.do_refresh = False
997 msg = self.queue.get(block=False)
1002 key = stdscr.getkey()
1003 self.do_refresh = True
1004 except curses.error:
1006 self.show_help = False
1007 if key == 'KEY_RESIZE':
1009 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1010 self.input_ = self.input_[:-1]
1011 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1012 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1014 if self.mode.name != 'chat':
1015 self.log_msg('@ aborted')
1016 self.switch_mode('play')
1017 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1018 self.show_help = True
1020 self.restore_input_values()
1021 elif self.mode.has_input_prompt and key != '\n': # Return key
1023 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1024 if len(self.input_) > max_length:
1025 self.input_ = self.input_[:max_length]
1026 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1027 self.show_help = True
1028 elif self.mode.name == 'login' and key == '\n':
1029 self.login_name = self.input_
1030 self.send('LOGIN ' + quote(self.input_))
1032 elif self.mode.name == 'take_thing' and key == '\n':
1034 i = int(self.input_)
1035 if i < 0 or i >= len(self.selectables):
1036 self.log_msg('? invalid index, aborted')
1038 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1040 self.log_msg('? invalid index, aborted')
1042 self.switch_mode('play')
1043 elif self.mode.name == 'command_thing' and key == '\n':
1044 if task_action_on('command'):
1045 self.send('TASK:COMMAND ' + quote(self.input_))
1047 elif self.mode.name == 'control_pw_pw' and key == '\n':
1048 if self.input_ == '':
1049 self.log_msg('@ aborted')
1051 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1052 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1053 self.switch_mode('admin')
1054 elif self.mode.name == 'password' and key == '\n':
1055 if self.input_ == '':
1057 self.password = self.input_
1058 self.switch_mode('edit')
1059 elif self.mode.name == 'admin_enter' and key == '\n':
1060 self.send('BECOME_ADMIN ' + quote(self.input_))
1061 self.switch_mode('play')
1062 elif self.mode.name == 'control_pw_type' and key == '\n':
1063 if len(self.input_) != 1:
1064 self.log_msg('@ entered non-single-char, therefore aborted')
1065 self.switch_mode('admin')
1067 self.tile_control_char = self.input_
1068 self.switch_mode('control_pw_pw')
1069 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1070 if len(self.input_) != 1:
1071 self.log_msg('@ entered non-single-char, therefore aborted')
1073 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1074 quote(self.input_)))
1075 self.log_msg('@ sent new protection character for thing')
1076 self.switch_mode('admin')
1077 elif self.mode.name == 'control_tile_type' and key == '\n':
1078 if len(self.input_) != 1:
1079 self.log_msg('@ entered non-single-char, therefore aborted')
1080 self.switch_mode('admin')
1082 self.tile_control_char = self.input_
1083 self.switch_mode('control_tile_draw')
1084 elif self.mode.name == 'chat' and key == '\n':
1085 if self.input_ == '':
1087 if self.input_[0] == '/':
1088 if self.input_.startswith('/nick'):
1089 tokens = self.input_.split(maxsplit=1)
1090 if len(tokens) == 2:
1091 self.send('NICK ' + quote(tokens[1]))
1093 self.log_msg('? need login name')
1095 self.log_msg('? unknown command')
1097 self.send('ALL ' + quote(self.input_))
1099 elif self.mode.name == 'name_thing' and key == '\n':
1100 if self.input_ == '':
1102 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1104 quote(self.password)))
1105 self.switch_mode('edit')
1106 elif self.mode.name == 'annotate' and key == '\n':
1107 if self.input_ == '':
1109 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1110 quote(self.password)))
1111 self.switch_mode('edit')
1112 elif self.mode.name == 'portal' and key == '\n':
1113 if self.input_ == '':
1115 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1116 quote(self.password)))
1117 self.switch_mode('edit')
1118 elif self.mode.name == 'study':
1119 if self.mode.mode_switch_on_key(self, key):
1121 elif key == self.keys['toggle_map_mode']:
1122 self.toggle_map_mode()
1123 elif key in self.movement_keys:
1124 move_explorer(self.movement_keys[key])
1125 elif self.mode.name == 'play':
1126 if self.mode.mode_switch_on_key(self, key):
1128 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1129 self.send('TASK:DROP')
1130 elif key == self.keys['door'] and task_action_on('door'):
1131 self.send('TASK:DOOR')
1132 elif key == self.keys['consume'] and task_action_on('consume'):
1133 self.send('TASK:INTOXICATE')
1134 elif key == self.keys['teleport']:
1135 player = self.game.get_thing(self.game.player_id)
1136 if player.position in self.game.portals:
1137 self.host = self.game.portals[player.position]
1141 self.log_msg('? not standing on portal')
1142 elif key in self.movement_keys and task_action_on('move'):
1143 self.send('TASK:MOVE ' + self.movement_keys[key])
1144 elif self.mode.name == 'write':
1145 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1146 self.switch_mode('edit')
1147 elif self.mode.name == 'control_tile_draw':
1148 if self.mode.mode_switch_on_key(self, key):
1150 elif key in self.movement_keys:
1151 move_explorer(self.movement_keys[key])
1152 elif key == self.keys['toggle_tile_draw']:
1153 self.tile_draw = False if self.tile_draw else True
1154 elif self.mode.name == 'admin':
1155 if self.mode.mode_switch_on_key(self, key):
1157 elif key in self.movement_keys and task_action_on('move'):
1158 self.send('TASK:MOVE ' + self.movement_keys[key])
1159 elif self.mode.name == 'edit':
1160 if self.mode.mode_switch_on_key(self, key):
1162 elif key == self.keys['flatten'] and task_action_on('flatten'):
1163 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1164 elif key == self.keys['toggle_map_mode']:
1165 self.toggle_map_mode()
1166 elif key in self.movement_keys and task_action_on('move'):
1167 self.send('TASK:MOVE ' + self.movement_keys[key])
1169 if len(sys.argv) != 2:
1170 raise ArgError('wrong number of arguments, need game host')