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_INSTALLED(game, thing_id):
296 game.get_thing(thing_id).installed = True
297 cmd_THING_INSTALLED.argtypes = 'int:pos'
299 def cmd_THING_CARRYING(game, thing_id):
300 game.get_thing(thing_id).carrying = True
301 cmd_THING_CARRYING.argtypes = 'int:pos'
303 def cmd_TERRAIN(game, terrain_char, terrain_desc):
304 game.terrains[terrain_char] = terrain_desc
305 cmd_TERRAIN.argtypes = 'char string'
309 cmd_PONG.argtypes = ''
311 def cmd_DEFAULT_COLORS(game):
312 game.tui.set_default_colors()
313 cmd_DEFAULT_COLORS.argtypes = ''
315 def cmd_RANDOM_COLORS(game):
316 game.tui.set_random_colors()
317 cmd_RANDOM_COLORS.argtypes = ''
319 class Game(GameBase):
320 turn_complete = False
324 def __init__(self, *args, **kwargs):
325 super().__init__(*args, **kwargs)
326 self.register_command(cmd_LOGIN_OK)
327 self.register_command(cmd_ADMIN_OK)
328 self.register_command(cmd_PONG)
329 self.register_command(cmd_CHAT)
330 self.register_command(cmd_REPLY)
331 self.register_command(cmd_PLAYER_ID)
332 self.register_command(cmd_TURN)
333 self.register_command(cmd_THING)
334 self.register_command(cmd_THING_TYPE)
335 self.register_command(cmd_THING_NAME)
336 self.register_command(cmd_THING_CHAR)
337 self.register_command(cmd_THING_CARRYING)
338 self.register_command(cmd_THING_INSTALLED)
339 self.register_command(cmd_TERRAIN)
340 self.register_command(cmd_MAP)
341 self.register_command(cmd_MAP_CONTROL)
342 self.register_command(cmd_PORTAL)
343 self.register_command(cmd_ANNOTATION)
344 self.register_command(cmd_GAME_STATE_COMPLETE)
345 self.register_command(cmd_ARGUMENT_ERROR)
346 self.register_command(cmd_GAME_ERROR)
347 self.register_command(cmd_PLAY_ERROR)
348 self.register_command(cmd_TASKS)
349 self.register_command(cmd_FOV)
350 self.register_command(cmd_DEFAULT_COLORS)
351 self.register_command(cmd_RANDOM_COLORS)
352 self.map_content = ''
354 self.annotations = {}
358 def get_string_options(self, string_option_type):
359 if string_option_type == 'map_geometry':
360 return ['Hex', 'Square']
361 elif string_option_type == 'thing_type':
362 return self.thing_types.keys()
365 def get_command(self, command_name):
366 from functools import partial
367 f = partial(self.commands[command_name], self)
368 f.argtypes = self.commands[command_name].argtypes
373 def __init__(self, name, has_input_prompt=False, shows_info=False,
374 is_intro=False, is_single_char_entry=False):
376 self.short_desc = mode_helps[name]['short']
377 self.available_modes = []
378 self.available_actions = []
379 self.has_input_prompt = has_input_prompt
380 self.shows_info = shows_info
381 self.is_intro = is_intro
382 self.help_intro = mode_helps[name]['long']
383 self.intro_msg = mode_helps[name]['intro']
384 self.is_single_char_entry = is_single_char_entry
387 def iter_available_modes(self, tui):
388 for mode_name in self.available_modes:
389 mode = getattr(tui, 'mode_' + mode_name)
392 key = tui.keys['switch_to_' + mode.name]
395 def list_available_modes(self, tui):
397 if len(self.available_modes) > 0:
398 msg = 'Other modes available from here:\n'
399 for mode, key in self.iter_available_modes(tui):
400 msg += '[%s] – %s\n' % (key, mode.short_desc)
403 def mode_switch_on_key(self, tui, key_pressed):
404 for mode, key in self.iter_available_modes(tui):
405 if key_pressed == key:
406 tui.switch_mode(mode.name)
411 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
412 mode_admin = Mode('admin')
413 mode_play = Mode('play')
414 mode_study = Mode('study', shows_info=True)
415 mode_write = Mode('write', is_single_char_entry=True)
416 mode_edit = Mode('edit')
417 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
418 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
419 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
420 mode_control_tile_draw = Mode('control_tile_draw')
421 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
422 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
423 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
424 mode_chat = Mode('chat', has_input_prompt=True)
425 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
426 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
427 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
428 mode_password = Mode('password', has_input_prompt=True)
429 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
430 mode_command_thing = Mode('command_thing', has_input_prompt=True)
431 mode_take_thing = Mode('take_thing', has_input_prompt=True)
435 def __init__(self, host):
438 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
439 "command_thing", "take_thing"]
440 self.mode_play.available_actions = ["move", "drop_thing",
441 "teleport", "door", "consume",
443 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
444 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
445 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
446 "control_tile_type", "chat",
447 "study", "play", "edit"]
448 self.mode_admin.available_actions = ["move"]
449 self.mode_control_tile_draw.available_modes = ["admin_enter"]
450 self.mode_control_tile_draw.available_actions = ["move_explorer",
452 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
453 "password", "chat", "study", "play",
455 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
460 self.parser = Parser(self.game)
462 self.do_refresh = True
463 self.queue = queue.Queue()
464 self.login_name = None
465 self.map_mode = 'terrain + things'
466 self.password = 'foo'
467 self.switch_mode('waiting_for_server')
469 'switch_to_chat': 't',
470 'switch_to_play': 'p',
471 'switch_to_password': 'P',
472 'switch_to_annotate': 'M',
473 'switch_to_portal': 'T',
474 'switch_to_study': '?',
475 'switch_to_edit': 'E',
476 'switch_to_write': 'm',
477 'switch_to_name_thing': 'N',
478 'switch_to_command_thing': 'O',
479 'switch_to_admin_enter': 'A',
480 'switch_to_control_pw_type': 'C',
481 'switch_to_control_tile_type': 'Q',
482 'switch_to_admin_thing_protect': 'T',
484 'switch_to_take_thing': 'z',
491 'toggle_map_mode': 'L',
492 'toggle_tile_draw': 'm',
493 'hex_move_upleft': 'w',
494 'hex_move_upright': 'e',
495 'hex_move_right': 'd',
496 'hex_move_downright': 'x',
497 'hex_move_downleft': 'y',
498 'hex_move_left': 'a',
499 'square_move_up': 'w',
500 'square_move_left': 'a',
501 'square_move_down': 's',
502 'square_move_right': 'd',
504 if os.path.isfile('config.json'):
505 with open('config.json', 'r') as f:
506 keys_conf = json.loads(f.read())
508 self.keys[k] = keys_conf[k]
509 self.show_help = False
510 self.disconnected = True
511 self.force_instant_connect = True
512 self.input_lines = []
516 self.offset = YX(0,0)
517 curses.wrapper(self.loop)
521 def handle_recv(msg):
527 self.log_msg('@ attempting connect')
528 socket_client_class = PlomSocketClient
529 if self.host.startswith('ws://') or self.host.startswith('wss://'):
530 socket_client_class = WebSocketClient
532 self.socket = socket_client_class(handle_recv, self.host)
533 self.socket_thread = threading.Thread(target=self.socket.run)
534 self.socket_thread.start()
535 self.disconnected = False
536 self.game.thing_types = {}
537 self.game.terrains = {}
538 time.sleep(0.1) # give potential SSL negotation some time …
539 self.socket.send('TASKS')
540 self.socket.send('TERRAINS')
541 self.socket.send('THING_TYPES')
542 self.switch_mode('login')
543 except ConnectionRefusedError:
544 self.log_msg('@ server connect failure')
545 self.disconnected = True
546 self.switch_mode('waiting_for_server')
547 self.do_refresh = True
550 self.log_msg('@ attempting reconnect')
552 # necessitated by some strange SSL race conditions with ws4py
553 time.sleep(0.1) # FIXME find out why exactly necessary
554 self.switch_mode('waiting_for_server')
559 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
560 raise BrokenSocketConnection
561 self.socket.send(msg)
562 except (BrokenPipeError, BrokenSocketConnection):
563 self.log_msg('@ server disconnected :(')
564 self.disconnected = True
565 self.force_instant_connect = True
566 self.do_refresh = True
568 def log_msg(self, msg):
570 if len(self.log) > 100:
571 self.log = self.log[-100:]
573 def restore_input_values(self):
574 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
575 self.input_ = self.game.annotations[self.explorer]
576 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
577 self.input_ = self.game.portals[self.explorer]
578 elif self.mode.name == 'password':
579 self.input_ = self.password
580 elif self.mode.name == 'name_thing':
581 if hasattr(self.thing_selected, 'name'):
582 self.input_ = self.thing_selected.name
583 elif self.mode.name == 'admin_thing_protect':
584 if hasattr(self.thing_selected, 'protection'):
585 self.input_ = self.thing_selected.protection
587 def send_tile_control_command(self):
588 self.send('SET_TILE_CONTROL %s %s' %
589 (self.explorer, quote(self.tile_control_char)))
591 def toggle_map_mode(self):
592 if self.map_mode == 'terrain only':
593 self.map_mode = 'terrain + annotations'
594 elif self.map_mode == 'terrain + annotations':
595 self.map_mode = 'terrain + things'
596 elif self.map_mode == 'terrain + things':
597 self.map_mode = 'protections'
598 elif self.map_mode == 'protections':
599 self.map_mode = 'terrain only'
601 def switch_mode(self, mode_name):
602 if self.mode and self.mode.name == 'control_tile_draw':
603 self.log_msg('@ finished tile protection drawing.')
604 self.tile_draw = False
605 if mode_name == 'admin_enter' and self.is_admin:
607 elif mode_name in {'name_thing', 'admin_thing_protect'}:
608 player = self.game.get_thing(self.game.player_id)
610 for t in [t for t in self.game.things if t.position == player.position
611 and t.id_ != player.id_]:
616 self.log_msg('? not standing over thing')
619 self.thing_selected = thing
620 self.mode = getattr(self, 'mode_' + mode_name)
621 if self.mode.name in {'control_tile_draw', 'control_tile_type',
623 self.map_mode = 'protections'
624 elif self.mode.name != 'edit':
625 self.map_mode = 'terrain + things'
626 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
627 player = self.game.get_thing(self.game.player_id)
628 self.explorer = YX(player.position.y, player.position.x)
629 if self.mode.is_single_char_entry:
630 self.show_help = True
631 if len(self.mode.intro_msg) > 0:
632 self.log_msg(self.mode.intro_msg)
633 if self.mode.name == 'login':
635 self.send('LOGIN ' + quote(self.login_name))
637 self.log_msg('@ enter username')
638 elif self.mode.name == 'take_thing':
639 self.log_msg('Portable things in reach for pick-up:')
640 player = self.game.get_thing(self.game.player_id)
641 select_range = [player.position,
642 player.position + YX(0,-1),
643 player.position + YX(0, 1),
644 player.position + YX(-1, 0),
645 player.position + YX(1, 0)]
646 if type(self.game.map_geometry) == MapGeometryHex:
647 if player.position.y % 2:
648 select_range += [player.position + YX(-1, 1),
649 player.position + YX(1, 1)]
651 select_range += [player.position + YX(-1, -1),
652 player.position + YX(1, -1)]
653 self.selectables = [t for t in self.game.things
654 if t.portable and t.position in select_range]
655 if len(self.selectables) == 0:
658 for i in range(len(self.selectables)):
659 t = self.selectables[i]
660 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
661 elif self.mode.name == 'command_thing':
662 self.send('TASK:COMMAND ' + quote('HELP'))
663 elif self.mode.name == 'control_pw_pw':
664 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
665 elif self.mode.name == 'control_tile_draw':
666 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']))
668 self.restore_input_values()
670 def set_default_colors(self):
671 curses.init_color(1, 1000, 1000, 1000)
672 curses.init_color(2, 0, 0, 0)
673 self.do_refresh = True
675 def set_random_colors(self):
679 return int(offset + random.random()*375)
681 curses.init_color(1, rand(625), rand(625), rand(625))
682 curses.init_color(2, rand(0), rand(0), rand(0))
683 self.do_refresh = True
687 return self.info_cached
688 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
690 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
691 info_to_cache += 'outside field of view'
693 terrain_char = self.game.map_content[pos_i]
695 if terrain_char in self.game.terrains:
696 terrain_desc = self.game.terrains[terrain_char]
697 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
699 protection = self.game.map_control_content[pos_i]
700 if protection == '.':
701 protection = 'unprotected'
702 info_to_cache += 'PROTECTION: %s\n' % protection
703 for t in self.game.things:
704 if t.position == self.explorer:
705 info_to_cache += 'THING: %s' % self.get_thing_info(t)
706 protection = t.protection
707 if protection == '.':
709 info_to_cache += ' / protection: %s\n' % protection
710 if self.explorer in self.game.portals:
711 info_to_cache += 'PORTAL: ' +\
712 self.game.portals[self.explorer] + '\n'
714 info_to_cache += 'PORTAL: (none)\n'
715 if self.explorer in self.game.annotations:
716 info_to_cache += 'ANNOTATION: ' +\
717 self.game.annotations[self.explorer]
718 self.info_cached = info_to_cache
719 return self.info_cached
721 def get_thing_info(self, t):
723 (t.type_, self.game.thing_types[t.type_])
724 if hasattr(t, 'thing_char'):
726 if hasattr(t, 'name'):
727 info += ' (%s)' % t.name
728 if hasattr(t, 'installed'):
729 info += ' / installed'
732 def loop(self, stdscr):
735 def safe_addstr(y, x, line):
736 if y < self.size.y - 1 or x + len(line) < self.size.x:
737 stdscr.addstr(y, x, line, curses.color_pair(1))
738 else: # workaround to <https://stackoverflow.com/q/7063128>
739 cut_i = self.size.x - x - 1
741 last_char = line[cut_i]
742 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
743 stdscr.insstr(y, self.size.x - 2, ' ')
744 stdscr.addstr(y, x, cut, curses.color_pair(1))
746 def handle_input(msg):
747 command, args = self.parser.parse(msg)
750 def task_action_on(action):
751 return action_tasks[action] in self.game.tasks
753 def msg_into_lines_of_width(msg, width):
757 for i in range(len(msg)):
758 if x >= width or msg[i] == "\n":
770 def reset_screen_size():
771 self.size = YX(*stdscr.getmaxyx())
772 self.size = self.size - YX(self.size.y % 4, 0)
773 self.size = self.size - YX(0, self.size.x % 4)
774 self.window_width = int(self.size.x / 2)
776 def recalc_input_lines():
777 if not self.mode.has_input_prompt:
778 self.input_lines = []
780 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
783 def move_explorer(direction):
784 target = self.game.map_geometry.move_yx(self.explorer, direction)
786 self.info_cached = None
787 self.explorer = target
789 self.send_tile_control_command()
795 for line in self.log:
796 lines += msg_into_lines_of_width(line, self.window_width)
799 max_y = self.size.y - len(self.input_lines)
800 for i in range(len(lines)):
801 if (i >= max_y - height_header):
803 safe_addstr(max_y - i - 1, self.window_width, lines[i])
806 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
807 lines = msg_into_lines_of_width(info, self.window_width)
809 for i in range(len(lines)):
810 y = height_header + i
811 if y >= self.size.y - len(self.input_lines):
813 safe_addstr(y, self.window_width, lines[i])
816 y = self.size.y - len(self.input_lines)
817 for i in range(len(self.input_lines)):
818 safe_addstr(y, self.window_width, self.input_lines[i])
822 if not self.game.turn_complete:
824 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
827 help = "hit [%s] for help" % self.keys['help']
828 if self.mode.has_input_prompt:
829 help = "enter /help for help"
830 safe_addstr(1, self.window_width,
831 'MODE: %s – %s' % (self.mode.short_desc, help))
834 if not self.game.turn_complete and len(self.map_lines) == 0:
836 if self.game.turn_complete:
838 for y in range(self.game.map_geometry.size.y):
839 start = self.game.map_geometry.size.x * y
840 end = start + self.game.map_geometry.size.x
841 if self.map_mode == 'protections':
842 map_lines_split += [[c + ' ' for c
843 in self.game.map_control_content[start:end]]]
845 map_lines_split += [[c + ' ' for c
846 in self.game.map_content[start:end]]]
847 if self.map_mode == 'terrain + annotations':
848 for p in self.game.annotations:
849 map_lines_split[p.y][p.x] = 'A '
850 elif self.map_mode == 'terrain + things':
851 for p in self.game.portals.keys():
852 original = map_lines_split[p.y][p.x]
853 map_lines_split[p.y][p.x] = original[0] + 'P'
856 def draw_thing(t, used_positions):
857 symbol = self.game.thing_types[t.type_]
859 if hasattr(t, 'thing_char'):
860 meta_char = t.thing_char
861 if t.position in used_positions:
863 if hasattr(t, 'carrying') and t.carrying:
865 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
866 used_positions += [t.position]
868 for t in [t for t in self.game.things if t.type_ != 'Player']:
869 draw_thing(t, used_positions)
870 for t in [t for t in self.game.things if t.type_ == 'Player']:
871 draw_thing(t, used_positions)
872 player = self.game.get_thing(self.game.player_id)
873 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
874 map_lines_split[self.explorer.y][self.explorer.x] = '??'
875 elif self.map_mode != 'terrain + things':
876 map_lines_split[player.position.y][player.position.x] = '??'
878 if type(self.game.map_geometry) == MapGeometryHex:
880 for line in map_lines_split:
881 self.map_lines += [indent * ' ' + ''.join(line)]
882 indent = 0 if indent else 1
884 for line in map_lines_split:
885 self.map_lines += [''.join(line)]
886 window_center = YX(int(self.size.y / 2),
887 int(self.window_width / 2))
888 center = player.position
889 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
890 center = self.explorer
891 center = YX(center.y, center.x * 2)
892 self.offset = center - window_center
893 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
894 self.offset += YX(0, 1)
895 term_y = max(0, -self.offset.y)
896 term_x = max(0, -self.offset.x)
897 map_y = max(0, self.offset.y)
898 map_x = max(0, self.offset.x)
899 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
900 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
901 safe_addstr(term_y, term_x, to_draw)
906 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
907 self.mode.help_intro)
908 if len(self.mode.available_actions) > 0:
909 content += "Available actions:\n"
910 for action in self.mode.available_actions:
911 if action in action_tasks:
912 if action_tasks[action] not in self.game.tasks:
914 if action == 'move_explorer':
917 key = ','.join(self.movement_keys)
919 key = self.keys[action]
920 content += '[%s] – %s\n' % (key, action_descriptions[action])
922 content += self.mode.list_available_modes(self)
923 for i in range(self.size.y):
925 self.window_width * (not self.mode.has_input_prompt),
926 ' ' * self.window_width)
928 for line in content.split('\n'):
929 lines += msg_into_lines_of_width(line, self.window_width)
930 for i in range(len(lines)):
934 self.window_width * (not self.mode.has_input_prompt),
939 stdscr.bkgd(' ', curses.color_pair(1))
941 if self.mode.has_input_prompt:
943 if self.mode.shows_info:
948 if not self.mode.is_intro:
954 action_descriptions = {
956 'flatten': 'flatten surroundings',
957 'teleport': 'teleport',
958 'take_thing': 'pick up thing',
959 'drop_thing': 'drop thing',
960 'toggle_map_mode': 'toggle map view',
961 'toggle_tile_draw': 'toggle protection character drawing',
962 'install': 'install',
963 'door': 'open/close',
964 'consume': 'consume',
968 'flatten': 'FLATTEN_SURROUNDINGS',
969 'take_thing': 'PICK_UP',
970 'drop_thing': 'DROP',
972 'install': 'INSTALL',
974 'command': 'COMMAND',
975 'consume': 'INTOXICATE',
978 curses.curs_set(False) # hide cursor
980 self.set_default_colors()
981 curses.init_pair(1, 1, 2)
984 self.explorer = YX(0, 0)
987 interval = datetime.timedelta(seconds=5)
988 last_ping = datetime.datetime.now() - interval
990 if self.disconnected and self.force_instant_connect:
991 self.force_instant_connect = False
993 now = datetime.datetime.now()
994 if now - last_ping > interval:
995 if self.disconnected:
1005 self.do_refresh = False
1008 msg = self.queue.get(block=False)
1013 key = stdscr.getkey()
1014 self.do_refresh = True
1015 except curses.error:
1017 self.show_help = False
1018 if key == 'KEY_RESIZE':
1020 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1021 self.input_ = self.input_[:-1]
1022 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1023 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1025 if self.mode.name != 'chat':
1026 self.log_msg('@ aborted')
1027 self.switch_mode('play')
1028 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1029 self.show_help = True
1031 self.restore_input_values()
1032 elif self.mode.has_input_prompt and key != '\n': # Return key
1034 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1035 if len(self.input_) > max_length:
1036 self.input_ = self.input_[:max_length]
1037 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1038 self.show_help = True
1039 elif self.mode.name == 'login' and key == '\n':
1040 self.login_name = self.input_
1041 self.send('LOGIN ' + quote(self.input_))
1043 elif self.mode.name == 'take_thing' and key == '\n':
1045 i = int(self.input_)
1046 if i < 0 or i >= len(self.selectables):
1047 self.log_msg('? invalid index, aborted')
1049 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1051 self.log_msg('? invalid index, aborted')
1053 self.switch_mode('play')
1054 elif self.mode.name == 'command_thing' and key == '\n':
1055 if task_action_on('command'):
1056 self.send('TASK:COMMAND ' + quote(self.input_))
1058 elif self.mode.name == 'control_pw_pw' and key == '\n':
1059 if self.input_ == '':
1060 self.log_msg('@ aborted')
1062 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1063 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1064 self.switch_mode('admin')
1065 elif self.mode.name == 'password' and key == '\n':
1066 if self.input_ == '':
1068 self.password = self.input_
1069 self.switch_mode('edit')
1070 elif self.mode.name == 'admin_enter' and key == '\n':
1071 self.send('BECOME_ADMIN ' + quote(self.input_))
1072 self.switch_mode('play')
1073 elif self.mode.name == 'control_pw_type' and key == '\n':
1074 if len(self.input_) != 1:
1075 self.log_msg('@ entered non-single-char, therefore aborted')
1076 self.switch_mode('admin')
1078 self.tile_control_char = self.input_
1079 self.switch_mode('control_pw_pw')
1080 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1081 if len(self.input_) != 1:
1082 self.log_msg('@ entered non-single-char, therefore aborted')
1084 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1085 quote(self.input_)))
1086 self.log_msg('@ sent new protection character for thing')
1087 self.switch_mode('admin')
1088 elif self.mode.name == 'control_tile_type' and key == '\n':
1089 if len(self.input_) != 1:
1090 self.log_msg('@ entered non-single-char, therefore aborted')
1091 self.switch_mode('admin')
1093 self.tile_control_char = self.input_
1094 self.switch_mode('control_tile_draw')
1095 elif self.mode.name == 'chat' and key == '\n':
1096 if self.input_ == '':
1098 if self.input_[0] == '/':
1099 if self.input_.startswith('/nick'):
1100 tokens = self.input_.split(maxsplit=1)
1101 if len(tokens) == 2:
1102 self.send('NICK ' + quote(tokens[1]))
1104 self.log_msg('? need login name')
1106 self.log_msg('? unknown command')
1108 self.send('ALL ' + quote(self.input_))
1110 elif self.mode.name == 'name_thing' and key == '\n':
1111 if self.input_ == '':
1113 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1115 quote(self.password)))
1116 self.switch_mode('edit')
1117 elif self.mode.name == 'annotate' and key == '\n':
1118 if self.input_ == '':
1120 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1121 quote(self.password)))
1122 self.switch_mode('edit')
1123 elif self.mode.name == 'portal' and key == '\n':
1124 if self.input_ == '':
1126 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1127 quote(self.password)))
1128 self.switch_mode('edit')
1129 elif self.mode.name == 'study':
1130 if self.mode.mode_switch_on_key(self, key):
1132 elif key == self.keys['toggle_map_mode']:
1133 self.toggle_map_mode()
1134 elif key in self.movement_keys:
1135 move_explorer(self.movement_keys[key])
1136 elif self.mode.name == 'play':
1137 if self.mode.mode_switch_on_key(self, key):
1139 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1140 self.send('TASK:DROP')
1141 elif key == self.keys['door'] and task_action_on('door'):
1142 self.send('TASK:DOOR')
1143 elif key == self.keys['consume'] and task_action_on('consume'):
1144 self.send('TASK:INTOXICATE')
1145 elif key == self.keys['install'] and task_action_on('install'):
1146 self.send('TASK:INSTALL')
1147 elif key == self.keys['teleport']:
1148 player = self.game.get_thing(self.game.player_id)
1149 if player.position in self.game.portals:
1150 self.host = self.game.portals[player.position]
1154 self.log_msg('? not standing on portal')
1155 elif key in self.movement_keys and task_action_on('move'):
1156 self.send('TASK:MOVE ' + self.movement_keys[key])
1157 elif self.mode.name == 'write':
1158 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1159 self.switch_mode('edit')
1160 elif self.mode.name == 'control_tile_draw':
1161 if self.mode.mode_switch_on_key(self, key):
1163 elif key in self.movement_keys:
1164 move_explorer(self.movement_keys[key])
1165 elif key == self.keys['toggle_tile_draw']:
1166 self.tile_draw = False if self.tile_draw else True
1167 elif self.mode.name == 'admin':
1168 if self.mode.mode_switch_on_key(self, key):
1170 elif key in self.movement_keys and task_action_on('move'):
1171 self.send('TASK:MOVE ' + self.movement_keys[key])
1172 elif self.mode.name == 'edit':
1173 if self.mode.mode_switch_on_key(self, key):
1175 elif key == self.keys['flatten'] and task_action_on('flatten'):
1176 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1177 elif key == self.keys['toggle_map_mode']:
1178 self.toggle_map_mode()
1179 elif key in self.movement_keys and task_action_on('move'):
1180 self.send('TASK:MOVE ' + self.movement_keys[key])
1182 if len(sys.argv) != 2:
1183 raise ArgError('wrong number of arguments, need game host')