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': 'enter your face',
51 'intro': '@ enter face line (enter nothing to abort):',
52 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
55 'short': 'change terrain',
57 '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.'
60 'short': 'change protection character password',
61 'intro': '@ enter protection character for which you want to change the password:',
62 '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.'
65 'short': 'change protection character password',
67 '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.'
69 'control_tile_type': {
70 'short': 'change tiles protection',
71 'intro': '@ enter protection character which you want to draw:',
72 '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.'
74 'control_tile_draw': {
75 'short': 'change tiles protection',
77 '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.'
80 'short': 'annotate tile',
82 '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.'
85 'short': 'edit portal',
87 '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.'
92 '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'
97 'long': 'Enter your player name.'
99 'waiting_for_server': {
100 'short': 'waiting for server response',
101 'intro': '@ waiting for server …',
102 'long': 'Waiting for a server response.'
105 'short': 'waiting for server response',
107 'long': 'Waiting for a server response.'
110 'short': 'set world edit password',
112 '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.'
115 'short': 'become admin',
116 'intro': '@ enter admin password:',
117 'long': 'This mode allows you to become admin if you know an admin password.'
122 'long': 'This mode allows you access to actions limited to administrators.'
126 from ws4py.client import WebSocketBaseClient
127 class WebSocketClient(WebSocketBaseClient):
129 def __init__(self, recv_handler, *args, **kwargs):
130 super().__init__(*args, **kwargs)
131 self.recv_handler = recv_handler
134 def received_message(self, message):
136 message = str(message)
137 self.recv_handler(message)
140 def plom_closed(self):
141 return self.client_terminated
143 from plomrogue.io_tcp import PlomSocket
144 class PlomSocketClient(PlomSocket):
146 def __init__(self, recv_handler, url):
148 self.recv_handler = recv_handler
149 host, port = url.split(':')
150 super().__init__(socket.create_connection((host, port)))
158 for msg in self.recv():
159 if msg == 'NEED_SSL':
160 self.socket = ssl.wrap_socket(self.socket)
162 self.recv_handler(msg)
163 except BrokenSocketConnection:
164 pass # we assume socket will be known as dead by now
166 def cmd_TURN(game, n):
167 game.annotations = {}
171 game.turn_complete = False
173 cmd_TURN.argtypes = 'int:nonneg'
175 def cmd_LOGIN_OK(game):
176 game.tui.switch_mode('post_login_wait')
177 game.tui.send('GET_GAMESTATE')
178 game.tui.log_msg('@ welcome')
179 cmd_LOGIN_OK.argtypes = ''
181 def cmd_ADMIN_OK(game):
182 game.tui.is_admin = True
183 game.tui.log_msg('@ you now have admin rights')
184 game.tui.switch_mode('admin')
185 game.tui.do_refresh = True
186 cmd_ADMIN_OK.argtypes = ''
188 def cmd_REPLY(game, msg):
189 game.tui.log_msg('#MUSICPLAYER: ' + msg)
190 game.tui.do_refresh = True
191 cmd_REPLY.argtypes = 'string'
193 def cmd_CHAT(game, msg):
194 game.tui.log_msg('# ' + msg)
195 game.tui.do_refresh = True
196 cmd_CHAT.argtypes = 'string'
198 def cmd_PLAYER_ID(game, player_id):
199 game.player_id = player_id
200 cmd_PLAYER_ID.argtypes = 'int:nonneg'
202 def cmd_THING(game, yx, thing_type, protection, thing_id, portable):
203 t = game.get_thing(thing_id)
205 t = ThingBase(game, thing_id)
209 t.protection = protection
210 t.portable = portable
211 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool'
213 def cmd_THING_NAME(game, thing_id, name):
214 t = game.get_thing(thing_id)
216 cmd_THING_NAME.argtypes = 'int:pos string'
218 def cmd_THING_FACE(game, thing_id, face):
219 t = game.get_thing(thing_id)
221 cmd_THING_FACE.argtypes = 'int:pos string'
223 def cmd_THING_HAT(game, thing_id, hat):
224 t = game.get_thing(thing_id)
226 cmd_THING_HAT.argtypes = 'int:pos string'
228 def cmd_THING_CHAR(game, thing_id, c):
229 t = game.get_thing(thing_id)
231 cmd_THING_CHAR.argtypes = 'int:pos char'
233 def cmd_MAP(game, geometry, size, content):
234 map_geometry_class = globals()['MapGeometry' + geometry]
235 game.map_geometry = map_geometry_class(size)
236 game.map_content = content
237 if type(game.map_geometry) == MapGeometrySquare:
238 game.tui.movement_keys = {
239 game.tui.keys['square_move_up']: 'UP',
240 game.tui.keys['square_move_left']: 'LEFT',
241 game.tui.keys['square_move_down']: 'DOWN',
242 game.tui.keys['square_move_right']: 'RIGHT',
244 elif type(game.map_geometry) == MapGeometryHex:
245 game.tui.movement_keys = {
246 game.tui.keys['hex_move_upleft']: 'UPLEFT',
247 game.tui.keys['hex_move_upright']: 'UPRIGHT',
248 game.tui.keys['hex_move_right']: 'RIGHT',
249 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
250 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
251 game.tui.keys['hex_move_left']: 'LEFT',
253 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
255 def cmd_FOV(game, content):
257 cmd_FOV.argtypes = 'string'
259 def cmd_MAP_CONTROL(game, content):
260 game.map_control_content = content
261 cmd_MAP_CONTROL.argtypes = 'string'
263 def cmd_GAME_STATE_COMPLETE(game):
264 if game.tui.mode.name == 'post_login_wait':
265 game.tui.switch_mode('play')
266 game.turn_complete = True
267 game.tui.do_refresh = True
268 game.tui.info_cached = None
269 cmd_GAME_STATE_COMPLETE.argtypes = ''
271 def cmd_PORTAL(game, position, msg):
272 game.portals[position] = msg
273 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
275 def cmd_PLAY_ERROR(game, msg):
276 game.tui.log_msg('? ' + msg)
277 game.tui.flash = True
278 game.tui.do_refresh = True
279 cmd_PLAY_ERROR.argtypes = 'string'
281 def cmd_GAME_ERROR(game, msg):
282 game.tui.log_msg('? game error: ' + msg)
283 game.tui.do_refresh = True
284 cmd_GAME_ERROR.argtypes = 'string'
286 def cmd_ARGUMENT_ERROR(game, msg):
287 game.tui.log_msg('? syntax error: ' + msg)
288 game.tui.do_refresh = True
289 cmd_ARGUMENT_ERROR.argtypes = 'string'
291 def cmd_ANNOTATION(game, position, msg):
292 game.annotations[position] = msg
293 if game.tui.mode.shows_info:
294 game.tui.do_refresh = True
295 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
297 def cmd_TASKS(game, tasks_comma_separated):
298 game.tasks = tasks_comma_separated.split(',')
299 game.tui.mode_write.legal = 'WRITE' in game.tasks
300 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
301 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
302 cmd_TASKS.argtypes = 'string'
304 def cmd_THING_TYPE(game, thing_type, symbol_hint):
305 game.thing_types[thing_type] = symbol_hint
306 cmd_THING_TYPE.argtypes = 'string char'
308 def cmd_THING_INSTALLED(game, thing_id):
309 game.get_thing(thing_id).installed = True
310 cmd_THING_INSTALLED.argtypes = 'int:pos'
312 def cmd_THING_CARRYING(game, thing_id):
313 game.get_thing(thing_id).carrying = True
314 cmd_THING_CARRYING.argtypes = 'int:pos'
316 def cmd_TERRAIN(game, terrain_char, terrain_desc):
317 game.terrains[terrain_char] = terrain_desc
318 cmd_TERRAIN.argtypes = 'char string'
322 cmd_PONG.argtypes = ''
324 def cmd_DEFAULT_COLORS(game):
325 game.tui.set_default_colors()
326 cmd_DEFAULT_COLORS.argtypes = ''
328 def cmd_RANDOM_COLORS(game):
329 game.tui.set_random_colors()
330 cmd_RANDOM_COLORS.argtypes = ''
332 class Game(GameBase):
333 turn_complete = False
337 def __init__(self, *args, **kwargs):
338 super().__init__(*args, **kwargs)
339 self.register_command(cmd_LOGIN_OK)
340 self.register_command(cmd_ADMIN_OK)
341 self.register_command(cmd_PONG)
342 self.register_command(cmd_CHAT)
343 self.register_command(cmd_REPLY)
344 self.register_command(cmd_PLAYER_ID)
345 self.register_command(cmd_TURN)
346 self.register_command(cmd_THING)
347 self.register_command(cmd_THING_TYPE)
348 self.register_command(cmd_THING_NAME)
349 self.register_command(cmd_THING_CHAR)
350 self.register_command(cmd_THING_FACE)
351 self.register_command(cmd_THING_HAT)
352 self.register_command(cmd_THING_CARRYING)
353 self.register_command(cmd_THING_INSTALLED)
354 self.register_command(cmd_TERRAIN)
355 self.register_command(cmd_MAP)
356 self.register_command(cmd_MAP_CONTROL)
357 self.register_command(cmd_PORTAL)
358 self.register_command(cmd_ANNOTATION)
359 self.register_command(cmd_GAME_STATE_COMPLETE)
360 self.register_command(cmd_ARGUMENT_ERROR)
361 self.register_command(cmd_GAME_ERROR)
362 self.register_command(cmd_PLAY_ERROR)
363 self.register_command(cmd_TASKS)
364 self.register_command(cmd_FOV)
365 self.register_command(cmd_DEFAULT_COLORS)
366 self.register_command(cmd_RANDOM_COLORS)
367 self.map_content = ''
369 self.annotations = {}
373 def get_string_options(self, string_option_type):
374 if string_option_type == 'map_geometry':
375 return ['Hex', 'Square']
376 elif string_option_type == 'thing_type':
377 return self.thing_types.keys()
380 def get_command(self, command_name):
381 from functools import partial
382 f = partial(self.commands[command_name], self)
383 f.argtypes = self.commands[command_name].argtypes
388 def __init__(self, name, has_input_prompt=False, shows_info=False,
389 is_intro=False, is_single_char_entry=False):
391 self.short_desc = mode_helps[name]['short']
392 self.available_modes = []
393 self.available_actions = []
394 self.has_input_prompt = has_input_prompt
395 self.shows_info = shows_info
396 self.is_intro = is_intro
397 self.help_intro = mode_helps[name]['long']
398 self.intro_msg = mode_helps[name]['intro']
399 self.is_single_char_entry = is_single_char_entry
402 def iter_available_modes(self, tui):
403 for mode_name in self.available_modes:
404 mode = getattr(tui, 'mode_' + mode_name)
407 key = tui.keys['switch_to_' + mode.name]
410 def list_available_modes(self, tui):
412 if len(self.available_modes) > 0:
413 msg = 'Other modes available from here:\n'
414 for mode, key in self.iter_available_modes(tui):
415 msg += '[%s] – %s\n' % (key, mode.short_desc)
418 def mode_switch_on_key(self, tui, key_pressed):
419 for mode, key in self.iter_available_modes(tui):
420 if key_pressed == key:
421 tui.switch_mode(mode.name)
426 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
427 mode_admin = Mode('admin')
428 mode_play = Mode('play')
429 mode_study = Mode('study', shows_info=True)
430 mode_write = Mode('write', is_single_char_entry=True)
431 mode_edit = Mode('edit')
432 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
433 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
434 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
435 mode_control_tile_draw = Mode('control_tile_draw')
436 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
437 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
438 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
439 mode_chat = Mode('chat', has_input_prompt=True)
440 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
441 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
442 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
443 mode_password = Mode('password', has_input_prompt=True)
444 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
445 mode_command_thing = Mode('command_thing', has_input_prompt=True)
446 mode_take_thing = Mode('take_thing', has_input_prompt=True)
447 mode_enter_face = Mode('enter_face', has_input_prompt=True)
451 def __init__(self, host):
454 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
455 "command_thing", "take_thing"]
456 self.mode_play.available_actions = ["move", "drop_thing",
457 "teleport", "door", "consume",
459 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
460 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
461 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
462 "control_tile_type", "chat",
463 "study", "play", "edit"]
464 self.mode_admin.available_actions = ["move"]
465 self.mode_control_tile_draw.available_modes = ["admin_enter"]
466 self.mode_control_tile_draw.available_actions = ["move_explorer",
468 self.mode_edit.available_modes = ["write", "annotate", "portal", "name_thing",
469 "password", "chat", "study", "play",
470 "admin_enter", "enter_face"]
471 self.mode_edit.available_actions = ["move", "flatten", "toggle_map_mode"]
476 self.parser = Parser(self.game)
478 self.do_refresh = True
479 self.queue = queue.Queue()
480 self.login_name = None
481 self.map_mode = 'terrain + things'
482 self.password = 'foo'
483 self.switch_mode('waiting_for_server')
485 'switch_to_chat': 't',
486 'switch_to_play': 'p',
487 'switch_to_password': 'P',
488 'switch_to_annotate': 'M',
489 'switch_to_portal': 'T',
490 'switch_to_study': '?',
491 'switch_to_edit': 'E',
492 'switch_to_write': 'm',
493 'switch_to_name_thing': 'N',
494 'switch_to_command_thing': 'O',
495 'switch_to_admin_enter': 'A',
496 'switch_to_control_pw_type': 'C',
497 'switch_to_control_tile_type': 'Q',
498 'switch_to_admin_thing_protect': 'T',
500 'switch_to_enter_face': 'f',
501 'switch_to_take_thing': 'z',
509 'toggle_map_mode': 'L',
510 'toggle_tile_draw': 'm',
511 'hex_move_upleft': 'w',
512 'hex_move_upright': 'e',
513 'hex_move_right': 'd',
514 'hex_move_downright': 'x',
515 'hex_move_downleft': 'y',
516 'hex_move_left': 'a',
517 'square_move_up': 'w',
518 'square_move_left': 'a',
519 'square_move_down': 's',
520 'square_move_right': 'd',
522 if os.path.isfile('config.json'):
523 with open('config.json', 'r') as f:
524 keys_conf = json.loads(f.read())
526 self.keys[k] = keys_conf[k]
527 self.show_help = False
528 self.disconnected = True
529 self.force_instant_connect = True
530 self.input_lines = []
534 self.offset = YX(0,0)
535 curses.wrapper(self.loop)
539 def handle_recv(msg):
545 self.log_msg('@ attempting connect')
546 socket_client_class = PlomSocketClient
547 if self.host.startswith('ws://') or self.host.startswith('wss://'):
548 socket_client_class = WebSocketClient
550 self.socket = socket_client_class(handle_recv, self.host)
551 self.socket_thread = threading.Thread(target=self.socket.run)
552 self.socket_thread.start()
553 self.disconnected = False
554 self.game.thing_types = {}
555 self.game.terrains = {}
556 time.sleep(0.1) # give potential SSL negotation some time …
557 self.socket.send('TASKS')
558 self.socket.send('TERRAINS')
559 self.socket.send('THING_TYPES')
560 self.switch_mode('login')
561 except ConnectionRefusedError:
562 self.log_msg('@ server connect failure')
563 self.disconnected = True
564 self.switch_mode('waiting_for_server')
565 self.do_refresh = True
568 self.log_msg('@ attempting reconnect')
570 # necessitated by some strange SSL race conditions with ws4py
571 time.sleep(0.1) # FIXME find out why exactly necessary
572 self.switch_mode('waiting_for_server')
577 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
578 raise BrokenSocketConnection
579 self.socket.send(msg)
580 except (BrokenPipeError, BrokenSocketConnection):
581 self.log_msg('@ server disconnected :(')
582 self.disconnected = True
583 self.force_instant_connect = True
584 self.do_refresh = True
586 def log_msg(self, msg):
588 if len(self.log) > 100:
589 self.log = self.log[-100:]
591 def restore_input_values(self):
592 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
593 self.input_ = self.game.annotations[self.explorer]
594 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
595 self.input_ = self.game.portals[self.explorer]
596 elif self.mode.name == 'password':
597 self.input_ = self.password
598 elif self.mode.name == 'name_thing':
599 if hasattr(self.thing_selected, 'name'):
600 self.input_ = self.thing_selected.name
601 elif self.mode.name == 'admin_thing_protect':
602 if hasattr(self.thing_selected, 'protection'):
603 self.input_ = self.thing_selected.protection
605 def send_tile_control_command(self):
606 self.send('SET_TILE_CONTROL %s %s' %
607 (self.explorer, quote(self.tile_control_char)))
609 def toggle_map_mode(self):
610 if self.map_mode == 'terrain only':
611 self.map_mode = 'terrain + annotations'
612 elif self.map_mode == 'terrain + annotations':
613 self.map_mode = 'terrain + things'
614 elif self.map_mode == 'terrain + things':
615 self.map_mode = 'protections'
616 elif self.map_mode == 'protections':
617 self.map_mode = 'terrain only'
619 def switch_mode(self, mode_name):
620 if self.mode and self.mode.name == 'control_tile_draw':
621 self.log_msg('@ finished tile protection drawing.')
622 self.tile_draw = False
623 if mode_name == 'admin_enter' and self.is_admin:
625 elif mode_name in {'name_thing', 'admin_thing_protect'}:
626 player = self.game.get_thing(self.game.player_id)
628 for t in [t for t in self.game.things if t.position == player.position
629 and t.id_ != player.id_]:
634 self.log_msg('? not standing over thing')
637 self.thing_selected = thing
638 self.mode = getattr(self, 'mode_' + mode_name)
639 if self.mode.name in {'control_tile_draw', 'control_tile_type',
641 self.map_mode = 'protections'
642 elif self.mode.name != 'edit':
643 self.map_mode = 'terrain + things'
644 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
645 player = self.game.get_thing(self.game.player_id)
646 self.explorer = YX(player.position.y, player.position.x)
647 if self.mode.is_single_char_entry:
648 self.show_help = True
649 if len(self.mode.intro_msg) > 0:
650 self.log_msg(self.mode.intro_msg)
651 if self.mode.name == 'login':
653 self.send('LOGIN ' + quote(self.login_name))
655 self.log_msg('@ enter username')
656 elif self.mode.name == 'take_thing':
657 self.log_msg('Portable things in reach for pick-up:')
658 player = self.game.get_thing(self.game.player_id)
659 select_range = [player.position,
660 player.position + YX(0,-1),
661 player.position + YX(0, 1),
662 player.position + YX(-1, 0),
663 player.position + YX(1, 0)]
664 if type(self.game.map_geometry) == MapGeometryHex:
665 if player.position.y % 2:
666 select_range += [player.position + YX(-1, 1),
667 player.position + YX(1, 1)]
669 select_range += [player.position + YX(-1, -1),
670 player.position + YX(1, -1)]
671 self.selectables = [t for t in self.game.things
672 if t.portable and t.position in select_range]
673 if len(self.selectables) == 0:
676 self.switch_mode('play')
679 for i in range(len(self.selectables)):
680 t = self.selectables[i]
681 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
682 elif self.mode.name == 'command_thing':
683 self.send('TASK:COMMAND ' + quote('HELP'))
684 elif self.mode.name == 'control_pw_pw':
685 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
686 elif self.mode.name == 'control_tile_draw':
687 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']))
689 self.restore_input_values()
691 def set_default_colors(self):
692 curses.init_color(1, 1000, 1000, 1000)
693 curses.init_color(2, 0, 0, 0)
694 self.do_refresh = True
696 def set_random_colors(self):
700 return int(offset + random.random()*375)
702 curses.init_color(1, rand(625), rand(625), rand(625))
703 curses.init_color(2, rand(0), rand(0), rand(0))
704 self.do_refresh = True
708 return self.info_cached
709 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
711 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
712 info_to_cache += 'outside field of view'
714 for t in self.game.things:
715 if t.position == self.explorer:
716 info_to_cache += 'THING: %s' % self.get_thing_info(t)
717 protection = t.protection
718 if protection == '.':
720 info_to_cache += ' / protection: %s\n' % protection
721 if hasattr(t, 'hat'):
722 info_to_cache += t.hat[0:6] + '\n'
723 info_to_cache += t.hat[6:12] + '\n'
724 info_to_cache += t.hat[12:18] + '\n'
725 if hasattr(t, 'face'):
726 info_to_cache += t.face[0:6] + '\n'
727 info_to_cache += t.face[6:12] + '\n'
728 info_to_cache += t.face[12:18] + '\n'
729 terrain_char = self.game.map_content[pos_i]
731 if terrain_char in self.game.terrains:
732 terrain_desc = self.game.terrains[terrain_char]
733 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
735 protection = self.game.map_control_content[pos_i]
736 if protection == '.':
737 protection = 'unprotected'
738 info_to_cache += 'PROTECTION: %s\n' % protection
739 if self.explorer in self.game.portals:
740 info_to_cache += 'PORTAL: ' +\
741 self.game.portals[self.explorer] + '\n'
743 info_to_cache += 'PORTAL: (none)\n'
744 if self.explorer in self.game.annotations:
745 info_to_cache += 'ANNOTATION: ' +\
746 self.game.annotations[self.explorer]
747 self.info_cached = info_to_cache
748 return self.info_cached
750 def get_thing_info(self, t):
752 (t.type_, self.game.thing_types[t.type_])
753 if hasattr(t, 'thing_char'):
755 if hasattr(t, 'name'):
756 info += ' (%s)' % t.name
757 if hasattr(t, 'installed'):
758 info += ' / installed'
761 def loop(self, stdscr):
764 def safe_addstr(y, x, line):
765 if y < self.size.y - 1 or x + len(line) < self.size.x:
766 stdscr.addstr(y, x, line, curses.color_pair(1))
767 else: # workaround to <https://stackoverflow.com/q/7063128>
768 cut_i = self.size.x - x - 1
770 last_char = line[cut_i]
771 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
772 stdscr.insstr(y, self.size.x - 2, ' ')
773 stdscr.addstr(y, x, cut, curses.color_pair(1))
775 def handle_input(msg):
776 command, args = self.parser.parse(msg)
779 def task_action_on(action):
780 return action_tasks[action] in self.game.tasks
782 def msg_into_lines_of_width(msg, width):
786 for i in range(len(msg)):
787 if x >= width or msg[i] == "\n":
799 def reset_screen_size():
800 self.size = YX(*stdscr.getmaxyx())
801 self.size = self.size - YX(self.size.y % 4, 0)
802 self.size = self.size - YX(0, self.size.x % 4)
803 self.window_width = int(self.size.x / 2)
805 def recalc_input_lines():
806 if not self.mode.has_input_prompt:
807 self.input_lines = []
809 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
812 def move_explorer(direction):
813 target = self.game.map_geometry.move_yx(self.explorer, direction)
815 self.info_cached = None
816 self.explorer = target
818 self.send_tile_control_command()
824 for line in self.log:
825 lines += msg_into_lines_of_width(line, self.window_width)
828 max_y = self.size.y - len(self.input_lines)
829 for i in range(len(lines)):
830 if (i >= max_y - height_header):
832 safe_addstr(max_y - i - 1, self.window_width, lines[i])
835 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
836 lines = msg_into_lines_of_width(info, self.window_width)
838 for i in range(len(lines)):
839 y = height_header + i
840 if y >= self.size.y - len(self.input_lines):
842 safe_addstr(y, self.window_width, lines[i])
845 y = self.size.y - len(self.input_lines)
846 for i in range(len(self.input_lines)):
847 safe_addstr(y, self.window_width, self.input_lines[i])
851 if not self.game.turn_complete:
853 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
856 help = "hit [%s] for help" % self.keys['help']
857 if self.mode.has_input_prompt:
858 help = "enter /help for help"
859 safe_addstr(1, self.window_width,
860 'MODE: %s – %s' % (self.mode.short_desc, help))
863 if not self.game.turn_complete and len(self.map_lines) == 0:
865 if self.game.turn_complete:
867 for y in range(self.game.map_geometry.size.y):
868 start = self.game.map_geometry.size.x * y
869 end = start + self.game.map_geometry.size.x
870 if self.map_mode == 'protections':
871 map_lines_split += [[c + ' ' for c
872 in self.game.map_control_content[start:end]]]
874 map_lines_split += [[c + ' ' for c
875 in self.game.map_content[start:end]]]
876 if self.map_mode == 'terrain + annotations':
877 for p in self.game.annotations:
878 map_lines_split[p.y][p.x] = 'A '
879 elif self.map_mode == 'terrain + things':
880 for p in self.game.portals.keys():
881 original = map_lines_split[p.y][p.x]
882 map_lines_split[p.y][p.x] = original[0] + 'P'
885 def draw_thing(t, used_positions):
886 symbol = self.game.thing_types[t.type_]
888 if hasattr(t, 'thing_char'):
889 meta_char = t.thing_char
890 if t.position in used_positions:
892 if hasattr(t, 'carrying') and t.carrying:
894 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
895 used_positions += [t.position]
897 for t in [t for t in self.game.things if t.type_ != 'Player']:
898 draw_thing(t, used_positions)
899 for t in [t for t in self.game.things if t.type_ == 'Player']:
900 draw_thing(t, used_positions)
901 player = self.game.get_thing(self.game.player_id)
902 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
903 map_lines_split[self.explorer.y][self.explorer.x] = '??'
904 elif self.map_mode != 'terrain + things':
905 map_lines_split[player.position.y][player.position.x] = '??'
907 if type(self.game.map_geometry) == MapGeometryHex:
909 for line in map_lines_split:
910 self.map_lines += [indent * ' ' + ''.join(line)]
911 indent = 0 if indent else 1
913 for line in map_lines_split:
914 self.map_lines += [''.join(line)]
915 window_center = YX(int(self.size.y / 2),
916 int(self.window_width / 2))
917 center = player.position
918 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
919 center = self.explorer
920 center = YX(center.y, center.x * 2)
921 self.offset = center - window_center
922 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
923 self.offset += YX(0, 1)
924 term_y = max(0, -self.offset.y)
925 term_x = max(0, -self.offset.x)
926 map_y = max(0, self.offset.y)
927 map_x = max(0, self.offset.x)
928 while term_y < self.size.y and map_y < len(self.map_lines):
929 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
930 safe_addstr(term_y, term_x, to_draw)
935 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
936 self.mode.help_intro)
937 if len(self.mode.available_actions) > 0:
938 content += "Available actions:\n"
939 for action in self.mode.available_actions:
940 if action in action_tasks:
941 if action_tasks[action] not in self.game.tasks:
943 if action == 'move_explorer':
946 key = ','.join(self.movement_keys)
948 key = self.keys[action]
949 content += '[%s] – %s\n' % (key, action_descriptions[action])
951 content += self.mode.list_available_modes(self)
952 for i in range(self.size.y):
954 self.window_width * (not self.mode.has_input_prompt),
955 ' ' * self.window_width)
957 for line in content.split('\n'):
958 lines += msg_into_lines_of_width(line, self.window_width)
959 for i in range(len(lines)):
963 self.window_width * (not self.mode.has_input_prompt),
968 stdscr.bkgd(' ', curses.color_pair(1))
970 if self.mode.has_input_prompt:
972 if self.mode.shows_info:
977 if not self.mode.is_intro:
983 action_descriptions = {
985 'flatten': 'flatten surroundings',
986 'teleport': 'teleport',
987 'take_thing': 'pick up thing',
988 'drop_thing': 'drop thing',
989 'toggle_map_mode': 'toggle map view',
990 'toggle_tile_draw': 'toggle protection character drawing',
991 'install': '(un-)install',
993 'door': 'open/close',
994 'consume': 'consume',
998 'flatten': 'FLATTEN_SURROUNDINGS',
999 'take_thing': 'PICK_UP',
1000 'drop_thing': 'DROP',
1002 'install': 'INSTALL',
1005 'command': 'COMMAND',
1006 'consume': 'INTOXICATE',
1009 curses.curs_set(False) # hide cursor
1010 curses.start_color()
1011 self.set_default_colors()
1012 curses.init_pair(1, 1, 2)
1015 self.explorer = YX(0, 0)
1018 interval = datetime.timedelta(seconds=5)
1019 last_ping = datetime.datetime.now() - interval
1021 if self.disconnected and self.force_instant_connect:
1022 self.force_instant_connect = False
1024 now = datetime.datetime.now()
1025 if now - last_ping > interval:
1026 if self.disconnected:
1036 self.do_refresh = False
1039 msg = self.queue.get(block=False)
1044 key = stdscr.getkey()
1045 self.do_refresh = True
1046 except curses.error:
1048 self.show_help = False
1049 if key == 'KEY_RESIZE':
1051 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1052 self.input_ = self.input_[:-1]
1053 elif self.mode.has_input_prompt and key == '\n' and self.input_ == ''\
1054 and self.mode.name in {'chat', 'command_thing', 'take_thing',
1056 if self.mode.name != 'chat':
1057 self.log_msg('@ aborted')
1058 self.switch_mode('play')
1059 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1060 self.show_help = True
1062 self.restore_input_values()
1063 elif self.mode.has_input_prompt and key != '\n': # Return key
1065 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1066 if len(self.input_) > max_length:
1067 self.input_ = self.input_[:max_length]
1068 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1069 self.show_help = True
1070 elif self.mode.name == 'login' and key == '\n':
1071 self.login_name = self.input_
1072 self.send('LOGIN ' + quote(self.input_))
1074 elif self.mode.name == 'enter_face' and key == '\n':
1075 if len(self.input_) != 18:
1076 self.log_msg('? wrong input length, aborting')
1078 self.send('PLAYER_FACE %s' % quote(self.input_))
1080 self.switch_mode('edit')
1081 elif self.mode.name == 'take_thing' and key == '\n':
1083 i = int(self.input_)
1084 if i < 0 or i >= len(self.selectables):
1085 self.log_msg('? invalid index, aborted')
1087 self.send('TASK:PICK_UP %s' % self.selectables[i].id_)
1089 self.log_msg('? invalid index, aborted')
1091 self.switch_mode('play')
1092 elif self.mode.name == 'command_thing' and key == '\n':
1093 if task_action_on('command'):
1094 self.send('TASK:COMMAND ' + quote(self.input_))
1096 elif self.mode.name == 'control_pw_pw' and key == '\n':
1097 if self.input_ == '':
1098 self.log_msg('@ aborted')
1100 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1101 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1102 self.switch_mode('admin')
1103 elif self.mode.name == 'password' and key == '\n':
1104 if self.input_ == '':
1106 self.password = self.input_
1107 self.switch_mode('edit')
1108 elif self.mode.name == 'admin_enter' and key == '\n':
1109 self.send('BECOME_ADMIN ' + quote(self.input_))
1110 self.switch_mode('play')
1111 elif self.mode.name == 'control_pw_type' and key == '\n':
1112 if len(self.input_) != 1:
1113 self.log_msg('@ entered non-single-char, therefore aborted')
1114 self.switch_mode('admin')
1116 self.tile_control_char = self.input_
1117 self.switch_mode('control_pw_pw')
1118 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1119 if len(self.input_) != 1:
1120 self.log_msg('@ entered non-single-char, therefore aborted')
1122 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1123 quote(self.input_)))
1124 self.log_msg('@ sent new protection character for thing')
1125 self.switch_mode('admin')
1126 elif self.mode.name == 'control_tile_type' and key == '\n':
1127 if len(self.input_) != 1:
1128 self.log_msg('@ entered non-single-char, therefore aborted')
1129 self.switch_mode('admin')
1131 self.tile_control_char = self.input_
1132 self.switch_mode('control_tile_draw')
1133 elif self.mode.name == 'chat' and key == '\n':
1134 if self.input_ == '':
1136 if self.input_[0] == '/':
1137 if self.input_.startswith('/nick'):
1138 tokens = self.input_.split(maxsplit=1)
1139 if len(tokens) == 2:
1140 self.send('NICK ' + quote(tokens[1]))
1142 self.log_msg('? need login name')
1144 self.log_msg('? unknown command')
1146 self.send('ALL ' + quote(self.input_))
1148 elif self.mode.name == 'name_thing' and key == '\n':
1149 if self.input_ == '':
1151 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1153 quote(self.password)))
1154 self.switch_mode('edit')
1155 elif self.mode.name == 'annotate' and key == '\n':
1156 if self.input_ == '':
1158 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1159 quote(self.password)))
1160 self.switch_mode('edit')
1161 elif self.mode.name == 'portal' and key == '\n':
1162 if self.input_ == '':
1164 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1165 quote(self.password)))
1166 self.switch_mode('edit')
1167 elif self.mode.name == 'study':
1168 if self.mode.mode_switch_on_key(self, key):
1170 elif key == self.keys['toggle_map_mode']:
1171 self.toggle_map_mode()
1172 elif key in self.movement_keys:
1173 move_explorer(self.movement_keys[key])
1174 elif self.mode.name == 'play':
1175 if self.mode.mode_switch_on_key(self, key):
1177 elif key == self.keys['drop_thing'] and task_action_on('drop_thing'):
1178 self.send('TASK:DROP')
1179 elif key == self.keys['door'] and task_action_on('door'):
1180 self.send('TASK:DOOR')
1181 elif key == self.keys['consume'] and task_action_on('consume'):
1182 self.send('TASK:INTOXICATE')
1183 elif key == self.keys['install'] and task_action_on('install'):
1184 self.send('TASK:INSTALL')
1185 elif key == self.keys['wear'] and task_action_on('wear'):
1186 self.send('TASK:WEAR')
1187 elif key == self.keys['teleport']:
1188 player = self.game.get_thing(self.game.player_id)
1189 if player.position in self.game.portals:
1190 self.host = self.game.portals[player.position]
1194 self.log_msg('? not standing on portal')
1195 elif key in self.movement_keys and task_action_on('move'):
1196 self.send('TASK:MOVE ' + self.movement_keys[key])
1197 elif self.mode.name == 'write':
1198 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1199 self.switch_mode('edit')
1200 elif self.mode.name == 'control_tile_draw':
1201 if self.mode.mode_switch_on_key(self, key):
1203 elif key in self.movement_keys:
1204 move_explorer(self.movement_keys[key])
1205 elif key == self.keys['toggle_tile_draw']:
1206 self.tile_draw = False if self.tile_draw else True
1207 elif self.mode.name == 'admin':
1208 if self.mode.mode_switch_on_key(self, key):
1210 elif key in self.movement_keys and task_action_on('move'):
1211 self.send('TASK:MOVE ' + self.movement_keys[key])
1212 elif self.mode.name == 'edit':
1213 if self.mode.mode_switch_on_key(self, key):
1215 elif key == self.keys['flatten'] and task_action_on('flatten'):
1216 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1217 elif key == self.keys['toggle_map_mode']:
1218 self.toggle_map_mode()
1219 elif key in self.movement_keys and task_action_on('move'):
1220 self.send('TASK:MOVE ' + self.movement_keys[key])
1222 if len(sys.argv) != 2:
1223 raise ArgError('wrong number of arguments, need game host')