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):
173 game.turn_complete = False
174 cmd_TURN.argtypes = 'int:nonneg'
176 def cmd_LOGIN_OK(game):
177 game.tui.switch_mode('post_login_wait')
178 game.tui.send('GET_GAMESTATE')
179 game.tui.log_msg('@ welcome')
180 cmd_LOGIN_OK.argtypes = ''
182 def cmd_ADMIN_OK(game):
183 game.tui.is_admin = True
184 game.tui.log_msg('@ you now have admin rights')
185 game.tui.switch_mode('admin')
186 game.tui.do_refresh = True
187 cmd_ADMIN_OK.argtypes = ''
189 def cmd_REPLY(game, msg):
190 game.tui.log_msg('#MUSICPLAYER: ' + msg)
191 game.tui.do_refresh = True
192 cmd_REPLY.argtypes = 'string'
194 def cmd_CHAT(game, msg):
195 game.tui.log_msg('# ' + msg)
196 game.tui.do_refresh = True
197 cmd_CHAT.argtypes = 'string'
199 def cmd_CHATFACE(game, thing_id):
200 game.tui.draw_face = thing_id
201 game.tui.do_refresh = True
202 cmd_CHATFACE.argtypes = 'int:pos'
204 def cmd_PLAYER_ID(game, player_id):
205 game.player_id = player_id
206 cmd_PLAYER_ID.argtypes = 'int:nonneg'
208 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
209 t = game.get_thing_temp(thing_id)
211 t = ThingBase(game, thing_id)
212 game.things_new += [t]
215 t.protection = protection
216 t.portable = portable
217 t.commandable = commandable
218 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
220 def cmd_THING_NAME(game, thing_id, name):
221 t = game.get_thing_temp(thing_id)
223 cmd_THING_NAME.argtypes = 'int:pos string'
225 def cmd_THING_FACE(game, thing_id, face):
226 t = game.get_thing_temp(thing_id)
228 cmd_THING_FACE.argtypes = 'int:pos string'
230 def cmd_THING_HAT(game, thing_id, hat):
231 t = game.get_thing_temp(thing_id)
233 cmd_THING_HAT.argtypes = 'int:pos string'
235 def cmd_THING_CHAR(game, thing_id, c):
236 t = game.get_thing_temp(thing_id)
238 cmd_THING_CHAR.argtypes = 'int:pos char'
240 def cmd_MAP(game, geometry, size, content):
241 map_geometry_class = globals()['MapGeometry' + geometry]
242 game.map_geometry_new = map_geometry_class(size)
243 game.map_content_new = content
244 if type(game.map_geometry) == MapGeometrySquare:
245 game.tui.movement_keys = {
246 game.tui.keys['square_move_up']: 'UP',
247 game.tui.keys['square_move_left']: 'LEFT',
248 game.tui.keys['square_move_down']: 'DOWN',
249 game.tui.keys['square_move_right']: 'RIGHT',
251 elif type(game.map_geometry) == MapGeometryHex:
252 game.tui.movement_keys = {
253 game.tui.keys['hex_move_upleft']: 'UPLEFT',
254 game.tui.keys['hex_move_upright']: 'UPRIGHT',
255 game.tui.keys['hex_move_right']: 'RIGHT',
256 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
257 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
258 game.tui.keys['hex_move_left']: 'LEFT',
260 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
262 def cmd_FOV(game, content):
263 game.fov_new = content
264 cmd_FOV.argtypes = 'string'
266 def cmd_MAP_CONTROL(game, content):
267 game.map_control_content_new = content
268 cmd_MAP_CONTROL.argtypes = 'string'
270 def cmd_GAME_STATE_COMPLETE(game):
271 game.turn_complete = True
272 game.tui.do_refresh = True
273 game.tui.info_cached = None
274 game.things = game.things_new
276 game.portals = game.portals_new
277 game.portals_new = {}
278 game.annotations = game.annotations_new
279 game.annotations_new = {}
280 game.fov = game.fov_new
281 game.map_geometry = game.map_geometry_new
282 game.map_content = game.map_content_new
283 game.map_control_content = game.map_control_content_new
284 game.player = game.get_thing(game.player_id)
285 if game.tui.mode.name == 'post_login_wait':
286 game.tui.switch_mode('play')
287 cmd_GAME_STATE_COMPLETE.argtypes = ''
289 def cmd_PORTAL(game, position, msg):
290 game.portals_new[position] = msg
291 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
293 def cmd_PLAY_ERROR(game, msg):
294 game.tui.log_msg('? ' + msg)
295 game.tui.flash = True
296 game.tui.do_refresh = True
297 cmd_PLAY_ERROR.argtypes = 'string'
299 def cmd_GAME_ERROR(game, msg):
300 game.tui.log_msg('? game error: ' + msg)
301 game.tui.do_refresh = True
302 cmd_GAME_ERROR.argtypes = 'string'
304 def cmd_ARGUMENT_ERROR(game, msg):
305 game.tui.log_msg('? syntax error: ' + msg)
306 game.tui.do_refresh = True
307 cmd_ARGUMENT_ERROR.argtypes = 'string'
309 def cmd_ANNOTATION(game, position, msg):
310 game.annotations_new[position] = msg
311 if game.tui.mode.shows_info:
312 game.tui.do_refresh = True
313 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
315 def cmd_TASKS(game, tasks_comma_separated):
316 game.tasks = tasks_comma_separated.split(',')
317 game.tui.mode_write.legal = 'WRITE' in game.tasks
318 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
319 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
320 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
321 cmd_TASKS.argtypes = 'string'
323 def cmd_THING_TYPE(game, thing_type, symbol_hint):
324 game.thing_types[thing_type] = symbol_hint
325 cmd_THING_TYPE.argtypes = 'string char'
327 def cmd_THING_INSTALLED(game, thing_id):
328 game.get_thing_temp(thing_id).installed = True
329 cmd_THING_INSTALLED.argtypes = 'int:pos'
331 def cmd_THING_CARRYING(game, thing_id, carried_id):
332 game.get_thing_temp(thing_id).carrying = game.get_thing(carried_id)
333 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
335 def cmd_TERRAIN(game, terrain_char, terrain_desc):
336 game.terrains[terrain_char] = terrain_desc
337 cmd_TERRAIN.argtypes = 'char string'
341 cmd_PONG.argtypes = ''
343 def cmd_DEFAULT_COLORS(game):
344 game.tui.set_default_colors()
345 cmd_DEFAULT_COLORS.argtypes = ''
347 def cmd_RANDOM_COLORS(game):
348 game.tui.set_random_colors()
349 cmd_RANDOM_COLORS.argtypes = ''
351 class Game(GameBase):
352 turn_complete = False
357 def __init__(self, *args, **kwargs):
358 super().__init__(*args, **kwargs)
359 self.register_command(cmd_LOGIN_OK)
360 self.register_command(cmd_ADMIN_OK)
361 self.register_command(cmd_PONG)
362 self.register_command(cmd_CHAT)
363 self.register_command(cmd_CHATFACE)
364 self.register_command(cmd_REPLY)
365 self.register_command(cmd_PLAYER_ID)
366 self.register_command(cmd_TURN)
367 self.register_command(cmd_THING)
368 self.register_command(cmd_THING_TYPE)
369 self.register_command(cmd_THING_NAME)
370 self.register_command(cmd_THING_CHAR)
371 self.register_command(cmd_THING_FACE)
372 self.register_command(cmd_THING_HAT)
373 self.register_command(cmd_THING_CARRYING)
374 self.register_command(cmd_THING_INSTALLED)
375 self.register_command(cmd_TERRAIN)
376 self.register_command(cmd_MAP)
377 self.register_command(cmd_MAP_CONTROL)
378 self.register_command(cmd_PORTAL)
379 self.register_command(cmd_ANNOTATION)
380 self.register_command(cmd_GAME_STATE_COMPLETE)
381 self.register_command(cmd_ARGUMENT_ERROR)
382 self.register_command(cmd_GAME_ERROR)
383 self.register_command(cmd_PLAY_ERROR)
384 self.register_command(cmd_TASKS)
385 self.register_command(cmd_FOV)
386 self.register_command(cmd_DEFAULT_COLORS)
387 self.register_command(cmd_RANDOM_COLORS)
388 self.map_content = ''
390 self.annotations = {}
391 self.annotations_new = {}
393 self.portals_new = {}
397 def get_string_options(self, string_option_type):
398 if string_option_type == 'map_geometry':
399 return ['Hex', 'Square']
400 elif string_option_type == 'thing_type':
401 return self.thing_types.keys()
404 def get_command(self, command_name):
405 from functools import partial
406 f = partial(self.commands[command_name], self)
407 f.argtypes = self.commands[command_name].argtypes
410 def get_thing_temp(self, id_):
411 for thing in self.things_new:
418 def __init__(self, name, has_input_prompt=False, shows_info=False,
419 is_intro=False, is_single_char_entry=False):
421 self.short_desc = mode_helps[name]['short']
422 self.available_modes = []
423 self.available_actions = []
424 self.has_input_prompt = has_input_prompt
425 self.shows_info = shows_info
426 self.is_intro = is_intro
427 self.help_intro = mode_helps[name]['long']
428 self.intro_msg = mode_helps[name]['intro']
429 self.is_single_char_entry = is_single_char_entry
432 def iter_available_modes(self, tui):
433 for mode_name in self.available_modes:
434 mode = getattr(tui, 'mode_' + mode_name)
437 key = tui.keys['switch_to_' + mode.name]
440 def list_available_modes(self, tui):
442 if len(self.available_modes) > 0:
443 msg = 'Other modes available from here:\n'
444 for mode, key in self.iter_available_modes(tui):
445 msg += '[%s] – %s\n' % (key, mode.short_desc)
448 def mode_switch_on_key(self, tui, key_pressed):
449 for mode, key in self.iter_available_modes(tui):
450 if key_pressed == key:
451 tui.switch_mode(mode.name)
456 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
457 mode_admin = Mode('admin')
458 mode_play = Mode('play')
459 mode_study = Mode('study', shows_info=True)
460 mode_write = Mode('write', is_single_char_entry=True)
461 mode_edit = Mode('edit')
462 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
463 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
464 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
465 mode_control_tile_draw = Mode('control_tile_draw')
466 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
467 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
468 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
469 mode_chat = Mode('chat', has_input_prompt=True)
470 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
471 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
472 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
473 mode_password = Mode('password', has_input_prompt=True)
474 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
475 mode_command_thing = Mode('command_thing', has_input_prompt=True)
476 mode_take_thing = Mode('take_thing', has_input_prompt=True)
477 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
478 mode_enter_face = Mode('enter_face', has_input_prompt=True)
482 def __init__(self, host):
485 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
486 "command_thing", "take_thing",
488 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
489 "install", "wear", "spin"]
490 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
491 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
492 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
493 "control_tile_type", "chat",
494 "study", "play", "edit"]
495 self.mode_admin.available_actions = ["move"]
496 self.mode_control_tile_draw.available_modes = ["admin_enter"]
497 self.mode_control_tile_draw.available_actions = ["move_explorer",
499 self.mode_edit.available_modes = ["write", "annotate", "portal",
500 "name_thing", "enter_face", "password",
501 "chat", "study", "play", "admin_enter"]
502 self.mode_edit.available_actions = ["move", "flatten", "install",
508 self.parser = Parser(self.game)
510 self.do_refresh = True
511 self.queue = queue.Queue()
512 self.login_name = None
513 self.map_mode = 'terrain + things'
514 self.password = 'foo'
515 self.switch_mode('waiting_for_server')
517 'switch_to_chat': 't',
518 'switch_to_play': 'p',
519 'switch_to_password': 'P',
520 'switch_to_annotate': 'M',
521 'switch_to_portal': 'T',
522 'switch_to_study': '?',
523 'switch_to_edit': 'E',
524 'switch_to_write': 'm',
525 'switch_to_name_thing': 'N',
526 'switch_to_command_thing': 'O',
527 'switch_to_admin_enter': 'A',
528 'switch_to_control_pw_type': 'C',
529 'switch_to_control_tile_type': 'Q',
530 'switch_to_admin_thing_protect': 'T',
532 'switch_to_enter_face': 'f',
533 'switch_to_take_thing': 'z',
534 'switch_to_drop_thing': 'u',
542 'toggle_map_mode': 'L',
543 'toggle_tile_draw': 'm',
544 'hex_move_upleft': 'w',
545 'hex_move_upright': 'e',
546 'hex_move_right': 'd',
547 'hex_move_downright': 'x',
548 'hex_move_downleft': 'y',
549 'hex_move_left': 'a',
550 'square_move_up': 'w',
551 'square_move_left': 'a',
552 'square_move_down': 's',
553 'square_move_right': 'd',
555 if os.path.isfile('config.json'):
556 with open('config.json', 'r') as f:
557 keys_conf = json.loads(f.read())
559 self.keys[k] = keys_conf[k]
560 self.show_help = False
561 self.disconnected = True
562 self.force_instant_connect = True
563 self.input_lines = []
567 self.offset = YX(0,0)
568 curses.wrapper(self.loop)
572 def handle_recv(msg):
578 self.log_msg('@ attempting connect')
579 socket_client_class = PlomSocketClient
580 if self.host.startswith('ws://') or self.host.startswith('wss://'):
581 socket_client_class = WebSocketClient
583 self.socket = socket_client_class(handle_recv, self.host)
584 self.socket_thread = threading.Thread(target=self.socket.run)
585 self.socket_thread.start()
586 self.disconnected = False
587 self.game.thing_types = {}
588 self.game.terrains = {}
589 time.sleep(0.1) # give potential SSL negotation some time …
590 self.socket.send('TASKS')
591 self.socket.send('TERRAINS')
592 self.socket.send('THING_TYPES')
593 self.switch_mode('login')
594 except ConnectionRefusedError:
595 self.log_msg('@ server connect failure')
596 self.disconnected = True
597 self.switch_mode('waiting_for_server')
598 self.do_refresh = True
601 self.log_msg('@ attempting reconnect')
603 # necessitated by some strange SSL race conditions with ws4py
604 time.sleep(0.1) # FIXME find out why exactly necessary
605 self.switch_mode('waiting_for_server')
610 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
611 raise BrokenSocketConnection
612 self.socket.send(msg)
613 except (BrokenPipeError, BrokenSocketConnection):
614 self.log_msg('@ server disconnected :(')
615 self.disconnected = True
616 self.force_instant_connect = True
617 self.do_refresh = True
619 def log_msg(self, msg):
621 if len(self.log) > 100:
622 self.log = self.log[-100:]
624 def restore_input_values(self):
625 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
626 self.input_ = self.game.annotations[self.explorer]
627 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
628 self.input_ = self.game.portals[self.explorer]
629 elif self.mode.name == 'password':
630 self.input_ = self.password
631 elif self.mode.name == 'name_thing':
632 if hasattr(self.thing_selected, 'name'):
633 self.input_ = self.thing_selected.name
634 elif self.mode.name == 'admin_thing_protect':
635 if hasattr(self.thing_selected, 'protection'):
636 self.input_ = self.thing_selected.protection
638 def send_tile_control_command(self):
639 self.send('SET_TILE_CONTROL %s %s' %
640 (self.explorer, quote(self.tile_control_char)))
642 def toggle_map_mode(self):
643 if self.map_mode == 'terrain only':
644 self.map_mode = 'terrain + annotations'
645 elif self.map_mode == 'terrain + annotations':
646 self.map_mode = 'terrain + things'
647 elif self.map_mode == 'terrain + things':
648 self.map_mode = 'protections'
649 elif self.map_mode == 'protections':
650 self.map_mode = 'terrain only'
652 def switch_mode(self, mode_name):
654 def fail(msg, return_mode='play'):
655 self.log_msg('? ' + msg)
657 self.switch_mode(return_mode)
659 if self.mode and self.mode.name == 'control_tile_draw':
660 self.log_msg('@ finished tile protection drawing.')
661 self.draw_face = False
662 self.tile_draw = False
663 if mode_name == 'command_thing' and\
664 (not self.game.player.carrying or
665 not self.game.player.carrying.commandable):
666 return fail('not carrying anything commandable')
667 if mode_name == 'take_thing' and self.game.player.carrying:
668 return fail('already carrying something')
669 if mode_name == 'drop_thing' and not self.game.player.carrying:
670 return fail('not carrying anything droppable')
671 if mode_name == 'admin_enter' and self.is_admin:
673 elif mode_name in {'name_thing', 'admin_thing_protect'}:
675 for t in [t for t in self.game.things
676 if t.position == self.game.player.position
677 and t.id_ != self.game.player.id_]:
681 return fail('not standing over thing', 'edit')
683 self.thing_selected = thing
684 self.mode = getattr(self, 'mode_' + mode_name)
685 if self.mode.name in {'control_tile_draw', 'control_tile_type',
687 self.map_mode = 'protections'
688 elif self.mode.name != 'edit':
689 self.map_mode = 'terrain + things'
690 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
691 self.explorer = YX(self.game.player.position.y,
692 self.game.player.position.x)
693 if self.mode.is_single_char_entry:
694 self.show_help = True
695 if len(self.mode.intro_msg) > 0:
696 self.log_msg(self.mode.intro_msg)
697 if self.mode.name == 'login':
699 self.send('LOGIN ' + quote(self.login_name))
701 self.log_msg('@ enter username')
702 elif self.mode.name == 'take_thing':
703 self.log_msg('Portable things in reach for pick-up:')
704 select_range = [self.game.player.position,
705 self.game.player.position + YX(0,-1),
706 self.game.player.position + YX(0, 1),
707 self.game.player.position + YX(-1, 0),
708 self.game.player.position + YX(1, 0)]
709 if type(self.game.map_geometry) == MapGeometryHex:
710 if self.game.player.position.y % 2:
711 select_range += [self.game.player.position + YX(-1, 1),
712 self.game.player.position + YX(1, 1)]
714 select_range += [self.game.player.position + YX(-1, -1),
715 self.game.player.position + YX(1, -1)]
716 self.selectables = [t.id_ for t in self.game.things
717 if t.portable and t.position in select_range]
718 if len(self.selectables) == 0:
719 return fail('nothing to pick-up')
721 for i in range(len(self.selectables)):
722 t = self.game.get_thing(self.selectables[i])
723 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
724 elif self.mode.name == 'drop_thing':
725 self.log_msg('Direction to drop thing to:')
727 ['HERE'] + list(self.game.tui.movement_keys.values())
728 for i in range(len(self.selectables)):
729 self.log_msg(str(i) + ': ' + self.selectables[i])
730 elif self.mode.name == 'command_thing':
731 self.send('TASK:COMMAND ' + quote('HELP'))
732 elif self.mode.name == 'control_pw_pw':
733 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
734 elif self.mode.name == 'control_tile_draw':
735 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']))
737 self.restore_input_values()
739 def set_default_colors(self):
740 curses.init_color(1, 1000, 1000, 1000)
741 curses.init_color(2, 0, 0, 0)
742 self.do_refresh = True
744 def set_random_colors(self):
748 return int(offset + random.random()*375)
750 curses.init_color(1, rand(625), rand(625), rand(625))
751 curses.init_color(2, rand(0), rand(0), rand(0))
752 self.do_refresh = True
756 return self.info_cached
757 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
759 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
760 info_to_cache += 'outside field of view'
762 for t in self.game.things:
763 if t.position == self.explorer:
764 info_to_cache += 'THING: %s' % self.get_thing_info(t)
765 protection = t.protection
766 if protection == '.':
768 info_to_cache += ' / protection: %s\n' % protection
769 if hasattr(t, 'hat'):
770 info_to_cache += t.hat[0:6] + '\n'
771 info_to_cache += t.hat[6:12] + '\n'
772 info_to_cache += t.hat[12:18] + '\n'
773 if hasattr(t, 'face'):
774 info_to_cache += t.face[0:6] + '\n'
775 info_to_cache += t.face[6:12] + '\n'
776 info_to_cache += t.face[12:18] + '\n'
777 terrain_char = self.game.map_content[pos_i]
779 if terrain_char in self.game.terrains:
780 terrain_desc = self.game.terrains[terrain_char]
781 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
783 protection = self.game.map_control_content[pos_i]
784 if protection == '.':
785 protection = 'unprotected'
786 info_to_cache += 'PROTECTION: %s\n' % protection
787 if self.explorer in self.game.portals:
788 info_to_cache += 'PORTAL: ' +\
789 self.game.portals[self.explorer] + '\n'
791 info_to_cache += 'PORTAL: (none)\n'
792 if self.explorer in self.game.annotations:
793 info_to_cache += 'ANNOTATION: ' +\
794 self.game.annotations[self.explorer]
795 self.info_cached = info_to_cache
796 return self.info_cached
798 def get_thing_info(self, t):
800 (t.type_, self.game.thing_types[t.type_])
801 if hasattr(t, 'thing_char'):
803 if hasattr(t, 'name'):
804 info += ' (%s)' % t.name
805 if hasattr(t, 'installed'):
806 info += ' / installed'
809 def loop(self, stdscr):
812 def safe_addstr(y, x, line):
813 if y < self.size.y - 1 or x + len(line) < self.size.x:
814 stdscr.addstr(y, x, line, curses.color_pair(1))
815 else: # workaround to <https://stackoverflow.com/q/7063128>
816 cut_i = self.size.x - x - 1
818 last_char = line[cut_i]
819 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
820 stdscr.insstr(y, self.size.x - 2, ' ')
821 stdscr.addstr(y, x, cut, curses.color_pair(1))
823 def handle_input(msg):
824 command, args = self.parser.parse(msg)
827 def task_action_on(action):
828 return action_tasks[action] in self.game.tasks
830 def msg_into_lines_of_width(msg, width):
834 for i in range(len(msg)):
835 if x >= width or msg[i] == "\n":
847 def reset_screen_size():
848 self.size = YX(*stdscr.getmaxyx())
849 self.size = self.size - YX(self.size.y % 4, 0)
850 self.size = self.size - YX(0, self.size.x % 4)
851 self.window_width = int(self.size.x / 2)
853 def recalc_input_lines():
854 if not self.mode.has_input_prompt:
855 self.input_lines = []
857 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
860 def move_explorer(direction):
861 target = self.game.map_geometry.move_yx(self.explorer, direction)
863 self.info_cached = None
864 self.explorer = target
866 self.send_tile_control_command()
872 for line in self.log:
873 lines += msg_into_lines_of_width(line, self.window_width)
876 max_y = self.size.y - len(self.input_lines)
877 for i in range(len(lines)):
878 if (i >= max_y - height_header):
880 safe_addstr(max_y - i - 1, self.window_width, lines[i])
883 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
884 lines = msg_into_lines_of_width(info, self.window_width)
886 for i in range(len(lines)):
887 y = height_header + i
888 if y >= self.size.y - len(self.input_lines):
890 safe_addstr(y, self.window_width, lines[i])
893 y = self.size.y - len(self.input_lines)
894 for i in range(len(self.input_lines)):
895 safe_addstr(y, self.window_width, self.input_lines[i])
899 if not self.game.turn_complete:
901 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
904 help = "hit [%s] for help" % self.keys['help']
905 if self.mode.has_input_prompt:
906 help = "enter /help for help"
907 safe_addstr(1, self.window_width,
908 'MODE: %s – %s' % (self.mode.short_desc, help))
911 if (not self.game.turn_complete) and len(self.map_lines) == 0:
913 if self.game.turn_complete:
915 for y in range(self.game.map_geometry.size.y):
916 start = self.game.map_geometry.size.x * y
917 end = start + self.game.map_geometry.size.x
918 if self.map_mode == 'protections':
919 map_lines_split += [[c + ' ' for c
920 in self.game.map_control_content[start:end]]]
922 map_lines_split += [[c + ' ' for c
923 in self.game.map_content[start:end]]]
924 if self.map_mode == 'terrain + annotations':
925 for p in self.game.annotations:
926 map_lines_split[p.y][p.x] = 'A '
927 elif self.map_mode == 'terrain + things':
928 for p in self.game.portals.keys():
929 original = map_lines_split[p.y][p.x]
930 map_lines_split[p.y][p.x] = original[0] + 'P'
933 def draw_thing(t, used_positions):
934 symbol = self.game.thing_types[t.type_]
936 if hasattr(t, 'thing_char'):
937 meta_char = t.thing_char
938 if t.position in used_positions:
940 if hasattr(t, 'carrying') and t.carrying:
942 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
943 used_positions += [t.position]
945 for t in [t for t in self.game.things if t.type_ != 'Player']:
946 draw_thing(t, used_positions)
947 for t in [t for t in self.game.things if t.type_ == 'Player']:
948 draw_thing(t, used_positions)
949 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
950 map_lines_split[self.explorer.y][self.explorer.x] = '??'
951 elif self.map_mode != 'terrain + things':
952 map_lines_split[self.game.player.position.y]\
953 [self.game.player.position.x] = '??'
955 if type(self.game.map_geometry) == MapGeometryHex:
957 for line in map_lines_split:
958 self.map_lines += [indent * ' ' + ''.join(line)]
959 indent = 0 if indent else 1
961 for line in map_lines_split:
962 self.map_lines += [''.join(line)]
963 window_center = YX(int(self.size.y / 2),
964 int(self.window_width / 2))
965 center = self.game.player.position
966 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
967 center = self.explorer
968 center = YX(center.y, center.x * 2)
969 self.offset = center - window_center
970 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
971 self.offset += YX(0, 1)
972 term_y = max(0, -self.offset.y)
973 term_x = max(0, -self.offset.x)
974 map_y = max(0, self.offset.y)
975 map_x = max(0, self.offset.x)
976 while term_y < self.size.y and map_y < len(self.map_lines):
977 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
978 safe_addstr(term_y, term_x, to_draw)
982 def draw_face_popup():
983 t = self.game.get_thing(self.draw_face)
984 if not t or not hasattr(t, 'face'):
985 self.draw_face = False
988 start_x = self.window_width - 10
990 if hasattr(t, 'thing_char'):
991 t_char = t.thing_char
992 def draw_body_part(body_part, end_y):
993 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
994 safe_addstr(end_y - 3, start_x, '| |')
995 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
996 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
997 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
999 if hasattr(t, 'face'):
1000 draw_body_part(t.face, self.size.y - 2)
1001 if hasattr(t, 'hat'):
1002 draw_body_part(t.hat, self.size.y - 5)
1003 safe_addstr(self.size.y - 1, start_x, '| |')
1006 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1007 self.mode.help_intro)
1008 if len(self.mode.available_actions) > 0:
1009 content += "Available actions:\n"
1010 for action in self.mode.available_actions:
1011 if action in action_tasks:
1012 if action_tasks[action] not in self.game.tasks:
1014 if action == 'move_explorer':
1016 if action == 'move':
1017 key = ','.join(self.movement_keys)
1019 key = self.keys[action]
1020 content += '[%s] – %s\n' % (key, action_descriptions[action])
1022 content += self.mode.list_available_modes(self)
1023 for i in range(self.size.y):
1025 self.window_width * (not self.mode.has_input_prompt),
1026 ' ' * self.window_width)
1028 for line in content.split('\n'):
1029 lines += msg_into_lines_of_width(line, self.window_width)
1030 for i in range(len(lines)):
1031 if i >= self.size.y:
1034 self.window_width * (not self.mode.has_input_prompt),
1039 stdscr.bkgd(' ', curses.color_pair(1))
1040 recalc_input_lines()
1041 if self.mode.has_input_prompt:
1043 if self.mode.shows_info:
1048 if not self.mode.is_intro:
1053 if self.draw_face and self.mode.name in {'chat', 'play'}:
1056 def pick_selectable(task_name):
1058 i = int(self.input_)
1059 if i < 0 or i >= len(self.selectables):
1060 self.log_msg('? invalid index, aborted')
1062 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1064 self.log_msg('? invalid index, aborted')
1066 self.switch_mode('play')
1068 action_descriptions = {
1070 'flatten': 'flatten surroundings',
1071 'teleport': 'teleport',
1072 'take_thing': 'pick up thing',
1073 'drop_thing': 'drop thing',
1074 'toggle_map_mode': 'toggle map view',
1075 'toggle_tile_draw': 'toggle protection character drawing',
1076 'install': '(un-)install',
1077 'wear': '(un-)wear',
1078 'door': 'open/close',
1079 'consume': 'consume',
1084 'flatten': 'FLATTEN_SURROUNDINGS',
1085 'take_thing': 'PICK_UP',
1086 'drop_thing': 'DROP',
1088 'install': 'INSTALL',
1091 'command': 'COMMAND',
1092 'consume': 'INTOXICATE',
1096 curses.curs_set(False) # hide cursor
1097 curses.start_color()
1098 self.set_default_colors()
1099 curses.init_pair(1, 1, 2)
1102 self.explorer = YX(0, 0)
1105 interval = datetime.timedelta(seconds=5)
1106 last_ping = datetime.datetime.now() - interval
1108 if self.disconnected and self.force_instant_connect:
1109 self.force_instant_connect = False
1111 now = datetime.datetime.now()
1112 if now - last_ping > interval:
1113 if self.disconnected:
1123 self.do_refresh = False
1126 msg = self.queue.get(block=False)
1131 key = stdscr.getkey()
1132 self.do_refresh = True
1133 except curses.error:
1138 self.show_help = False
1139 self.draw_face = False
1140 if key == 'KEY_RESIZE':
1142 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1143 self.input_ = self.input_[:-1]
1144 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1145 or (self.mode.has_input_prompt and key == '\n'
1146 and self.input_ == ''\
1147 and self.mode.name in {'chat', 'command_thing',
1148 'take_thing', 'drop_thing',
1150 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1151 self.log_msg('@ aborted')
1152 self.switch_mode('play')
1153 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1154 self.show_help = True
1156 self.restore_input_values()
1157 elif self.mode.has_input_prompt and key != '\n': # Return key
1159 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1160 if len(self.input_) > max_length:
1161 self.input_ = self.input_[:max_length]
1162 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1163 self.show_help = True
1164 elif self.mode.name == 'login' and key == '\n':
1165 self.login_name = self.input_
1166 self.send('LOGIN ' + quote(self.input_))
1168 elif self.mode.name == 'enter_face' and key == '\n':
1169 if len(self.input_) != 18:
1170 self.log_msg('? wrong input length, aborting')
1172 self.send('PLAYER_FACE %s' % quote(self.input_))
1174 self.switch_mode('edit')
1175 elif self.mode.name == 'take_thing' and key == '\n':
1176 pick_selectable('PICK_UP')
1177 elif self.mode.name == 'drop_thing' and key == '\n':
1178 pick_selectable('DROP')
1179 elif self.mode.name == 'command_thing' and key == '\n':
1180 self.send('TASK:COMMAND ' + quote(self.input_))
1182 elif self.mode.name == 'control_pw_pw' and key == '\n':
1183 if self.input_ == '':
1184 self.log_msg('@ aborted')
1186 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1187 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1188 self.switch_mode('admin')
1189 elif self.mode.name == 'password' and key == '\n':
1190 if self.input_ == '':
1192 self.password = self.input_
1193 self.switch_mode('edit')
1194 elif self.mode.name == 'admin_enter' and key == '\n':
1195 self.send('BECOME_ADMIN ' + quote(self.input_))
1196 self.switch_mode('play')
1197 elif self.mode.name == 'control_pw_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_pw_pw')
1204 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1205 if len(self.input_) != 1:
1206 self.log_msg('@ entered non-single-char, therefore aborted')
1208 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1209 quote(self.input_)))
1210 self.log_msg('@ sent new protection character for thing')
1211 self.switch_mode('admin')
1212 elif self.mode.name == 'control_tile_type' and key == '\n':
1213 if len(self.input_) != 1:
1214 self.log_msg('@ entered non-single-char, therefore aborted')
1215 self.switch_mode('admin')
1217 self.tile_control_char = self.input_
1218 self.switch_mode('control_tile_draw')
1219 elif self.mode.name == 'chat' and key == '\n':
1220 if self.input_ == '':
1222 if self.input_[0] == '/':
1223 if self.input_.startswith('/nick'):
1224 tokens = self.input_.split(maxsplit=1)
1225 if len(tokens) == 2:
1226 self.send('NICK ' + quote(tokens[1]))
1228 self.log_msg('? need login name')
1230 self.log_msg('? unknown command')
1232 self.send('ALL ' + quote(self.input_))
1234 elif self.mode.name == 'name_thing' and key == '\n':
1235 if self.input_ == '':
1237 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1239 quote(self.password)))
1240 self.switch_mode('edit')
1241 elif self.mode.name == 'annotate' and key == '\n':
1242 if self.input_ == '':
1244 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1245 quote(self.password)))
1246 self.switch_mode('edit')
1247 elif self.mode.name == 'portal' and key == '\n':
1248 if self.input_ == '':
1250 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1251 quote(self.password)))
1252 self.switch_mode('edit')
1253 elif self.mode.name == 'study':
1254 if self.mode.mode_switch_on_key(self, key):
1256 elif key == self.keys['toggle_map_mode']:
1257 self.toggle_map_mode()
1258 elif key in self.movement_keys:
1259 move_explorer(self.movement_keys[key])
1260 elif self.mode.name == 'play':
1261 if self.mode.mode_switch_on_key(self, key):
1263 elif key == self.keys['door'] and task_action_on('door'):
1264 self.send('TASK:DOOR')
1265 elif key == self.keys['consume'] and task_action_on('consume'):
1266 self.send('TASK:INTOXICATE')
1267 elif key == self.keys['wear'] and task_action_on('wear'):
1268 self.send('TASK:WEAR')
1269 elif key == self.keys['spin'] and task_action_on('spin'):
1270 self.send('TASK:SPIN')
1271 elif key == self.keys['teleport']:
1272 if self.game.player.position in self.game.portals:
1273 self.host = self.game.portals[self.game.player.position]
1277 self.log_msg('? not standing on portal')
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 == 'write':
1281 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1282 self.switch_mode('edit')
1283 elif self.mode.name == 'control_tile_draw':
1284 if self.mode.mode_switch_on_key(self, key):
1286 elif key in self.movement_keys:
1287 move_explorer(self.movement_keys[key])
1288 elif key == self.keys['toggle_tile_draw']:
1289 self.tile_draw = False if self.tile_draw else True
1290 elif self.mode.name == 'admin':
1291 if self.mode.mode_switch_on_key(self, key):
1293 elif key in self.movement_keys and task_action_on('move'):
1294 self.send('TASK:MOVE ' + self.movement_keys[key])
1295 elif self.mode.name == 'edit':
1296 if self.mode.mode_switch_on_key(self, key):
1298 elif key == self.keys['flatten'] and task_action_on('flatten'):
1299 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1300 elif key == self.keys['install'] and task_action_on('install'):
1301 self.send('TASK:INSTALL %s' % quote(self.password))
1302 elif key == self.keys['toggle_map_mode']:
1303 self.toggle_map_mode()
1304 elif key in self.movement_keys and task_action_on('move'):
1305 self.send('TASK:MOVE ' + self.movement_keys[key])
1307 if len(sys.argv) != 2:
1308 raise ArgError('wrong number of arguments, need game host')