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_PLAYER_ID(game, player_id):
204 game.player_id = player_id
205 cmd_PLAYER_ID.argtypes = 'int:nonneg'
207 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
208 t = game.get_thing(thing_id)
210 t = ThingBase(game, thing_id)
214 t.protection = protection
215 t.portable = portable
216 t.commandable = commandable
217 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
219 def cmd_THING_NAME(game, thing_id, name):
220 t = game.get_thing(thing_id)
222 cmd_THING_NAME.argtypes = 'int:pos string'
224 def cmd_THING_FACE(game, thing_id, face):
225 t = game.get_thing(thing_id)
227 cmd_THING_FACE.argtypes = 'int:pos string'
229 def cmd_THING_HAT(game, thing_id, hat):
230 t = game.get_thing(thing_id)
232 cmd_THING_HAT.argtypes = 'int:pos string'
234 def cmd_THING_CHAR(game, thing_id, c):
235 t = game.get_thing(thing_id)
237 cmd_THING_CHAR.argtypes = 'int:pos char'
239 def cmd_MAP(game, geometry, size, content):
240 map_geometry_class = globals()['MapGeometry' + geometry]
241 game.map_geometry = map_geometry_class(size)
242 game.map_content = content
243 if type(game.map_geometry) == MapGeometrySquare:
244 game.tui.movement_keys = {
245 game.tui.keys['square_move_up']: 'UP',
246 game.tui.keys['square_move_left']: 'LEFT',
247 game.tui.keys['square_move_down']: 'DOWN',
248 game.tui.keys['square_move_right']: 'RIGHT',
250 elif type(game.map_geometry) == MapGeometryHex:
251 game.tui.movement_keys = {
252 game.tui.keys['hex_move_upleft']: 'UPLEFT',
253 game.tui.keys['hex_move_upright']: 'UPRIGHT',
254 game.tui.keys['hex_move_right']: 'RIGHT',
255 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
256 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
257 game.tui.keys['hex_move_left']: 'LEFT',
259 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
261 def cmd_FOV(game, content):
263 cmd_FOV.argtypes = 'string'
265 def cmd_MAP_CONTROL(game, content):
266 game.map_control_content = content
267 cmd_MAP_CONTROL.argtypes = 'string'
269 def cmd_GAME_STATE_COMPLETE(game):
270 game.turn_complete = True
271 game.tui.do_refresh = True
272 game.tui.info_cached = None
273 game.player = game.get_thing(game.player_id)
274 if game.tui.mode.name == 'post_login_wait':
275 game.tui.switch_mode('play')
276 cmd_GAME_STATE_COMPLETE.argtypes = ''
278 def cmd_PORTAL(game, position, msg):
279 game.portals[position] = msg
280 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
282 def cmd_PLAY_ERROR(game, msg):
283 game.tui.log_msg('? ' + msg)
284 game.tui.flash = True
285 game.tui.do_refresh = True
286 cmd_PLAY_ERROR.argtypes = 'string'
288 def cmd_GAME_ERROR(game, msg):
289 game.tui.log_msg('? game error: ' + msg)
290 game.tui.do_refresh = True
291 cmd_GAME_ERROR.argtypes = 'string'
293 def cmd_ARGUMENT_ERROR(game, msg):
294 game.tui.log_msg('? syntax error: ' + msg)
295 game.tui.do_refresh = True
296 cmd_ARGUMENT_ERROR.argtypes = 'string'
298 def cmd_ANNOTATION(game, position, msg):
299 game.annotations[position] = msg
300 if game.tui.mode.shows_info:
301 game.tui.do_refresh = True
302 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
304 def cmd_TASKS(game, tasks_comma_separated):
305 game.tasks = tasks_comma_separated.split(',')
306 game.tui.mode_write.legal = 'WRITE' in game.tasks
307 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
308 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
309 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
310 cmd_TASKS.argtypes = 'string'
312 def cmd_THING_TYPE(game, thing_type, symbol_hint):
313 game.thing_types[thing_type] = symbol_hint
314 cmd_THING_TYPE.argtypes = 'string char'
316 def cmd_THING_INSTALLED(game, thing_id):
317 game.get_thing(thing_id).installed = True
318 cmd_THING_INSTALLED.argtypes = 'int:pos'
320 def cmd_THING_CARRYING(game, thing_id, carried_id):
321 game.get_thing(thing_id).carrying = game.get_thing(carried_id)
322 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
324 def cmd_TERRAIN(game, terrain_char, terrain_desc):
325 game.terrains[terrain_char] = terrain_desc
326 cmd_TERRAIN.argtypes = 'char string'
330 cmd_PONG.argtypes = ''
332 def cmd_DEFAULT_COLORS(game):
333 game.tui.set_default_colors()
334 cmd_DEFAULT_COLORS.argtypes = ''
336 def cmd_RANDOM_COLORS(game):
337 game.tui.set_random_colors()
338 cmd_RANDOM_COLORS.argtypes = ''
340 class Game(GameBase):
341 turn_complete = False
345 def __init__(self, *args, **kwargs):
346 super().__init__(*args, **kwargs)
347 self.register_command(cmd_LOGIN_OK)
348 self.register_command(cmd_ADMIN_OK)
349 self.register_command(cmd_PONG)
350 self.register_command(cmd_CHAT)
351 self.register_command(cmd_REPLY)
352 self.register_command(cmd_PLAYER_ID)
353 self.register_command(cmd_TURN)
354 self.register_command(cmd_THING)
355 self.register_command(cmd_THING_TYPE)
356 self.register_command(cmd_THING_NAME)
357 self.register_command(cmd_THING_CHAR)
358 self.register_command(cmd_THING_FACE)
359 self.register_command(cmd_THING_HAT)
360 self.register_command(cmd_THING_CARRYING)
361 self.register_command(cmd_THING_INSTALLED)
362 self.register_command(cmd_TERRAIN)
363 self.register_command(cmd_MAP)
364 self.register_command(cmd_MAP_CONTROL)
365 self.register_command(cmd_PORTAL)
366 self.register_command(cmd_ANNOTATION)
367 self.register_command(cmd_GAME_STATE_COMPLETE)
368 self.register_command(cmd_ARGUMENT_ERROR)
369 self.register_command(cmd_GAME_ERROR)
370 self.register_command(cmd_PLAY_ERROR)
371 self.register_command(cmd_TASKS)
372 self.register_command(cmd_FOV)
373 self.register_command(cmd_DEFAULT_COLORS)
374 self.register_command(cmd_RANDOM_COLORS)
375 self.map_content = ''
377 self.annotations = {}
382 def get_string_options(self, string_option_type):
383 if string_option_type == 'map_geometry':
384 return ['Hex', 'Square']
385 elif string_option_type == 'thing_type':
386 return self.thing_types.keys()
389 def get_command(self, command_name):
390 from functools import partial
391 f = partial(self.commands[command_name], self)
392 f.argtypes = self.commands[command_name].argtypes
397 def __init__(self, name, has_input_prompt=False, shows_info=False,
398 is_intro=False, is_single_char_entry=False):
400 self.short_desc = mode_helps[name]['short']
401 self.available_modes = []
402 self.available_actions = []
403 self.has_input_prompt = has_input_prompt
404 self.shows_info = shows_info
405 self.is_intro = is_intro
406 self.help_intro = mode_helps[name]['long']
407 self.intro_msg = mode_helps[name]['intro']
408 self.is_single_char_entry = is_single_char_entry
411 def iter_available_modes(self, tui):
412 for mode_name in self.available_modes:
413 mode = getattr(tui, 'mode_' + mode_name)
416 key = tui.keys['switch_to_' + mode.name]
419 def list_available_modes(self, tui):
421 if len(self.available_modes) > 0:
422 msg = 'Other modes available from here:\n'
423 for mode, key in self.iter_available_modes(tui):
424 msg += '[%s] – %s\n' % (key, mode.short_desc)
427 def mode_switch_on_key(self, tui, key_pressed):
428 for mode, key in self.iter_available_modes(tui):
429 if key_pressed == key:
430 tui.switch_mode(mode.name)
435 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
436 mode_admin = Mode('admin')
437 mode_play = Mode('play')
438 mode_study = Mode('study', shows_info=True)
439 mode_write = Mode('write', is_single_char_entry=True)
440 mode_edit = Mode('edit')
441 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
442 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
443 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
444 mode_control_tile_draw = Mode('control_tile_draw')
445 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
446 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
447 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
448 mode_chat = Mode('chat', has_input_prompt=True)
449 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
450 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
451 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
452 mode_password = Mode('password', has_input_prompt=True)
453 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
454 mode_command_thing = Mode('command_thing', has_input_prompt=True)
455 mode_take_thing = Mode('take_thing', has_input_prompt=True)
456 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
457 mode_enter_face = Mode('enter_face', has_input_prompt=True)
461 def __init__(self, host):
464 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
465 "command_thing", "take_thing",
467 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
468 "install", "wear", "spin"]
469 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
470 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
471 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
472 "control_tile_type", "chat",
473 "study", "play", "edit"]
474 self.mode_admin.available_actions = ["move"]
475 self.mode_control_tile_draw.available_modes = ["admin_enter"]
476 self.mode_control_tile_draw.available_actions = ["move_explorer",
478 self.mode_edit.available_modes = ["write", "annotate", "portal",
479 "name_thing", "enter_face", "password",
480 "chat", "study", "play", "admin_enter"]
481 self.mode_edit.available_actions = ["move", "flatten", "install",
487 self.parser = Parser(self.game)
489 self.do_refresh = True
490 self.queue = queue.Queue()
491 self.login_name = None
492 self.map_mode = 'terrain + things'
493 self.password = 'foo'
494 self.switch_mode('waiting_for_server')
496 'switch_to_chat': 't',
497 'switch_to_play': 'p',
498 'switch_to_password': 'P',
499 'switch_to_annotate': 'M',
500 'switch_to_portal': 'T',
501 'switch_to_study': '?',
502 'switch_to_edit': 'E',
503 'switch_to_write': 'm',
504 'switch_to_name_thing': 'N',
505 'switch_to_command_thing': 'O',
506 'switch_to_admin_enter': 'A',
507 'switch_to_control_pw_type': 'C',
508 'switch_to_control_tile_type': 'Q',
509 'switch_to_admin_thing_protect': 'T',
511 'switch_to_enter_face': 'f',
512 'switch_to_take_thing': 'z',
513 'switch_to_drop_thing': 'u',
521 'toggle_map_mode': 'L',
522 'toggle_tile_draw': 'm',
523 'hex_move_upleft': 'w',
524 'hex_move_upright': 'e',
525 'hex_move_right': 'd',
526 'hex_move_downright': 'x',
527 'hex_move_downleft': 'y',
528 'hex_move_left': 'a',
529 'square_move_up': 'w',
530 'square_move_left': 'a',
531 'square_move_down': 's',
532 'square_move_right': 'd',
534 if os.path.isfile('config.json'):
535 with open('config.json', 'r') as f:
536 keys_conf = json.loads(f.read())
538 self.keys[k] = keys_conf[k]
539 self.show_help = False
540 self.disconnected = True
541 self.force_instant_connect = True
542 self.input_lines = []
546 self.offset = YX(0,0)
547 curses.wrapper(self.loop)
551 def handle_recv(msg):
557 self.log_msg('@ attempting connect')
558 socket_client_class = PlomSocketClient
559 if self.host.startswith('ws://') or self.host.startswith('wss://'):
560 socket_client_class = WebSocketClient
562 self.socket = socket_client_class(handle_recv, self.host)
563 self.socket_thread = threading.Thread(target=self.socket.run)
564 self.socket_thread.start()
565 self.disconnected = False
566 self.game.thing_types = {}
567 self.game.terrains = {}
568 time.sleep(0.1) # give potential SSL negotation some time …
569 self.socket.send('TASKS')
570 self.socket.send('TERRAINS')
571 self.socket.send('THING_TYPES')
572 self.switch_mode('login')
573 except ConnectionRefusedError:
574 self.log_msg('@ server connect failure')
575 self.disconnected = True
576 self.switch_mode('waiting_for_server')
577 self.do_refresh = True
580 self.log_msg('@ attempting reconnect')
582 # necessitated by some strange SSL race conditions with ws4py
583 time.sleep(0.1) # FIXME find out why exactly necessary
584 self.switch_mode('waiting_for_server')
589 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
590 raise BrokenSocketConnection
591 self.socket.send(msg)
592 except (BrokenPipeError, BrokenSocketConnection):
593 self.log_msg('@ server disconnected :(')
594 self.disconnected = True
595 self.force_instant_connect = True
596 self.do_refresh = True
598 def log_msg(self, msg):
600 if len(self.log) > 100:
601 self.log = self.log[-100:]
603 def restore_input_values(self):
604 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
605 self.input_ = self.game.annotations[self.explorer]
606 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
607 self.input_ = self.game.portals[self.explorer]
608 elif self.mode.name == 'password':
609 self.input_ = self.password
610 elif self.mode.name == 'name_thing':
611 if hasattr(self.thing_selected, 'name'):
612 self.input_ = self.thing_selected.name
613 elif self.mode.name == 'admin_thing_protect':
614 if hasattr(self.thing_selected, 'protection'):
615 self.input_ = self.thing_selected.protection
617 def send_tile_control_command(self):
618 self.send('SET_TILE_CONTROL %s %s' %
619 (self.explorer, quote(self.tile_control_char)))
621 def toggle_map_mode(self):
622 if self.map_mode == 'terrain only':
623 self.map_mode = 'terrain + annotations'
624 elif self.map_mode == 'terrain + annotations':
625 self.map_mode = 'terrain + things'
626 elif self.map_mode == 'terrain + things':
627 self.map_mode = 'protections'
628 elif self.map_mode == 'protections':
629 self.map_mode = 'terrain only'
631 def switch_mode(self, mode_name):
633 def fail(msg, return_mode='play'):
634 self.log_msg('? ' + msg)
636 self.switch_mode(return_mode)
638 if self.mode and self.mode.name == 'control_tile_draw':
639 self.log_msg('@ finished tile protection drawing.')
640 self.tile_draw = False
641 if mode_name == 'command_thing' and\
642 (not self.game.player.carrying or
643 not self.game.player.carrying.commandable):
644 return fail('not carrying anything commandable')
645 if mode_name == 'take_thing' and self.game.player.carrying:
646 return fail('already carrying something')
647 if mode_name == 'drop_thing' and not self.game.player.carrying:
648 return fail('not carrying anything droppable')
649 if mode_name == 'admin_enter' and self.is_admin:
651 elif mode_name in {'name_thing', 'admin_thing_protect'}:
653 for t in [t for t in self.game.things
654 if t.position == self.game.player.position
655 and t.id_ != self.game.player.id_]:
659 return fail('not standing over thing', 'edit')
661 self.thing_selected = thing
662 self.mode = getattr(self, 'mode_' + mode_name)
663 if self.mode.name in {'control_tile_draw', 'control_tile_type',
665 self.map_mode = 'protections'
666 elif self.mode.name != 'edit':
667 self.map_mode = 'terrain + things'
668 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
669 self.explorer = YX(self.game.player.position.y,
670 self.game.player.position.x)
671 if self.mode.is_single_char_entry:
672 self.show_help = True
673 if len(self.mode.intro_msg) > 0:
674 self.log_msg(self.mode.intro_msg)
675 if self.mode.name == 'login':
677 self.send('LOGIN ' + quote(self.login_name))
679 self.log_msg('@ enter username')
680 elif self.mode.name == 'take_thing':
681 self.log_msg('Portable things in reach for pick-up:')
682 select_range = [self.game.player.position,
683 self.game.player.position + YX(0,-1),
684 self.game.player.position + YX(0, 1),
685 self.game.player.position + YX(-1, 0),
686 self.game.player.position + YX(1, 0)]
687 if type(self.game.map_geometry) == MapGeometryHex:
688 if self.game.player.position.y % 2:
689 select_range += [self.game.player.position + YX(-1, 1),
690 self.game.player.position + YX(1, 1)]
692 select_range += [self.game.player.position + YX(-1, -1),
693 self.game.player.position + YX(1, -1)]
694 self.selectables = [t.id_ for t in self.game.things
695 if t.portable and t.position in select_range]
696 if len(self.selectables) == 0:
697 return fail('nothing to pick-up')
699 for i in range(len(self.selectables)):
700 t = self.game.get_thing(self.selectables[i])
701 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
702 elif self.mode.name == 'drop_thing':
703 self.log_msg('Direction to drop thing to:')
705 ['HERE'] + list(self.game.tui.movement_keys.values())
706 for i in range(len(self.selectables)):
707 self.log_msg(str(i) + ': ' + self.selectables[i])
708 elif self.mode.name == 'command_thing':
709 self.send('TASK:COMMAND ' + quote('HELP'))
710 elif self.mode.name == 'control_pw_pw':
711 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
712 elif self.mode.name == 'control_tile_draw':
713 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']))
715 self.restore_input_values()
717 def set_default_colors(self):
718 curses.init_color(1, 1000, 1000, 1000)
719 curses.init_color(2, 0, 0, 0)
720 self.do_refresh = True
722 def set_random_colors(self):
726 return int(offset + random.random()*375)
728 curses.init_color(1, rand(625), rand(625), rand(625))
729 curses.init_color(2, rand(0), rand(0), rand(0))
730 self.do_refresh = True
734 return self.info_cached
735 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
737 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
738 info_to_cache += 'outside field of view'
740 for t in self.game.things:
741 if t.position == self.explorer:
742 info_to_cache += 'THING: %s' % self.get_thing_info(t)
743 protection = t.protection
744 if protection == '.':
746 info_to_cache += ' / protection: %s\n' % protection
747 if hasattr(t, 'hat'):
748 info_to_cache += t.hat[0:6] + '\n'
749 info_to_cache += t.hat[6:12] + '\n'
750 info_to_cache += t.hat[12:18] + '\n'
751 if hasattr(t, 'face'):
752 info_to_cache += t.face[0:6] + '\n'
753 info_to_cache += t.face[6:12] + '\n'
754 info_to_cache += t.face[12:18] + '\n'
755 terrain_char = self.game.map_content[pos_i]
757 if terrain_char in self.game.terrains:
758 terrain_desc = self.game.terrains[terrain_char]
759 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
761 protection = self.game.map_control_content[pos_i]
762 if protection == '.':
763 protection = 'unprotected'
764 info_to_cache += 'PROTECTION: %s\n' % protection
765 if self.explorer in self.game.portals:
766 info_to_cache += 'PORTAL: ' +\
767 self.game.portals[self.explorer] + '\n'
769 info_to_cache += 'PORTAL: (none)\n'
770 if self.explorer in self.game.annotations:
771 info_to_cache += 'ANNOTATION: ' +\
772 self.game.annotations[self.explorer]
773 self.info_cached = info_to_cache
774 return self.info_cached
776 def get_thing_info(self, t):
778 (t.type_, self.game.thing_types[t.type_])
779 if hasattr(t, 'thing_char'):
781 if hasattr(t, 'name'):
782 info += ' (%s)' % t.name
783 if hasattr(t, 'installed'):
784 info += ' / installed'
787 def loop(self, stdscr):
790 def safe_addstr(y, x, line):
791 if y < self.size.y - 1 or x + len(line) < self.size.x:
792 stdscr.addstr(y, x, line, curses.color_pair(1))
793 else: # workaround to <https://stackoverflow.com/q/7063128>
794 cut_i = self.size.x - x - 1
796 last_char = line[cut_i]
797 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
798 stdscr.insstr(y, self.size.x - 2, ' ')
799 stdscr.addstr(y, x, cut, curses.color_pair(1))
801 def handle_input(msg):
802 command, args = self.parser.parse(msg)
805 def task_action_on(action):
806 return action_tasks[action] in self.game.tasks
808 def msg_into_lines_of_width(msg, width):
812 for i in range(len(msg)):
813 if x >= width or msg[i] == "\n":
825 def reset_screen_size():
826 self.size = YX(*stdscr.getmaxyx())
827 self.size = self.size - YX(self.size.y % 4, 0)
828 self.size = self.size - YX(0, self.size.x % 4)
829 self.window_width = int(self.size.x / 2)
831 def recalc_input_lines():
832 if not self.mode.has_input_prompt:
833 self.input_lines = []
835 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
838 def move_explorer(direction):
839 target = self.game.map_geometry.move_yx(self.explorer, direction)
841 self.info_cached = None
842 self.explorer = target
844 self.send_tile_control_command()
850 for line in self.log:
851 lines += msg_into_lines_of_width(line, self.window_width)
854 max_y = self.size.y - len(self.input_lines)
855 for i in range(len(lines)):
856 if (i >= max_y - height_header):
858 safe_addstr(max_y - i - 1, self.window_width, lines[i])
861 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
862 lines = msg_into_lines_of_width(info, self.window_width)
864 for i in range(len(lines)):
865 y = height_header + i
866 if y >= self.size.y - len(self.input_lines):
868 safe_addstr(y, self.window_width, lines[i])
871 y = self.size.y - len(self.input_lines)
872 for i in range(len(self.input_lines)):
873 safe_addstr(y, self.window_width, self.input_lines[i])
877 if not self.game.turn_complete:
879 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
882 help = "hit [%s] for help" % self.keys['help']
883 if self.mode.has_input_prompt:
884 help = "enter /help for help"
885 safe_addstr(1, self.window_width,
886 'MODE: %s – %s' % (self.mode.short_desc, help))
889 if not self.game.turn_complete and len(self.map_lines) == 0:
891 if self.game.turn_complete:
893 for y in range(self.game.map_geometry.size.y):
894 start = self.game.map_geometry.size.x * y
895 end = start + self.game.map_geometry.size.x
896 if self.map_mode == 'protections':
897 map_lines_split += [[c + ' ' for c
898 in self.game.map_control_content[start:end]]]
900 map_lines_split += [[c + ' ' for c
901 in self.game.map_content[start:end]]]
902 if self.map_mode == 'terrain + annotations':
903 for p in self.game.annotations:
904 map_lines_split[p.y][p.x] = 'A '
905 elif self.map_mode == 'terrain + things':
906 for p in self.game.portals.keys():
907 original = map_lines_split[p.y][p.x]
908 map_lines_split[p.y][p.x] = original[0] + 'P'
911 def draw_thing(t, used_positions):
912 symbol = self.game.thing_types[t.type_]
914 if hasattr(t, 'thing_char'):
915 meta_char = t.thing_char
916 if t.position in used_positions:
918 if hasattr(t, 'carrying') and t.carrying:
920 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
921 used_positions += [t.position]
923 for t in [t for t in self.game.things if t.type_ != 'Player']:
924 draw_thing(t, used_positions)
925 for t in [t for t in self.game.things if t.type_ == 'Player']:
926 draw_thing(t, used_positions)
927 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
928 map_lines_split[self.explorer.y][self.explorer.x] = '??'
929 elif self.map_mode != 'terrain + things':
930 map_lines_split[self.game.player.position.y]\
931 [self.game.player.position.x] = '??'
933 if type(self.game.map_geometry) == MapGeometryHex:
935 for line in map_lines_split:
936 self.map_lines += [indent * ' ' + ''.join(line)]
937 indent = 0 if indent else 1
939 for line in map_lines_split:
940 self.map_lines += [''.join(line)]
941 window_center = YX(int(self.size.y / 2),
942 int(self.window_width / 2))
943 center = self.game.player.position
944 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
945 center = self.explorer
946 center = YX(center.y, center.x * 2)
947 self.offset = center - window_center
948 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
949 self.offset += YX(0, 1)
950 term_y = max(0, -self.offset.y)
951 term_x = max(0, -self.offset.x)
952 map_y = max(0, self.offset.y)
953 map_x = max(0, self.offset.x)
954 while term_y < self.size.y and map_y < len(self.map_lines):
955 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
956 safe_addstr(term_y, term_x, to_draw)
961 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
962 self.mode.help_intro)
963 if len(self.mode.available_actions) > 0:
964 content += "Available actions:\n"
965 for action in self.mode.available_actions:
966 if action in action_tasks:
967 if action_tasks[action] not in self.game.tasks:
969 if action == 'move_explorer':
972 key = ','.join(self.movement_keys)
974 key = self.keys[action]
975 content += '[%s] – %s\n' % (key, action_descriptions[action])
977 content += self.mode.list_available_modes(self)
978 for i in range(self.size.y):
980 self.window_width * (not self.mode.has_input_prompt),
981 ' ' * self.window_width)
983 for line in content.split('\n'):
984 lines += msg_into_lines_of_width(line, self.window_width)
985 for i in range(len(lines)):
989 self.window_width * (not self.mode.has_input_prompt),
994 stdscr.bkgd(' ', curses.color_pair(1))
996 if self.mode.has_input_prompt:
998 if self.mode.shows_info:
1003 if not self.mode.is_intro:
1009 def pick_selectable(task_name):
1011 i = int(self.input_)
1012 if i < 0 or i >= len(self.selectables):
1013 self.log_msg('? invalid index, aborted')
1015 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1017 self.log_msg('? invalid index, aborted')
1019 self.switch_mode('play')
1021 action_descriptions = {
1023 'flatten': 'flatten surroundings',
1024 'teleport': 'teleport',
1025 'take_thing': 'pick up thing',
1026 'drop_thing': 'drop thing',
1027 'toggle_map_mode': 'toggle map view',
1028 'toggle_tile_draw': 'toggle protection character drawing',
1029 'install': '(un-)install',
1030 'wear': '(un-)wear',
1031 'door': 'open/close',
1032 'consume': 'consume',
1037 'flatten': 'FLATTEN_SURROUNDINGS',
1038 'take_thing': 'PICK_UP',
1039 'drop_thing': 'DROP',
1041 'install': 'INSTALL',
1044 'command': 'COMMAND',
1045 'consume': 'INTOXICATE',
1049 curses.curs_set(False) # hide cursor
1050 curses.start_color()
1051 self.set_default_colors()
1052 curses.init_pair(1, 1, 2)
1055 self.explorer = YX(0, 0)
1058 interval = datetime.timedelta(seconds=5)
1059 last_ping = datetime.datetime.now() - interval
1061 if self.disconnected and self.force_instant_connect:
1062 self.force_instant_connect = False
1064 now = datetime.datetime.now()
1065 if now - last_ping > interval:
1066 if self.disconnected:
1076 self.do_refresh = False
1079 msg = self.queue.get(block=False)
1084 key = stdscr.getkey()
1085 self.do_refresh = True
1086 except curses.error:
1091 self.show_help = False
1092 if key == 'KEY_RESIZE':
1094 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1095 self.input_ = self.input_[:-1]
1096 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1097 or (self.mode.has_input_prompt and key == '\n'
1098 and self.input_ == ''\
1099 and self.mode.name in {'chat', 'command_thing',
1100 'take_thing', 'drop_thing',
1102 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1103 self.log_msg('@ aborted')
1104 self.switch_mode('play')
1105 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1106 self.show_help = True
1108 self.restore_input_values()
1109 elif self.mode.has_input_prompt and key != '\n': # Return key
1111 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1112 if len(self.input_) > max_length:
1113 self.input_ = self.input_[:max_length]
1114 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1115 self.show_help = True
1116 elif self.mode.name == 'login' and key == '\n':
1117 self.login_name = self.input_
1118 self.send('LOGIN ' + quote(self.input_))
1120 elif self.mode.name == 'enter_face' and key == '\n':
1121 if len(self.input_) != 18:
1122 self.log_msg('? wrong input length, aborting')
1124 self.send('PLAYER_FACE %s' % quote(self.input_))
1126 self.switch_mode('edit')
1127 elif self.mode.name == 'take_thing' and key == '\n':
1128 pick_selectable('PICK_UP')
1129 elif self.mode.name == 'drop_thing' and key == '\n':
1130 pick_selectable('DROP')
1131 elif self.mode.name == 'command_thing' and key == '\n':
1132 self.send('TASK:COMMAND ' + quote(self.input_))
1134 elif self.mode.name == 'control_pw_pw' and key == '\n':
1135 if self.input_ == '':
1136 self.log_msg('@ aborted')
1138 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1139 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1140 self.switch_mode('admin')
1141 elif self.mode.name == 'password' and key == '\n':
1142 if self.input_ == '':
1144 self.password = self.input_
1145 self.switch_mode('edit')
1146 elif self.mode.name == 'admin_enter' and key == '\n':
1147 self.send('BECOME_ADMIN ' + quote(self.input_))
1148 self.switch_mode('play')
1149 elif self.mode.name == 'control_pw_type' and key == '\n':
1150 if len(self.input_) != 1:
1151 self.log_msg('@ entered non-single-char, therefore aborted')
1152 self.switch_mode('admin')
1154 self.tile_control_char = self.input_
1155 self.switch_mode('control_pw_pw')
1156 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1157 if len(self.input_) != 1:
1158 self.log_msg('@ entered non-single-char, therefore aborted')
1160 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1161 quote(self.input_)))
1162 self.log_msg('@ sent new protection character for thing')
1163 self.switch_mode('admin')
1164 elif self.mode.name == 'control_tile_type' and key == '\n':
1165 if len(self.input_) != 1:
1166 self.log_msg('@ entered non-single-char, therefore aborted')
1167 self.switch_mode('admin')
1169 self.tile_control_char = self.input_
1170 self.switch_mode('control_tile_draw')
1171 elif self.mode.name == 'chat' and key == '\n':
1172 if self.input_ == '':
1174 if self.input_[0] == '/':
1175 if self.input_.startswith('/nick'):
1176 tokens = self.input_.split(maxsplit=1)
1177 if len(tokens) == 2:
1178 self.send('NICK ' + quote(tokens[1]))
1180 self.log_msg('? need login name')
1182 self.log_msg('? unknown command')
1184 self.send('ALL ' + quote(self.input_))
1186 elif self.mode.name == 'name_thing' and key == '\n':
1187 if self.input_ == '':
1189 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1191 quote(self.password)))
1192 self.switch_mode('edit')
1193 elif self.mode.name == 'annotate' and key == '\n':
1194 if self.input_ == '':
1196 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1197 quote(self.password)))
1198 self.switch_mode('edit')
1199 elif self.mode.name == 'portal' and key == '\n':
1200 if self.input_ == '':
1202 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1203 quote(self.password)))
1204 self.switch_mode('edit')
1205 elif self.mode.name == 'study':
1206 if self.mode.mode_switch_on_key(self, key):
1208 elif key == self.keys['toggle_map_mode']:
1209 self.toggle_map_mode()
1210 elif key in self.movement_keys:
1211 move_explorer(self.movement_keys[key])
1212 elif self.mode.name == 'play':
1213 if self.mode.mode_switch_on_key(self, key):
1215 elif key == self.keys['door'] and task_action_on('door'):
1216 self.send('TASK:DOOR')
1217 elif key == self.keys['consume'] and task_action_on('consume'):
1218 self.send('TASK:INTOXICATE')
1219 elif key == self.keys['wear'] and task_action_on('wear'):
1220 self.send('TASK:WEAR')
1221 elif key == self.keys['spin'] and task_action_on('spin'):
1222 self.send('TASK:SPIN')
1223 elif key == self.keys['teleport']:
1224 if self.game.player.position in self.game.portals:
1225 self.host = self.game.portals[self.game.player.position]
1229 self.log_msg('? not standing on portal')
1230 elif key in self.movement_keys and task_action_on('move'):
1231 self.send('TASK:MOVE ' + self.movement_keys[key])
1232 elif self.mode.name == 'write':
1233 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1234 self.switch_mode('edit')
1235 elif self.mode.name == 'control_tile_draw':
1236 if self.mode.mode_switch_on_key(self, key):
1238 elif key in self.movement_keys:
1239 move_explorer(self.movement_keys[key])
1240 elif key == self.keys['toggle_tile_draw']:
1241 self.tile_draw = False if self.tile_draw else True
1242 elif self.mode.name == 'admin':
1243 if self.mode.mode_switch_on_key(self, key):
1245 elif key in self.movement_keys and task_action_on('move'):
1246 self.send('TASK:MOVE ' + self.movement_keys[key])
1247 elif self.mode.name == 'edit':
1248 if self.mode.mode_switch_on_key(self, key):
1250 elif key == self.keys['flatten'] and task_action_on('flatten'):
1251 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1252 elif key == self.keys['install'] and task_action_on('install'):
1253 self.send('TASK:INSTALL %s' % quote(self.password))
1254 elif key == self.keys['toggle_map_mode']:
1255 self.toggle_map_mode()
1256 elif key in self.movement_keys and task_action_on('move'):
1257 self.send('TASK:MOVE ' + self.movement_keys[key])
1259 if len(sys.argv) != 2:
1260 raise ArgError('wrong number of arguments, need game host')