7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
12 from plomrogue.errors import BrokenSocketConnection, ArgError
18 'long': 'This mode allows you to interact with the map in various ways.'
23 'long': 'This mode allows you to study the map and its tiles in detail. Move the question mark over a tile, and the right half of the screen will show detailed information on it. Toggle the map view to show or hide different information layers.'},
25 'short': 'world edit',
27 'long': 'This mode allows you to change the game world in various ways. Individual map tiles can be protected by "protection characters", which you can see by toggling into the protections map view. You can edit a tile if you set the world edit password that matches its protection character. The character "." marks the absence of protection: Such tiles can always be edited.'
30 'short': 'name thing',
32 'long': 'Give name to/change name of thing here.'
35 'short': 'command thing',
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
40 'short': 'take thing',
41 'intro': 'Pick up a thing in reach by entering its index number. Enter nothing to abort.',
42 'long': 'You see a list of things which you could pick up. Enter the target thing\'s index, or, to leave, nothing.'
45 'short': 'drop thing',
46 'intro': 'Enter number of direction to which you want to drop thing.',
47 'long': 'Drop currently carried thing by entering the target direction index. Enter nothing to return to play mode..'
49 'admin_thing_protect': {
50 'short': 'change thing protection',
51 'intro': '@ enter thing protection character:',
52 'long': 'Change protection character for thing here.'
55 'short': 'enter your face',
56 'intro': '@ enter face line (enter nothing to abort):',
57 'long': 'Draw your face as ASCII art. The string you enter must be 18 characters long, and will be divided on display into 3 lines of 6 characters each, from top to bottom..'
60 'short': 'change terrain',
62 'long': 'This mode allows you to change the map tile you currently stand on (if your world editing password authorizes you so). Just enter any printable ASCII character to imprint it on the ground below you.'
65 'short': 'change protection character password',
66 'intro': '@ enter protection character for which you want to change the password:',
67 'long': 'This mode is the first of two steps to change the password for a protection character. First enter the protection character for which you want to change the password.'
70 'short': 'change protection character password',
72 'long': 'This mode is the second of two steps to change the password for a protection character. Enter the new password for the protection character you chose.'
74 'control_tile_type': {
75 'short': 'change tiles protection',
76 'intro': '@ enter protection character which you want to draw:',
77 'long': 'This mode is the first of two steps to change tile protection areas on the map. First enter the tile protection character you want to write.'
79 'control_tile_draw': {
80 'short': 'change tiles protection',
82 'long': 'This mode is the second of two steps to change tile protection areas on the map. Toggle tile protection drawing on/off and move the ?? cursor around the map to draw the selected protection character.'
85 'short': 'annotate tile',
87 'long': 'This mode allows you to add/edit a comment on the tile you are currently standing on (provided your world editing password authorizes you so). Hit Return to leave.'
90 'short': 'edit portal',
92 'long': 'This mode allows you to imprint/edit/remove a teleportation target on the ground you are currently standing on (provided your world editing password authorizes you so). Enter or edit a URL to imprint a teleportation target; enter emptiness to remove a pre-existing teleportation target. Hit Return to leave.'
97 'long': 'This mode allows you to engage in chit-chat with other users. Any line you enter into the input prompt that does not start with a "/" will be sent out to nearby players – but barriers and distance will reduce what they can read, so stand close to them to ensure they get your message. Lines that start with a "/" are used for commands like:\n\n/nick NAME – re-name yourself to NAME'
102 'long': 'Enter your player name.'
104 'waiting_for_server': {
105 'short': 'waiting for server response',
106 'intro': '@ waiting for server …',
107 'long': 'Waiting for a server response.'
110 'short': 'waiting for server response',
112 'long': 'Waiting for a server response.'
115 'short': 'set world edit password',
117 'long': 'This mode allows you to change the password that you send to authorize yourself for editing password-protected elements of the world. Hit return to confirm and leave.'
120 'short': 'become admin',
121 'intro': '@ enter admin password:',
122 'long': 'This mode allows you to become admin if you know an admin password.'
127 'long': 'This mode allows you access to actions limited to administrators.'
131 from ws4py.client import WebSocketBaseClient
132 class WebSocketClient(WebSocketBaseClient):
134 def __init__(self, recv_handler, *args, **kwargs):
135 super().__init__(*args, **kwargs)
136 self.recv_handler = recv_handler
139 def received_message(self, message):
141 message = str(message)
142 self.recv_handler(message)
145 def plom_closed(self):
146 return self.client_terminated
148 from plomrogue.io_tcp import PlomSocket
149 class PlomSocketClient(PlomSocket):
151 def __init__(self, recv_handler, url):
153 self.recv_handler = recv_handler
154 host, port = url.split(':')
155 super().__init__(socket.create_connection((host, port)))
163 for msg in self.recv():
164 if msg == 'NEED_SSL':
165 self.socket = ssl.wrap_socket(self.socket)
167 self.recv_handler(msg)
168 except BrokenSocketConnection:
169 pass # we assume socket will be known as dead by now
171 def cmd_TURN(game, n):
172 game.annotations = {}
176 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_LOGIN_OK(game):
181 game.tui.switch_mode('post_login_wait')
182 game.tui.send('GET_GAMESTATE')
183 game.tui.log_msg('@ welcome')
184 cmd_LOGIN_OK.argtypes = ''
186 def cmd_ADMIN_OK(game):
187 game.tui.is_admin = True
188 game.tui.log_msg('@ you now have admin rights')
189 game.tui.switch_mode('admin')
190 game.tui.do_refresh = True
191 cmd_ADMIN_OK.argtypes = ''
193 def cmd_REPLY(game, msg):
194 game.tui.log_msg('#MUSICPLAYER: ' + msg)
195 game.tui.do_refresh = True
196 cmd_REPLY.argtypes = 'string'
198 def cmd_CHAT(game, msg):
199 game.tui.log_msg('# ' + msg)
200 game.tui.do_refresh = True
201 cmd_CHAT.argtypes = 'string'
203 def cmd_CHATFACE(game, thing_id):
204 game.tui.draw_face = thing_id
205 game.tui.do_refresh = True
206 cmd_CHATFACE.argtypes = 'int:pos'
208 def cmd_PLAYER_ID(game, player_id):
209 game.player_id = player_id
210 cmd_PLAYER_ID.argtypes = 'int:nonneg'
212 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
213 t = game.get_thing(thing_id)
215 t = ThingBase(game, thing_id)
219 t.protection = protection
220 t.portable = portable
221 t.commandable = commandable
222 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
224 def cmd_THING_NAME(game, thing_id, name):
225 t = game.get_thing(thing_id)
227 cmd_THING_NAME.argtypes = 'int:pos string'
229 def cmd_THING_FACE(game, thing_id, face):
230 t = game.get_thing(thing_id)
232 cmd_THING_FACE.argtypes = 'int:pos string'
234 def cmd_THING_HAT(game, thing_id, hat):
235 t = game.get_thing(thing_id)
237 cmd_THING_HAT.argtypes = 'int:pos string'
239 def cmd_THING_CHAR(game, thing_id, c):
240 t = game.get_thing(thing_id)
242 cmd_THING_CHAR.argtypes = 'int:pos char'
244 def cmd_MAP(game, geometry, size, content):
245 map_geometry_class = globals()['MapGeometry' + geometry]
246 game.map_geometry = map_geometry_class(size)
247 game.map_content = content
248 if type(game.map_geometry) == MapGeometrySquare:
249 game.tui.movement_keys = {
250 game.tui.keys['square_move_up']: 'UP',
251 game.tui.keys['square_move_left']: 'LEFT',
252 game.tui.keys['square_move_down']: 'DOWN',
253 game.tui.keys['square_move_right']: 'RIGHT',
255 elif type(game.map_geometry) == MapGeometryHex:
256 game.tui.movement_keys = {
257 game.tui.keys['hex_move_upleft']: 'UPLEFT',
258 game.tui.keys['hex_move_upright']: 'UPRIGHT',
259 game.tui.keys['hex_move_right']: 'RIGHT',
260 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
261 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
262 game.tui.keys['hex_move_left']: 'LEFT',
264 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
266 def cmd_FOV(game, content):
268 cmd_FOV.argtypes = 'string'
270 def cmd_MAP_CONTROL(game, content):
271 game.map_control_content = content
272 cmd_MAP_CONTROL.argtypes = 'string'
274 def cmd_GAME_STATE_COMPLETE(game):
275 game.turn_complete = True
276 game.tui.do_refresh = True
277 game.tui.info_cached = None
278 game.player = game.get_thing(game.player_id)
279 if game.tui.mode.name == 'post_login_wait':
280 game.tui.switch_mode('play')
281 cmd_GAME_STATE_COMPLETE.argtypes = ''
283 def cmd_PORTAL(game, position, msg):
284 game.portals[position] = msg
285 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
287 def cmd_PLAY_ERROR(game, msg):
288 game.tui.log_msg('? ' + msg)
289 game.tui.flash = True
290 game.tui.do_refresh = True
291 cmd_PLAY_ERROR.argtypes = 'string'
293 def cmd_GAME_ERROR(game, msg):
294 game.tui.log_msg('? game error: ' + msg)
295 game.tui.do_refresh = True
296 cmd_GAME_ERROR.argtypes = 'string'
298 def cmd_ARGUMENT_ERROR(game, msg):
299 game.tui.log_msg('? syntax error: ' + msg)
300 game.tui.do_refresh = True
301 cmd_ARGUMENT_ERROR.argtypes = 'string'
303 def cmd_ANNOTATION(game, position, msg):
304 game.annotations[position] = msg
305 if game.tui.mode.shows_info:
306 game.tui.do_refresh = True
307 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
309 def cmd_TASKS(game, tasks_comma_separated):
310 game.tasks = tasks_comma_separated.split(',')
311 game.tui.mode_write.legal = 'WRITE' in game.tasks
312 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
313 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
314 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
315 cmd_TASKS.argtypes = 'string'
317 def cmd_THING_TYPE(game, thing_type, symbol_hint):
318 game.thing_types[thing_type] = symbol_hint
319 cmd_THING_TYPE.argtypes = 'string char'
321 def cmd_THING_INSTALLED(game, thing_id):
322 game.get_thing(thing_id).installed = True
323 cmd_THING_INSTALLED.argtypes = 'int:pos'
325 def cmd_THING_CARRYING(game, thing_id, carried_id):
326 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
327 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
329 def cmd_TERRAIN(game, terrain_char, terrain_desc):
330 game.terrains[terrain_char] = terrain_desc
331 cmd_TERRAIN.argtypes = 'char string'
335 cmd_PONG.argtypes = ''
337 def cmd_DEFAULT_COLORS(game):
338 game.tui.set_default_colors()
339 cmd_DEFAULT_COLORS.argtypes = ''
341 def cmd_RANDOM_COLORS(game):
342 game.tui.set_random_colors()
343 cmd_RANDOM_COLORS.argtypes = ''
345 class Game(GameBase):
346 turn_complete = False
350 def __init__(self, *args, **kwargs):
351 super().__init__(*args, **kwargs)
352 self.register_command(cmd_LOGIN_OK)
353 self.register_command(cmd_ADMIN_OK)
354 self.register_command(cmd_PONG)
355 self.register_command(cmd_CHAT)
356 self.register_command(cmd_CHATFACE)
357 self.register_command(cmd_REPLY)
358 self.register_command(cmd_PLAYER_ID)
359 self.register_command(cmd_TURN)
360 self.register_command(cmd_THING)
361 self.register_command(cmd_THING_TYPE)
362 self.register_command(cmd_THING_NAME)
363 self.register_command(cmd_THING_CHAR)
364 self.register_command(cmd_THING_FACE)
365 self.register_command(cmd_THING_HAT)
366 self.register_command(cmd_THING_CARRYING)
367 self.register_command(cmd_THING_INSTALLED)
368 self.register_command(cmd_TERRAIN)
369 self.register_command(cmd_MAP)
370 self.register_command(cmd_MAP_CONTROL)
371 self.register_command(cmd_PORTAL)
372 self.register_command(cmd_ANNOTATION)
373 self.register_command(cmd_GAME_STATE_COMPLETE)
374 self.register_command(cmd_ARGUMENT_ERROR)
375 self.register_command(cmd_GAME_ERROR)
376 self.register_command(cmd_PLAY_ERROR)
377 self.register_command(cmd_TASKS)
378 self.register_command(cmd_FOV)
379 self.register_command(cmd_DEFAULT_COLORS)
380 self.register_command(cmd_RANDOM_COLORS)
381 self.map_content = ''
383 self.annotations = {}
388 def get_string_options(self, string_option_type):
389 if string_option_type == 'map_geometry':
390 return ['Hex', 'Square']
391 elif string_option_type == 'thing_type':
392 return self.thing_types.keys()
395 def get_command(self, command_name):
396 from functools import partial
397 f = partial(self.commands[command_name], self)
398 f.argtypes = self.commands[command_name].argtypes
403 def __init__(self, name, has_input_prompt=False, shows_info=False,
404 is_intro=False, is_single_char_entry=False):
406 self.short_desc = mode_helps[name]['short']
407 self.available_modes = []
408 self.available_actions = []
409 self.has_input_prompt = has_input_prompt
410 self.shows_info = shows_info
411 self.is_intro = is_intro
412 self.help_intro = mode_helps[name]['long']
413 self.intro_msg = mode_helps[name]['intro']
414 self.is_single_char_entry = is_single_char_entry
417 def iter_available_modes(self, tui):
418 for mode_name in self.available_modes:
419 mode = getattr(tui, 'mode_' + mode_name)
422 key = tui.keys['switch_to_' + mode.name]
425 def list_available_modes(self, tui):
427 if len(self.available_modes) > 0:
428 msg = 'Other modes available from here:\n'
429 for mode, key in self.iter_available_modes(tui):
430 msg += '[%s] – %s\n' % (key, mode.short_desc)
433 def mode_switch_on_key(self, tui, key_pressed):
434 for mode, key in self.iter_available_modes(tui):
435 if key_pressed == key:
436 tui.switch_mode(mode.name)
441 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
442 mode_admin = Mode('admin')
443 mode_play = Mode('play')
444 mode_study = Mode('study', shows_info=True)
445 mode_write = Mode('write', is_single_char_entry=True)
446 mode_edit = Mode('edit')
447 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
448 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
449 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
450 mode_control_tile_draw = Mode('control_tile_draw')
451 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
452 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
453 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
454 mode_chat = Mode('chat', has_input_prompt=True)
455 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
456 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
457 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
458 mode_password = Mode('password', has_input_prompt=True)
459 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
460 mode_command_thing = Mode('command_thing', has_input_prompt=True)
461 mode_take_thing = Mode('take_thing', has_input_prompt=True)
462 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
463 mode_enter_face = Mode('enter_face', has_input_prompt=True)
467 def __init__(self, host):
470 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
471 "command_thing", "take_thing",
473 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
474 "install", "wear", "spin"]
475 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
476 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
477 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
478 "control_tile_type", "chat",
479 "study", "play", "edit"]
480 self.mode_admin.available_actions = ["move"]
481 self.mode_control_tile_draw.available_modes = ["admin_enter"]
482 self.mode_control_tile_draw.available_actions = ["move_explorer",
484 self.mode_edit.available_modes = ["write", "annotate", "portal",
485 "name_thing", "enter_face", "password",
486 "chat", "study", "play", "admin_enter"]
487 self.mode_edit.available_actions = ["move", "flatten", "install",
493 self.parser = Parser(self.game)
495 self.do_refresh = True
496 self.queue = queue.Queue()
497 self.login_name = None
498 self.map_mode = 'terrain + things'
499 self.password = 'foo'
500 self.switch_mode('waiting_for_server')
502 'switch_to_chat': 't',
503 'switch_to_play': 'p',
504 'switch_to_password': 'P',
505 'switch_to_annotate': 'M',
506 'switch_to_portal': 'T',
507 'switch_to_study': '?',
508 'switch_to_edit': 'E',
509 'switch_to_write': 'm',
510 'switch_to_name_thing': 'N',
511 'switch_to_command_thing': 'O',
512 'switch_to_admin_enter': 'A',
513 'switch_to_control_pw_type': 'C',
514 'switch_to_control_tile_type': 'Q',
515 'switch_to_admin_thing_protect': 'T',
517 'switch_to_enter_face': 'f',
518 'switch_to_take_thing': 'z',
519 'switch_to_drop_thing': 'u',
527 'toggle_map_mode': 'L',
528 'toggle_tile_draw': 'm',
529 'hex_move_upleft': 'w',
530 'hex_move_upright': 'e',
531 'hex_move_right': 'd',
532 'hex_move_downright': 'x',
533 'hex_move_downleft': 'y',
534 'hex_move_left': 'a',
535 'square_move_up': 'w',
536 'square_move_left': 'a',
537 'square_move_down': 's',
538 'square_move_right': 'd',
540 if os.path.isfile('config.json'):
541 with open('config.json', 'r') as f:
542 keys_conf = json.loads(f.read())
544 self.keys[k] = keys_conf[k]
545 self.show_help = False
546 self.disconnected = True
547 self.force_instant_connect = True
548 self.input_lines = []
552 self.offset = YX(0,0)
553 curses.wrapper(self.loop)
557 def handle_recv(msg):
563 self.log_msg('@ attempting connect')
564 socket_client_class = PlomSocketClient
565 if self.host.startswith('ws://') or self.host.startswith('wss://'):
566 socket_client_class = WebSocketClient
568 self.socket = socket_client_class(handle_recv, self.host)
569 self.socket_thread = threading.Thread(target=self.socket.run)
570 self.socket_thread.start()
571 self.disconnected = False
572 self.game.thing_types = {}
573 self.game.terrains = {}
574 time.sleep(0.1) # give potential SSL negotation some time …
575 self.socket.send('TASKS')
576 self.socket.send('TERRAINS')
577 self.socket.send('THING_TYPES')
578 self.switch_mode('login')
579 except ConnectionRefusedError:
580 self.log_msg('@ server connect failure')
581 self.disconnected = True
582 self.switch_mode('waiting_for_server')
583 self.do_refresh = True
586 self.log_msg('@ attempting reconnect')
588 # necessitated by some strange SSL race conditions with ws4py
589 time.sleep(0.1) # FIXME find out why exactly necessary
590 self.switch_mode('waiting_for_server')
595 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
596 raise BrokenSocketConnection
597 self.socket.send(msg)
598 except (BrokenPipeError, BrokenSocketConnection):
599 self.log_msg('@ server disconnected :(')
600 self.disconnected = True
601 self.force_instant_connect = True
602 self.do_refresh = True
604 def log_msg(self, msg):
606 if len(self.log) > 100:
607 self.log = self.log[-100:]
609 def restore_input_values(self):
610 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
611 self.input_ = self.game.annotations[self.explorer]
612 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
613 self.input_ = self.game.portals[self.explorer]
614 elif self.mode.name == 'password':
615 self.input_ = self.password
616 elif self.mode.name == 'name_thing':
617 if hasattr(self.thing_selected, 'name'):
618 self.input_ = self.thing_selected.name
619 elif self.mode.name == 'admin_thing_protect':
620 if hasattr(self.thing_selected, 'protection'):
621 self.input_ = self.thing_selected.protection
623 def send_tile_control_command(self):
624 self.send('SET_TILE_CONTROL %s %s' %
625 (self.explorer, quote(self.tile_control_char)))
627 def toggle_map_mode(self):
628 if self.map_mode == 'terrain only':
629 self.map_mode = 'terrain + annotations'
630 elif self.map_mode == 'terrain + annotations':
631 self.map_mode = 'terrain + things'
632 elif self.map_mode == 'terrain + things':
633 self.map_mode = 'protections'
634 elif self.map_mode == 'protections':
635 self.map_mode = 'terrain only'
637 def switch_mode(self, mode_name):
639 def fail(msg, return_mode='play'):
640 self.log_msg('? ' + msg)
642 self.switch_mode(return_mode)
644 if self.mode and self.mode.name == 'control_tile_draw':
645 self.log_msg('@ finished tile protection drawing.')
646 self.draw_face = False
647 self.tile_draw = False
648 if mode_name == 'command_thing' and\
649 (not self.game.player.carrying or
650 not self.game.player.carrying.commandable):
651 return fail('not carrying anything commandable')
652 if mode_name == 'take_thing' and self.game.player.carrying:
653 return fail('already carrying something')
654 if mode_name == 'drop_thing' and not self.game.player.carrying:
655 return fail('not carrying anything droppable')
656 if mode_name == 'admin_enter' and self.is_admin:
658 elif mode_name in {'name_thing', 'admin_thing_protect'}:
660 for t in [t for t in self.game.things
661 if t.position == self.game.player.position
662 and t.id_ != self.game.player.id_]:
666 return fail('not standing over thing', 'edit')
668 self.thing_selected = thing
669 self.mode = getattr(self, 'mode_' + mode_name)
670 if self.mode.name in {'control_tile_draw', 'control_tile_type',
672 self.map_mode = 'protections'
673 elif self.mode.name != 'edit':
674 self.map_mode = 'terrain + things'
675 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
676 self.explorer = YX(self.game.player.position.y,
677 self.game.player.position.x)
678 if self.mode.is_single_char_entry:
679 self.show_help = True
680 if len(self.mode.intro_msg) > 0:
681 self.log_msg(self.mode.intro_msg)
682 if self.mode.name == 'login':
684 self.send('LOGIN ' + quote(self.login_name))
686 self.log_msg('@ enter username')
687 elif self.mode.name == 'take_thing':
688 self.log_msg('Portable things in reach for pick-up:')
689 select_range = [self.game.player.position,
690 self.game.player.position + YX(0,-1),
691 self.game.player.position + YX(0, 1),
692 self.game.player.position + YX(-1, 0),
693 self.game.player.position + YX(1, 0)]
694 if type(self.game.map_geometry) == MapGeometryHex:
695 if self.game.player.position.y % 2:
696 select_range += [self.game.player.position + YX(-1, 1),
697 self.game.player.position + YX(1, 1)]
699 select_range += [self.game.player.position + YX(-1, -1),
700 self.game.player.position + YX(1, -1)]
701 self.selectables = [t.id_ for t in self.game.things
702 if t.portable and t.position in select_range]
703 if len(self.selectables) == 0:
704 return fail('nothing to pick-up')
706 for i in range(len(self.selectables)):
707 t = self.game.get_thing(self.selectables[i])
708 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
709 elif self.mode.name == 'drop_thing':
710 self.log_msg('Direction to drop thing to:')
712 ['HERE'] + list(self.game.tui.movement_keys.values())
713 for i in range(len(self.selectables)):
714 self.log_msg(str(i) + ': ' + self.selectables[i])
715 elif self.mode.name == 'command_thing':
716 self.send('TASK:COMMAND ' + quote('HELP'))
717 elif self.mode.name == 'control_pw_pw':
718 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
719 elif self.mode.name == 'control_tile_draw':
720 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']))
722 self.restore_input_values()
724 def set_default_colors(self):
725 curses.init_color(1, 1000, 1000, 1000)
726 curses.init_color(2, 0, 0, 0)
727 self.do_refresh = True
729 def set_random_colors(self):
733 return int(offset + random.random()*375)
735 curses.init_color(1, rand(625), rand(625), rand(625))
736 curses.init_color(2, rand(0), rand(0), rand(0))
737 self.do_refresh = True
741 return self.info_cached
742 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
744 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
745 info_to_cache += 'outside field of view'
747 for t in self.game.things:
748 if t.position == self.explorer:
749 info_to_cache += 'THING: %s' % self.get_thing_info(t)
750 protection = t.protection
751 if protection == '.':
753 info_to_cache += ' / protection: %s\n' % protection
754 if hasattr(t, 'hat'):
755 info_to_cache += t.hat[0:6] + '\n'
756 info_to_cache += t.hat[6:12] + '\n'
757 info_to_cache += t.hat[12:18] + '\n'
758 if hasattr(t, 'face'):
759 info_to_cache += t.face[0:6] + '\n'
760 info_to_cache += t.face[6:12] + '\n'
761 info_to_cache += t.face[12:18] + '\n'
762 terrain_char = self.game.map_content[pos_i]
764 if terrain_char in self.game.terrains:
765 terrain_desc = self.game.terrains[terrain_char]
766 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
768 protection = self.game.map_control_content[pos_i]
769 if protection == '.':
770 protection = 'unprotected'
771 info_to_cache += 'PROTECTION: %s\n' % protection
772 if self.explorer in self.game.portals:
773 info_to_cache += 'PORTAL: ' +\
774 self.game.portals[self.explorer] + '\n'
776 info_to_cache += 'PORTAL: (none)\n'
777 if self.explorer in self.game.annotations:
778 info_to_cache += 'ANNOTATION: ' +\
779 self.game.annotations[self.explorer]
780 self.info_cached = info_to_cache
781 return self.info_cached
783 def get_thing_info(self, t):
785 (t.type_, self.game.thing_types[t.type_])
786 if hasattr(t, 'thing_char'):
788 if hasattr(t, 'name'):
789 info += ' (%s)' % t.name
790 if hasattr(t, 'installed'):
791 info += ' / installed'
794 def loop(self, stdscr):
797 def safe_addstr(y, x, line):
798 if y < self.size.y - 1 or x + len(line) < self.size.x:
799 stdscr.addstr(y, x, line, curses.color_pair(1))
800 else: # workaround to <https://stackoverflow.com/q/7063128>
801 cut_i = self.size.x - x - 1
803 last_char = line[cut_i]
804 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
805 stdscr.insstr(y, self.size.x - 2, ' ')
806 stdscr.addstr(y, x, cut, curses.color_pair(1))
808 def handle_input(msg):
809 command, args = self.parser.parse(msg)
812 def task_action_on(action):
813 return action_tasks[action] in self.game.tasks
815 def msg_into_lines_of_width(msg, width):
819 for i in range(len(msg)):
820 if x >= width or msg[i] == "\n":
832 def reset_screen_size():
833 self.size = YX(*stdscr.getmaxyx())
834 self.size = self.size - YX(self.size.y % 4, 0)
835 self.size = self.size - YX(0, self.size.x % 4)
836 self.window_width = int(self.size.x / 2)
838 def recalc_input_lines():
839 if not self.mode.has_input_prompt:
840 self.input_lines = []
842 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
845 def move_explorer(direction):
846 target = self.game.map_geometry.move_yx(self.explorer, direction)
848 self.info_cached = None
849 self.explorer = target
851 self.send_tile_control_command()
857 for line in self.log:
858 lines += msg_into_lines_of_width(line, self.window_width)
861 max_y = self.size.y - len(self.input_lines)
862 for i in range(len(lines)):
863 if (i >= max_y - height_header):
865 safe_addstr(max_y - i - 1, self.window_width, lines[i])
868 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
869 lines = msg_into_lines_of_width(info, self.window_width)
871 for i in range(len(lines)):
872 y = height_header + i
873 if y >= self.size.y - len(self.input_lines):
875 safe_addstr(y, self.window_width, lines[i])
878 y = self.size.y - len(self.input_lines)
879 for i in range(len(self.input_lines)):
880 safe_addstr(y, self.window_width, self.input_lines[i])
884 if not self.game.turn_complete:
886 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
889 help = "hit [%s] for help" % self.keys['help']
890 if self.mode.has_input_prompt:
891 help = "enter /help for help"
892 safe_addstr(1, self.window_width,
893 'MODE: %s – %s' % (self.mode.short_desc, help))
896 if not self.game.turn_complete and len(self.map_lines) == 0:
898 if self.game.turn_complete:
900 for y in range(self.game.map_geometry.size.y):
901 start = self.game.map_geometry.size.x * y
902 end = start + self.game.map_geometry.size.x
903 if self.map_mode == 'protections':
904 map_lines_split += [[c + ' ' for c
905 in self.game.map_control_content[start:end]]]
907 map_lines_split += [[c + ' ' for c
908 in self.game.map_content[start:end]]]
909 if self.map_mode == 'terrain + annotations':
910 for p in self.game.annotations:
911 map_lines_split[p.y][p.x] = 'A '
912 elif self.map_mode == 'terrain + things':
913 for p in self.game.portals.keys():
914 original = map_lines_split[p.y][p.x]
915 map_lines_split[p.y][p.x] = original[0] + 'P'
918 def draw_thing(t, used_positions):
919 symbol = self.game.thing_types[t.type_]
921 if hasattr(t, 'thing_char'):
922 meta_char = t.thing_char
923 if t.position in used_positions:
925 if hasattr(t, 'carrying') and t.carrying:
927 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
928 used_positions += [t.position]
930 for t in [t for t in self.game.things if t.type_ != 'Player']:
931 draw_thing(t, used_positions)
932 for t in [t for t in self.game.things if t.type_ == 'Player']:
933 draw_thing(t, used_positions)
934 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
935 map_lines_split[self.explorer.y][self.explorer.x] = '??'
936 elif self.map_mode != 'terrain + things':
937 map_lines_split[self.game.player.position.y]\
938 [self.game.player.position.x] = '??'
940 if type(self.game.map_geometry) == MapGeometryHex:
942 for line in map_lines_split:
943 self.map_lines += [indent * ' ' + ''.join(line)]
944 indent = 0 if indent else 1
946 for line in map_lines_split:
947 self.map_lines += [''.join(line)]
948 window_center = YX(int(self.size.y / 2),
949 int(self.window_width / 2))
950 center = self.game.player.position
951 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
952 center = self.explorer
953 center = YX(center.y, center.x * 2)
954 self.offset = center - window_center
955 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
956 self.offset += YX(0, 1)
957 term_y = max(0, -self.offset.y)
958 term_x = max(0, -self.offset.x)
959 map_y = max(0, self.offset.y)
960 map_x = max(0, self.offset.x)
961 while term_y < self.size.y and map_y < len(self.map_lines):
962 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
963 safe_addstr(term_y, term_x, to_draw)
967 def draw_face_popup():
968 t = self.game.get_thing(self.draw_face)
969 if not t or not hasattr(t, 'face'):
970 self.draw_face = False
973 start_x = self.window_width - 10
975 if hasattr(t, 'thing_char'):
976 t_char = t.thing_char
977 def draw_body_part(body_part, end_y):
978 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
979 safe_addstr(end_y - 3, start_x, '| |')
980 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
981 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
982 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
984 if hasattr(t, 'face'):
985 draw_body_part(t.face, self.size.y - 2)
986 if hasattr(t, 'hat'):
987 draw_body_part(t.hat, self.size.y - 5)
988 safe_addstr(self.size.y - 1, start_x, '| |')
991 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
992 self.mode.help_intro)
993 if len(self.mode.available_actions) > 0:
994 content += "Available actions:\n"
995 for action in self.mode.available_actions:
996 if action in action_tasks:
997 if action_tasks[action] not in self.game.tasks:
999 if action == 'move_explorer':
1001 if action == 'move':
1002 key = ','.join(self.movement_keys)
1004 key = self.keys[action]
1005 content += '[%s] – %s\n' % (key, action_descriptions[action])
1007 content += self.mode.list_available_modes(self)
1008 for i in range(self.size.y):
1010 self.window_width * (not self.mode.has_input_prompt),
1011 ' ' * self.window_width)
1013 for line in content.split('\n'):
1014 lines += msg_into_lines_of_width(line, self.window_width)
1015 for i in range(len(lines)):
1016 if i >= self.size.y:
1019 self.window_width * (not self.mode.has_input_prompt),
1024 stdscr.bkgd(' ', curses.color_pair(1))
1025 recalc_input_lines()
1026 if self.mode.has_input_prompt:
1028 if self.mode.shows_info:
1033 if not self.mode.is_intro:
1038 if self.draw_face and self.mode.name in {'chat', 'play'}:
1041 def pick_selectable(task_name):
1043 i = int(self.input_)
1044 if i < 0 or i >= len(self.selectables):
1045 self.log_msg('? invalid index, aborted')
1047 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1049 self.log_msg('? invalid index, aborted')
1051 self.switch_mode('play')
1053 action_descriptions = {
1055 'flatten': 'flatten surroundings',
1056 'teleport': 'teleport',
1057 'take_thing': 'pick up thing',
1058 'drop_thing': 'drop thing',
1059 'toggle_map_mode': 'toggle map view',
1060 'toggle_tile_draw': 'toggle protection character drawing',
1061 'install': '(un-)install',
1062 'wear': '(un-)wear',
1063 'door': 'open/close',
1064 'consume': 'consume',
1069 'flatten': 'FLATTEN_SURROUNDINGS',
1070 'take_thing': 'PICK_UP',
1071 'drop_thing': 'DROP',
1073 'install': 'INSTALL',
1076 'command': 'COMMAND',
1077 'consume': 'INTOXICATE',
1081 curses.curs_set(False) # hide cursor
1082 curses.start_color()
1083 self.set_default_colors()
1084 curses.init_pair(1, 1, 2)
1087 self.explorer = YX(0, 0)
1090 interval = datetime.timedelta(seconds=5)
1091 last_ping = datetime.datetime.now() - interval
1093 if self.disconnected and self.force_instant_connect:
1094 self.force_instant_connect = False
1096 now = datetime.datetime.now()
1097 if now - last_ping > interval:
1098 if self.disconnected:
1108 self.do_refresh = False
1111 msg = self.queue.get(block=False)
1116 key = stdscr.getkey()
1117 self.do_refresh = True
1118 except curses.error:
1123 self.show_help = False
1124 self.draw_face = False
1125 if key == 'KEY_RESIZE':
1127 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1128 self.input_ = self.input_[:-1]
1129 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1130 or (self.mode.has_input_prompt and key == '\n'
1131 and self.input_ == ''\
1132 and self.mode.name in {'chat', 'command_thing',
1133 'take_thing', 'drop_thing',
1135 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1136 self.log_msg('@ aborted')
1137 self.switch_mode('play')
1138 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1139 self.show_help = True
1141 self.restore_input_values()
1142 elif self.mode.has_input_prompt and key != '\n': # Return key
1144 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1145 if len(self.input_) > max_length:
1146 self.input_ = self.input_[:max_length]
1147 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1148 self.show_help = True
1149 elif self.mode.name == 'login' and key == '\n':
1150 self.login_name = self.input_
1151 self.send('LOGIN ' + quote(self.input_))
1153 elif self.mode.name == 'enter_face' and key == '\n':
1154 if len(self.input_) != 18:
1155 self.log_msg('? wrong input length, aborting')
1157 self.send('PLAYER_FACE %s' % quote(self.input_))
1159 self.switch_mode('edit')
1160 elif self.mode.name == 'take_thing' and key == '\n':
1161 pick_selectable('PICK_UP')
1162 elif self.mode.name == 'drop_thing' and key == '\n':
1163 pick_selectable('DROP')
1164 elif self.mode.name == 'command_thing' and key == '\n':
1165 self.send('TASK:COMMAND ' + quote(self.input_))
1167 elif self.mode.name == 'control_pw_pw' and key == '\n':
1168 if self.input_ == '':
1169 self.log_msg('@ aborted')
1171 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1172 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1173 self.switch_mode('admin')
1174 elif self.mode.name == 'password' and key == '\n':
1175 if self.input_ == '':
1177 self.password = self.input_
1178 self.switch_mode('edit')
1179 elif self.mode.name == 'admin_enter' and key == '\n':
1180 self.send('BECOME_ADMIN ' + quote(self.input_))
1181 self.switch_mode('play')
1182 elif self.mode.name == 'control_pw_type' and key == '\n':
1183 if len(self.input_) != 1:
1184 self.log_msg('@ entered non-single-char, therefore aborted')
1185 self.switch_mode('admin')
1187 self.tile_control_char = self.input_
1188 self.switch_mode('control_pw_pw')
1189 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1190 if len(self.input_) != 1:
1191 self.log_msg('@ entered non-single-char, therefore aborted')
1193 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1194 quote(self.input_)))
1195 self.log_msg('@ sent new protection character for thing')
1196 self.switch_mode('admin')
1197 elif self.mode.name == 'control_tile_type' and key == '\n':
1198 if len(self.input_) != 1:
1199 self.log_msg('@ entered non-single-char, therefore aborted')
1200 self.switch_mode('admin')
1202 self.tile_control_char = self.input_
1203 self.switch_mode('control_tile_draw')
1204 elif self.mode.name == 'chat' and key == '\n':
1205 if self.input_ == '':
1207 if self.input_[0] == '/':
1208 if self.input_.startswith('/nick'):
1209 tokens = self.input_.split(maxsplit=1)
1210 if len(tokens) == 2:
1211 self.send('NICK ' + quote(tokens[1]))
1213 self.log_msg('? need login name')
1215 self.log_msg('? unknown command')
1217 self.send('ALL ' + quote(self.input_))
1219 elif self.mode.name == 'name_thing' and key == '\n':
1220 if self.input_ == '':
1222 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1224 quote(self.password)))
1225 self.switch_mode('edit')
1226 elif self.mode.name == 'annotate' and key == '\n':
1227 if self.input_ == '':
1229 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1230 quote(self.password)))
1231 self.switch_mode('edit')
1232 elif self.mode.name == 'portal' and key == '\n':
1233 if self.input_ == '':
1235 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1236 quote(self.password)))
1237 self.switch_mode('edit')
1238 elif self.mode.name == 'study':
1239 if self.mode.mode_switch_on_key(self, key):
1241 elif key == self.keys['toggle_map_mode']:
1242 self.toggle_map_mode()
1243 elif key in self.movement_keys:
1244 move_explorer(self.movement_keys[key])
1245 elif self.mode.name == 'play':
1246 if self.mode.mode_switch_on_key(self, key):
1248 elif key == self.keys['door'] and task_action_on('door'):
1249 self.send('TASK:DOOR')
1250 elif key == self.keys['consume'] and task_action_on('consume'):
1251 self.send('TASK:INTOXICATE')
1252 elif key == self.keys['wear'] and task_action_on('wear'):
1253 self.send('TASK:WEAR')
1254 elif key == self.keys['spin'] and task_action_on('spin'):
1255 self.send('TASK:SPIN')
1256 elif key == self.keys['teleport']:
1257 if self.game.player.position in self.game.portals:
1258 self.host = self.game.portals[self.game.player.position]
1262 self.log_msg('? not standing on portal')
1263 elif key in self.movement_keys and task_action_on('move'):
1264 self.send('TASK:MOVE ' + self.movement_keys[key])
1265 elif self.mode.name == 'write':
1266 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1267 self.switch_mode('edit')
1268 elif self.mode.name == 'control_tile_draw':
1269 if self.mode.mode_switch_on_key(self, key):
1271 elif key in self.movement_keys:
1272 move_explorer(self.movement_keys[key])
1273 elif key == self.keys['toggle_tile_draw']:
1274 self.tile_draw = False if self.tile_draw else True
1275 elif self.mode.name == 'admin':
1276 if self.mode.mode_switch_on_key(self, key):
1278 elif key in self.movement_keys and task_action_on('move'):
1279 self.send('TASK:MOVE ' + self.movement_keys[key])
1280 elif self.mode.name == 'edit':
1281 if self.mode.mode_switch_on_key(self, key):
1283 elif key == self.keys['flatten'] and task_action_on('flatten'):
1284 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1285 elif key == self.keys['install'] and task_action_on('install'):
1286 self.send('TASK:INSTALL %s' % quote(self.password))
1287 elif key == self.keys['toggle_map_mode']:
1288 self.toggle_map_mode()
1289 elif key in self.movement_keys and task_action_on('move'):
1290 self.send('TASK:MOVE ' + self.movement_keys[key])
1292 if len(sys.argv) != 2:
1293 raise ArgError('wrong number of arguments, need game host')