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_PSEUDO_FOV_WIPE(game):
177 game.portals_new = {}
178 game.annotations_new = {}
180 cmd_PSEUDO_FOV_WIPE.argtypes = ''
182 def cmd_LOGIN_OK(game):
183 game.tui.switch_mode('post_login_wait')
184 game.tui.send('GET_GAMESTATE')
185 game.tui.log_msg('@ welcome')
186 cmd_LOGIN_OK.argtypes = ''
188 def cmd_ADMIN_OK(game):
189 game.tui.is_admin = True
190 game.tui.log_msg('@ you now have admin rights')
191 game.tui.switch_mode('admin')
192 game.tui.do_refresh = True
193 cmd_ADMIN_OK.argtypes = ''
195 def cmd_REPLY(game, msg):
196 game.tui.log_msg('#MUSICPLAYER: ' + msg)
197 game.tui.do_refresh = True
198 cmd_REPLY.argtypes = 'string'
200 def cmd_CHAT(game, msg):
201 game.tui.log_msg('# ' + msg)
202 game.tui.do_refresh = True
203 cmd_CHAT.argtypes = 'string'
205 def cmd_CHATFACE(game, thing_id):
206 game.tui.draw_face = thing_id
207 game.tui.do_refresh = True
208 cmd_CHATFACE.argtypes = 'int:pos'
210 def cmd_PLAYER_ID(game, player_id):
211 game.player_id = player_id
212 cmd_PLAYER_ID.argtypes = 'int:nonneg'
214 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
215 t = game.get_thing_temp(thing_id)
217 t = ThingBase(game, thing_id)
218 game.things_new += [t]
221 t.protection = protection
222 t.portable = portable
223 t.commandable = commandable
224 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
226 def cmd_THING_NAME(game, thing_id, name):
227 t = game.get_thing_temp(thing_id)
229 cmd_THING_NAME.argtypes = 'int:pos string'
231 def cmd_THING_FACE(game, thing_id, face):
232 t = game.get_thing_temp(thing_id)
234 cmd_THING_FACE.argtypes = 'int:pos string'
236 def cmd_THING_HAT(game, thing_id, hat):
237 t = game.get_thing_temp(thing_id)
239 cmd_THING_HAT.argtypes = 'int:pos string'
241 def cmd_THING_CHAR(game, thing_id, c):
242 t = game.get_thing_temp(thing_id)
244 cmd_THING_CHAR.argtypes = 'int:pos char'
246 def cmd_MAP(game, geometry, size, content):
247 map_geometry_class = globals()['MapGeometry' + geometry]
248 game.map_geometry_new = map_geometry_class(size)
249 game.map_content_new = content
250 if type(game.map_geometry) == MapGeometrySquare:
251 game.tui.movement_keys = {
252 game.tui.keys['square_move_up']: 'UP',
253 game.tui.keys['square_move_left']: 'LEFT',
254 game.tui.keys['square_move_down']: 'DOWN',
255 game.tui.keys['square_move_right']: 'RIGHT',
257 elif type(game.map_geometry) == MapGeometryHex:
258 game.tui.movement_keys = {
259 game.tui.keys['hex_move_upleft']: 'UPLEFT',
260 game.tui.keys['hex_move_upright']: 'UPRIGHT',
261 game.tui.keys['hex_move_right']: 'RIGHT',
262 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
263 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
264 game.tui.keys['hex_move_left']: 'LEFT',
266 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
268 def cmd_FOV(game, content):
269 game.fov_new = content
270 cmd_FOV.argtypes = 'string'
272 def cmd_MAP_CONTROL(game, content):
273 game.map_control_content_new = content
274 cmd_MAP_CONTROL.argtypes = 'string'
276 def cmd_GAME_STATE_COMPLETE(game):
277 game.tui.do_refresh = True
278 game.tui.info_cached = None
279 game.things = game.things_new
280 game.portals = game.portals_new
281 game.annotations = game.annotations_new
282 game.fov = game.fov_new
283 game.map_geometry = game.map_geometry_new
284 game.map_content = game.map_content_new
285 game.map_control_content = game.map_control_content_new
286 game.player = game.get_thing(game.player_id)
287 game.turn_complete = True
288 if game.tui.mode.name == 'post_login_wait':
289 game.tui.switch_mode('play')
290 cmd_GAME_STATE_COMPLETE.argtypes = ''
292 def cmd_PORTAL(game, position, msg):
293 game.portals_new[position] = msg
294 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
296 def cmd_PLAY_ERROR(game, msg):
297 game.tui.log_msg('? ' + msg)
298 game.tui.flash = True
299 game.tui.do_refresh = True
300 cmd_PLAY_ERROR.argtypes = 'string'
302 def cmd_GAME_ERROR(game, msg):
303 game.tui.log_msg('? game error: ' + msg)
304 game.tui.do_refresh = True
305 cmd_GAME_ERROR.argtypes = 'string'
307 def cmd_ARGUMENT_ERROR(game, msg):
308 game.tui.log_msg('? syntax error: ' + msg)
309 game.tui.do_refresh = True
310 cmd_ARGUMENT_ERROR.argtypes = 'string'
312 def cmd_ANNOTATION(game, position, msg):
313 game.annotations_new[position] = msg
314 if game.tui.mode.shows_info:
315 game.tui.do_refresh = True
316 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
318 def cmd_TASKS(game, tasks_comma_separated):
319 game.tasks = tasks_comma_separated.split(',')
320 game.tui.mode_write.legal = 'WRITE' in game.tasks
321 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
322 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
323 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
324 cmd_TASKS.argtypes = 'string'
326 def cmd_THING_TYPE(game, thing_type, symbol_hint):
327 game.thing_types[thing_type] = symbol_hint
328 cmd_THING_TYPE.argtypes = 'string char'
330 def cmd_THING_INSTALLED(game, thing_id):
331 game.get_thing_temp(thing_id).installed = True
332 cmd_THING_INSTALLED.argtypes = 'int:pos'
334 def cmd_THING_CARRYING(game, thing_id, carried_id):
335 game.get_thing_temp(thing_id).carrying = game.get_thing(carried_id)
336 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
338 def cmd_TERRAIN(game, terrain_char, terrain_desc):
339 game.terrains[terrain_char] = terrain_desc
340 cmd_TERRAIN.argtypes = 'char string'
344 cmd_PONG.argtypes = ''
346 def cmd_DEFAULT_COLORS(game):
347 game.tui.set_default_colors()
348 cmd_DEFAULT_COLORS.argtypes = ''
350 def cmd_RANDOM_COLORS(game):
351 game.tui.set_random_colors()
352 cmd_RANDOM_COLORS.argtypes = ''
354 class Game(GameBase):
355 turn_complete = False
360 def __init__(self, *args, **kwargs):
361 super().__init__(*args, **kwargs)
362 self.register_command(cmd_LOGIN_OK)
363 self.register_command(cmd_ADMIN_OK)
364 self.register_command(cmd_PONG)
365 self.register_command(cmd_CHAT)
366 self.register_command(cmd_CHATFACE)
367 self.register_command(cmd_REPLY)
368 self.register_command(cmd_PLAYER_ID)
369 self.register_command(cmd_TURN)
370 self.register_command(cmd_PSEUDO_FOV_WIPE)
371 self.register_command(cmd_THING)
372 self.register_command(cmd_THING_TYPE)
373 self.register_command(cmd_THING_NAME)
374 self.register_command(cmd_THING_CHAR)
375 self.register_command(cmd_THING_FACE)
376 self.register_command(cmd_THING_HAT)
377 self.register_command(cmd_THING_CARRYING)
378 self.register_command(cmd_THING_INSTALLED)
379 self.register_command(cmd_TERRAIN)
380 self.register_command(cmd_MAP)
381 self.register_command(cmd_MAP_CONTROL)
382 self.register_command(cmd_PORTAL)
383 self.register_command(cmd_ANNOTATION)
384 self.register_command(cmd_GAME_STATE_COMPLETE)
385 self.register_command(cmd_ARGUMENT_ERROR)
386 self.register_command(cmd_GAME_ERROR)
387 self.register_command(cmd_PLAY_ERROR)
388 self.register_command(cmd_TASKS)
389 self.register_command(cmd_FOV)
390 self.register_command(cmd_DEFAULT_COLORS)
391 self.register_command(cmd_RANDOM_COLORS)
392 self.map_content = ''
394 self.annotations = {}
395 self.annotations_new = {}
397 self.portals_new = {}
401 def get_string_options(self, string_option_type):
402 if string_option_type == 'map_geometry':
403 return ['Hex', 'Square']
404 elif string_option_type == 'thing_type':
405 return self.thing_types.keys()
408 def get_command(self, command_name):
409 from functools import partial
410 f = partial(self.commands[command_name], self)
411 f.argtypes = self.commands[command_name].argtypes
414 def get_thing_temp(self, id_):
415 for thing in self.things_new:
422 def __init__(self, name, has_input_prompt=False, shows_info=False,
423 is_intro=False, is_single_char_entry=False):
425 self.short_desc = mode_helps[name]['short']
426 self.available_modes = []
427 self.available_actions = []
428 self.has_input_prompt = has_input_prompt
429 self.shows_info = shows_info
430 self.is_intro = is_intro
431 self.help_intro = mode_helps[name]['long']
432 self.intro_msg = mode_helps[name]['intro']
433 self.is_single_char_entry = is_single_char_entry
436 def iter_available_modes(self, tui):
437 for mode_name in self.available_modes:
438 mode = getattr(tui, 'mode_' + mode_name)
441 key = tui.keys['switch_to_' + mode.name]
444 def list_available_modes(self, tui):
446 if len(self.available_modes) > 0:
447 msg = 'Other modes available from here:\n'
448 for mode, key in self.iter_available_modes(tui):
449 msg += '[%s] – %s\n' % (key, mode.short_desc)
452 def mode_switch_on_key(self, tui, key_pressed):
453 for mode, key in self.iter_available_modes(tui):
454 if key_pressed == key:
455 tui.switch_mode(mode.name)
460 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
461 mode_admin = Mode('admin')
462 mode_play = Mode('play')
463 mode_study = Mode('study', shows_info=True)
464 mode_write = Mode('write', is_single_char_entry=True)
465 mode_edit = Mode('edit')
466 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
467 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
468 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
469 mode_control_tile_draw = Mode('control_tile_draw')
470 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
471 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
472 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
473 mode_chat = Mode('chat', has_input_prompt=True)
474 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
475 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
476 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
477 mode_password = Mode('password', has_input_prompt=True)
478 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
479 mode_command_thing = Mode('command_thing', has_input_prompt=True)
480 mode_take_thing = Mode('take_thing', has_input_prompt=True)
481 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
482 mode_enter_face = Mode('enter_face', has_input_prompt=True)
486 def __init__(self, host):
489 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
490 "command_thing", "take_thing",
492 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
493 "install", "wear", "spin"]
494 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
495 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
496 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
497 "control_tile_type", "chat",
498 "study", "play", "edit"]
499 self.mode_admin.available_actions = ["move"]
500 self.mode_control_tile_draw.available_modes = ["admin_enter"]
501 self.mode_control_tile_draw.available_actions = ["move_explorer",
503 self.mode_edit.available_modes = ["write", "annotate", "portal",
504 "name_thing", "enter_face", "password",
505 "chat", "study", "play", "admin_enter"]
506 self.mode_edit.available_actions = ["move", "flatten", "install",
512 self.parser = Parser(self.game)
514 self.do_refresh = True
515 self.queue = queue.Queue()
516 self.login_name = None
517 self.map_mode = 'terrain + things'
518 self.password = 'foo'
519 self.switch_mode('waiting_for_server')
521 'switch_to_chat': 't',
522 'switch_to_play': 'p',
523 'switch_to_password': 'P',
524 'switch_to_annotate': 'M',
525 'switch_to_portal': 'T',
526 'switch_to_study': '?',
527 'switch_to_edit': 'E',
528 'switch_to_write': 'm',
529 'switch_to_name_thing': 'N',
530 'switch_to_command_thing': 'O',
531 'switch_to_admin_enter': 'A',
532 'switch_to_control_pw_type': 'C',
533 'switch_to_control_tile_type': 'Q',
534 'switch_to_admin_thing_protect': 'T',
536 'switch_to_enter_face': 'f',
537 'switch_to_take_thing': 'z',
538 'switch_to_drop_thing': 'u',
546 'toggle_map_mode': 'L',
547 'toggle_tile_draw': 'm',
548 'hex_move_upleft': 'w',
549 'hex_move_upright': 'e',
550 'hex_move_right': 'd',
551 'hex_move_downright': 'x',
552 'hex_move_downleft': 'y',
553 'hex_move_left': 'a',
554 'square_move_up': 'w',
555 'square_move_left': 'a',
556 'square_move_down': 's',
557 'square_move_right': 'd',
559 if os.path.isfile('config.json'):
560 with open('config.json', 'r') as f:
561 keys_conf = json.loads(f.read())
563 self.keys[k] = keys_conf[k]
564 self.show_help = False
565 self.disconnected = True
566 self.force_instant_connect = True
567 self.input_lines = []
571 self.offset = YX(0,0)
572 curses.wrapper(self.loop)
576 def handle_recv(msg):
582 self.log_msg('@ attempting connect')
583 socket_client_class = PlomSocketClient
584 if self.host.startswith('ws://') or self.host.startswith('wss://'):
585 socket_client_class = WebSocketClient
587 self.socket = socket_client_class(handle_recv, self.host)
588 self.socket_thread = threading.Thread(target=self.socket.run)
589 self.socket_thread.start()
590 self.disconnected = False
591 self.game.thing_types = {}
592 self.game.terrains = {}
593 time.sleep(0.1) # give potential SSL negotation some time …
594 self.socket.send('TASKS')
595 self.socket.send('TERRAINS')
596 self.socket.send('THING_TYPES')
597 self.switch_mode('login')
598 except ConnectionRefusedError:
599 self.log_msg('@ server connect failure')
600 self.disconnected = True
601 self.switch_mode('waiting_for_server')
602 self.do_refresh = True
605 self.log_msg('@ attempting reconnect')
607 # necessitated by some strange SSL race conditions with ws4py
608 time.sleep(0.1) # FIXME find out why exactly necessary
609 self.switch_mode('waiting_for_server')
614 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
615 raise BrokenSocketConnection
616 self.socket.send(msg)
617 except (BrokenPipeError, BrokenSocketConnection):
618 self.log_msg('@ server disconnected :(')
619 self.disconnected = True
620 self.force_instant_connect = True
621 self.do_refresh = True
623 def log_msg(self, msg):
625 if len(self.log) > 100:
626 self.log = self.log[-100:]
628 def restore_input_values(self):
629 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
630 self.input_ = self.game.annotations[self.explorer]
631 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
632 self.input_ = self.game.portals[self.explorer]
633 elif self.mode.name == 'password':
634 self.input_ = self.password
635 elif self.mode.name == 'name_thing':
636 if hasattr(self.thing_selected, 'name'):
637 self.input_ = self.thing_selected.name
638 elif self.mode.name == 'admin_thing_protect':
639 if hasattr(self.thing_selected, 'protection'):
640 self.input_ = self.thing_selected.protection
642 def send_tile_control_command(self):
643 self.send('SET_TILE_CONTROL %s %s' %
644 (self.explorer, quote(self.tile_control_char)))
646 def toggle_map_mode(self):
647 if self.map_mode == 'terrain only':
648 self.map_mode = 'terrain + annotations'
649 elif self.map_mode == 'terrain + annotations':
650 self.map_mode = 'terrain + things'
651 elif self.map_mode == 'terrain + things':
652 self.map_mode = 'protections'
653 elif self.map_mode == 'protections':
654 self.map_mode = 'terrain only'
656 def switch_mode(self, mode_name):
658 def fail(msg, return_mode='play'):
659 self.log_msg('? ' + msg)
661 self.switch_mode(return_mode)
663 if self.mode and self.mode.name == 'control_tile_draw':
664 self.log_msg('@ finished tile protection drawing.')
665 self.draw_face = False
666 self.tile_draw = False
667 if mode_name == 'command_thing' and\
668 (not self.game.player.carrying or
669 not self.game.player.carrying.commandable):
670 return fail('not carrying anything commandable')
671 if mode_name == 'take_thing' and self.game.player.carrying:
672 return fail('already carrying something')
673 if mode_name == 'drop_thing' and not self.game.player.carrying:
674 return fail('not carrying anything droppable')
675 if mode_name == 'admin_enter' and self.is_admin:
677 elif mode_name in {'name_thing', 'admin_thing_protect'}:
679 for t in [t for t in self.game.things
680 if t.position == self.game.player.position
681 and t.id_ != self.game.player.id_]:
685 return fail('not standing over thing', 'edit')
687 self.thing_selected = thing
688 self.mode = getattr(self, 'mode_' + mode_name)
689 if self.mode.name in {'control_tile_draw', 'control_tile_type',
691 self.map_mode = 'protections'
692 elif self.mode.name != 'edit':
693 self.map_mode = 'terrain + things'
694 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
695 self.explorer = YX(self.game.player.position.y,
696 self.game.player.position.x)
697 if self.mode.is_single_char_entry:
698 self.show_help = True
699 if len(self.mode.intro_msg) > 0:
700 self.log_msg(self.mode.intro_msg)
701 if self.mode.name == 'login':
703 self.send('LOGIN ' + quote(self.login_name))
705 self.log_msg('@ enter username')
706 elif self.mode.name == 'take_thing':
707 self.log_msg('Portable things in reach for pick-up:')
708 select_range = [self.game.player.position,
709 self.game.player.position + YX(0,-1),
710 self.game.player.position + YX(0, 1),
711 self.game.player.position + YX(-1, 0),
712 self.game.player.position + YX(1, 0)]
713 if type(self.game.map_geometry) == MapGeometryHex:
714 if self.game.player.position.y % 2:
715 select_range += [self.game.player.position + YX(-1, 1),
716 self.game.player.position + YX(1, 1)]
718 select_range += [self.game.player.position + YX(-1, -1),
719 self.game.player.position + YX(1, -1)]
720 self.selectables = [t.id_ for t in self.game.things
721 if t.portable and t.position in select_range]
722 if len(self.selectables) == 0:
723 return fail('nothing to pick-up')
725 for i in range(len(self.selectables)):
726 t = self.game.get_thing(self.selectables[i])
727 self.log_msg(str(i) + ': ' + self.get_thing_info(t))
728 elif self.mode.name == 'drop_thing':
729 self.log_msg('Direction to drop thing to:')
731 ['HERE'] + list(self.game.tui.movement_keys.values())
732 for i in range(len(self.selectables)):
733 self.log_msg(str(i) + ': ' + self.selectables[i])
734 elif self.mode.name == 'command_thing':
735 self.send('TASK:COMMAND ' + quote('HELP'))
736 elif self.mode.name == 'control_pw_pw':
737 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
738 elif self.mode.name == 'control_tile_draw':
739 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']))
741 self.restore_input_values()
743 def set_default_colors(self):
744 curses.init_color(1, 1000, 1000, 1000)
745 curses.init_color(2, 0, 0, 0)
746 self.do_refresh = True
748 def set_random_colors(self):
752 return int(offset + random.random()*375)
754 curses.init_color(1, rand(625), rand(625), rand(625))
755 curses.init_color(2, rand(0), rand(0), rand(0))
756 self.do_refresh = True
760 return self.info_cached
761 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
763 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
764 info_to_cache += 'outside field of view'
766 for t in self.game.things:
767 if t.position == self.explorer:
768 info_to_cache += 'THING: %s' % self.get_thing_info(t)
769 protection = t.protection
770 if protection == '.':
772 info_to_cache += ' / protection: %s\n' % protection
773 if hasattr(t, 'hat'):
774 info_to_cache += t.hat[0:6] + '\n'
775 info_to_cache += t.hat[6:12] + '\n'
776 info_to_cache += t.hat[12:18] + '\n'
777 if hasattr(t, 'face'):
778 info_to_cache += t.face[0:6] + '\n'
779 info_to_cache += t.face[6:12] + '\n'
780 info_to_cache += t.face[12:18] + '\n'
781 terrain_char = self.game.map_content[pos_i]
783 if terrain_char in self.game.terrains:
784 terrain_desc = self.game.terrains[terrain_char]
785 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
787 protection = self.game.map_control_content[pos_i]
788 if protection == '.':
789 protection = 'unprotected'
790 info_to_cache += 'PROTECTION: %s\n' % protection
791 if self.explorer in self.game.portals:
792 info_to_cache += 'PORTAL: ' +\
793 self.game.portals[self.explorer] + '\n'
795 info_to_cache += 'PORTAL: (none)\n'
796 if self.explorer in self.game.annotations:
797 info_to_cache += 'ANNOTATION: ' +\
798 self.game.annotations[self.explorer]
799 self.info_cached = info_to_cache
800 return self.info_cached
802 def get_thing_info(self, t):
804 (t.type_, self.game.thing_types[t.type_])
805 if hasattr(t, 'thing_char'):
807 if hasattr(t, 'name'):
808 info += ' (%s)' % t.name
809 if hasattr(t, 'installed'):
810 info += ' / installed'
813 def loop(self, stdscr):
816 def safe_addstr(y, x, line):
817 if y < self.size.y - 1 or x + len(line) < self.size.x:
818 stdscr.addstr(y, x, line, curses.color_pair(1))
819 else: # workaround to <https://stackoverflow.com/q/7063128>
820 cut_i = self.size.x - x - 1
822 last_char = line[cut_i]
823 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
824 stdscr.insstr(y, self.size.x - 2, ' ')
825 stdscr.addstr(y, x, cut, curses.color_pair(1))
827 def handle_input(msg):
828 command, args = self.parser.parse(msg)
831 def task_action_on(action):
832 return action_tasks[action] in self.game.tasks
834 def msg_into_lines_of_width(msg, width):
838 for i in range(len(msg)):
839 if x >= width or msg[i] == "\n":
851 def reset_screen_size():
852 self.size = YX(*stdscr.getmaxyx())
853 self.size = self.size - YX(self.size.y % 4, 0)
854 self.size = self.size - YX(0, self.size.x % 4)
855 self.window_width = int(self.size.x / 2)
857 def recalc_input_lines():
858 if not self.mode.has_input_prompt:
859 self.input_lines = []
861 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
864 def move_explorer(direction):
865 target = self.game.map_geometry.move_yx(self.explorer, direction)
867 self.info_cached = None
868 self.explorer = target
870 self.send_tile_control_command()
876 for line in self.log:
877 lines += msg_into_lines_of_width(line, self.window_width)
880 max_y = self.size.y - len(self.input_lines)
881 for i in range(len(lines)):
882 if (i >= max_y - height_header):
884 safe_addstr(max_y - i - 1, self.window_width, lines[i])
887 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
888 lines = msg_into_lines_of_width(info, self.window_width)
890 for i in range(len(lines)):
891 y = height_header + i
892 if y >= self.size.y - len(self.input_lines):
894 safe_addstr(y, self.window_width, lines[i])
897 y = self.size.y - len(self.input_lines)
898 for i in range(len(self.input_lines)):
899 safe_addstr(y, self.window_width, self.input_lines[i])
903 if not self.game.turn_complete:
905 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
908 help = "hit [%s] for help" % self.keys['help']
909 if self.mode.has_input_prompt:
910 help = "enter /help for help"
911 safe_addstr(1, self.window_width,
912 'MODE: %s – %s' % (self.mode.short_desc, help))
915 if (not self.game.turn_complete) and len(self.map_lines) == 0:
917 if self.game.turn_complete:
919 for y in range(self.game.map_geometry.size.y):
920 start = self.game.map_geometry.size.x * y
921 end = start + self.game.map_geometry.size.x
922 if self.map_mode == 'protections':
923 map_lines_split += [[c + ' ' for c
924 in self.game.map_control_content[start:end]]]
926 map_lines_split += [[c + ' ' for c
927 in self.game.map_content[start:end]]]
928 if self.map_mode == 'terrain + annotations':
929 for p in self.game.annotations:
930 map_lines_split[p.y][p.x] = 'A '
931 elif self.map_mode == 'terrain + things':
932 for p in self.game.portals.keys():
933 original = map_lines_split[p.y][p.x]
934 map_lines_split[p.y][p.x] = original[0] + 'P'
937 def draw_thing(t, used_positions):
938 symbol = self.game.thing_types[t.type_]
940 if hasattr(t, 'thing_char'):
941 meta_char = t.thing_char
942 if t.position in used_positions:
944 if hasattr(t, 'carrying') and t.carrying:
946 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
947 used_positions += [t.position]
949 for t in [t for t in self.game.things if t.type_ != 'Player']:
950 draw_thing(t, used_positions)
951 for t in [t for t in self.game.things if t.type_ == 'Player']:
952 draw_thing(t, used_positions)
953 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
954 map_lines_split[self.explorer.y][self.explorer.x] = '??'
955 elif self.map_mode != 'terrain + things':
956 map_lines_split[self.game.player.position.y]\
957 [self.game.player.position.x] = '??'
959 if type(self.game.map_geometry) == MapGeometryHex:
961 for line in map_lines_split:
962 self.map_lines += [indent * ' ' + ''.join(line)]
963 indent = 0 if indent else 1
965 for line in map_lines_split:
966 self.map_lines += [''.join(line)]
967 window_center = YX(int(self.size.y / 2),
968 int(self.window_width / 2))
969 center = self.game.player.position
970 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
971 center = self.explorer
972 center = YX(center.y, center.x * 2)
973 self.offset = center - window_center
974 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
975 self.offset += YX(0, 1)
976 term_y = max(0, -self.offset.y)
977 term_x = max(0, -self.offset.x)
978 map_y = max(0, self.offset.y)
979 map_x = max(0, self.offset.x)
980 while term_y < self.size.y and map_y < len(self.map_lines):
981 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
982 safe_addstr(term_y, term_x, to_draw)
986 def draw_face_popup():
987 t = self.game.get_thing(self.draw_face)
988 if not t or not hasattr(t, 'face'):
989 self.draw_face = False
992 start_x = self.window_width - 10
994 if hasattr(t, 'thing_char'):
995 t_char = t.thing_char
996 def draw_body_part(body_part, end_y):
997 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
998 safe_addstr(end_y - 3, start_x, '| |')
999 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1000 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1001 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1003 if hasattr(t, 'face'):
1004 draw_body_part(t.face, self.size.y - 2)
1005 if hasattr(t, 'hat'):
1006 draw_body_part(t.hat, self.size.y - 5)
1007 safe_addstr(self.size.y - 1, start_x, '| |')
1010 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1011 self.mode.help_intro)
1012 if len(self.mode.available_actions) > 0:
1013 content += "Available actions:\n"
1014 for action in self.mode.available_actions:
1015 if action in action_tasks:
1016 if action_tasks[action] not in self.game.tasks:
1018 if action == 'move_explorer':
1020 if action == 'move':
1021 key = ','.join(self.movement_keys)
1023 key = self.keys[action]
1024 content += '[%s] – %s\n' % (key, action_descriptions[action])
1026 content += self.mode.list_available_modes(self)
1027 for i in range(self.size.y):
1029 self.window_width * (not self.mode.has_input_prompt),
1030 ' ' * self.window_width)
1032 for line in content.split('\n'):
1033 lines += msg_into_lines_of_width(line, self.window_width)
1034 for i in range(len(lines)):
1035 if i >= self.size.y:
1038 self.window_width * (not self.mode.has_input_prompt),
1043 stdscr.bkgd(' ', curses.color_pair(1))
1044 recalc_input_lines()
1045 if self.mode.has_input_prompt:
1047 if self.mode.shows_info:
1052 if not self.mode.is_intro:
1057 if self.draw_face and self.mode.name in {'chat', 'play'}:
1060 def pick_selectable(task_name):
1062 i = int(self.input_)
1063 if i < 0 or i >= len(self.selectables):
1064 self.log_msg('? invalid index, aborted')
1066 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1068 self.log_msg('? invalid index, aborted')
1070 self.switch_mode('play')
1072 action_descriptions = {
1074 'flatten': 'flatten surroundings',
1075 'teleport': 'teleport',
1076 'take_thing': 'pick up thing',
1077 'drop_thing': 'drop thing',
1078 'toggle_map_mode': 'toggle map view',
1079 'toggle_tile_draw': 'toggle protection character drawing',
1080 'install': '(un-)install',
1081 'wear': '(un-)wear',
1082 'door': 'open/close',
1083 'consume': 'consume',
1088 'flatten': 'FLATTEN_SURROUNDINGS',
1089 'take_thing': 'PICK_UP',
1090 'drop_thing': 'DROP',
1092 'install': 'INSTALL',
1095 'command': 'COMMAND',
1096 'consume': 'INTOXICATE',
1100 curses.curs_set(False) # hide cursor
1101 curses.start_color()
1102 self.set_default_colors()
1103 curses.init_pair(1, 1, 2)
1106 self.explorer = YX(0, 0)
1109 interval = datetime.timedelta(seconds=5)
1110 last_ping = datetime.datetime.now() - interval
1112 if self.disconnected and self.force_instant_connect:
1113 self.force_instant_connect = False
1115 now = datetime.datetime.now()
1116 if now - last_ping > interval:
1117 if self.disconnected:
1127 self.do_refresh = False
1130 msg = self.queue.get(block=False)
1135 key = stdscr.getkey()
1136 self.do_refresh = True
1137 except curses.error:
1142 self.show_help = False
1143 self.draw_face = False
1144 if key == 'KEY_RESIZE':
1146 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1147 self.input_ = self.input_[:-1]
1148 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1149 or (self.mode.has_input_prompt and key == '\n'
1150 and self.input_ == ''\
1151 and self.mode.name in {'chat', 'command_thing',
1152 'take_thing', 'drop_thing',
1154 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1155 self.log_msg('@ aborted')
1156 self.switch_mode('play')
1157 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1158 self.show_help = True
1160 self.restore_input_values()
1161 elif self.mode.has_input_prompt and key != '\n': # Return key
1163 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1164 if len(self.input_) > max_length:
1165 self.input_ = self.input_[:max_length]
1166 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1167 self.show_help = True
1168 elif self.mode.name == 'login' and key == '\n':
1169 self.login_name = self.input_
1170 self.send('LOGIN ' + quote(self.input_))
1172 elif self.mode.name == 'enter_face' and key == '\n':
1173 if len(self.input_) != 18:
1174 self.log_msg('? wrong input length, aborting')
1176 self.send('PLAYER_FACE %s' % quote(self.input_))
1178 self.switch_mode('edit')
1179 elif self.mode.name == 'take_thing' and key == '\n':
1180 pick_selectable('PICK_UP')
1181 elif self.mode.name == 'drop_thing' and key == '\n':
1182 pick_selectable('DROP')
1183 elif self.mode.name == 'command_thing' and key == '\n':
1184 self.send('TASK:COMMAND ' + quote(self.input_))
1186 elif self.mode.name == 'control_pw_pw' and key == '\n':
1187 if self.input_ == '':
1188 self.log_msg('@ aborted')
1190 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1191 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1192 self.switch_mode('admin')
1193 elif self.mode.name == 'password' and key == '\n':
1194 if self.input_ == '':
1196 self.password = self.input_
1197 self.switch_mode('edit')
1198 elif self.mode.name == 'admin_enter' and key == '\n':
1199 self.send('BECOME_ADMIN ' + quote(self.input_))
1200 self.switch_mode('play')
1201 elif self.mode.name == 'control_pw_type' and key == '\n':
1202 if len(self.input_) != 1:
1203 self.log_msg('@ entered non-single-char, therefore aborted')
1204 self.switch_mode('admin')
1206 self.tile_control_char = self.input_
1207 self.switch_mode('control_pw_pw')
1208 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1209 if len(self.input_) != 1:
1210 self.log_msg('@ entered non-single-char, therefore aborted')
1212 self.send('THING_PROTECTION %s %s' % (self.thing_selected.id_,
1213 quote(self.input_)))
1214 self.log_msg('@ sent new protection character for thing')
1215 self.switch_mode('admin')
1216 elif self.mode.name == 'control_tile_type' and key == '\n':
1217 if len(self.input_) != 1:
1218 self.log_msg('@ entered non-single-char, therefore aborted')
1219 self.switch_mode('admin')
1221 self.tile_control_char = self.input_
1222 self.switch_mode('control_tile_draw')
1223 elif self.mode.name == 'chat' and key == '\n':
1224 if self.input_ == '':
1226 if self.input_[0] == '/':
1227 if self.input_.startswith('/nick'):
1228 tokens = self.input_.split(maxsplit=1)
1229 if len(tokens) == 2:
1230 self.send('NICK ' + quote(tokens[1]))
1232 self.log_msg('? need login name')
1234 self.log_msg('? unknown command')
1236 self.send('ALL ' + quote(self.input_))
1238 elif self.mode.name == 'name_thing' and key == '\n':
1239 if self.input_ == '':
1241 self.send('THING_NAME %s %s %s' % (self.thing_selected.id_,
1243 quote(self.password)))
1244 self.switch_mode('edit')
1245 elif self.mode.name == 'annotate' and key == '\n':
1246 if self.input_ == '':
1248 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1249 quote(self.password)))
1250 self.switch_mode('edit')
1251 elif self.mode.name == 'portal' and key == '\n':
1252 if self.input_ == '':
1254 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1255 quote(self.password)))
1256 self.switch_mode('edit')
1257 elif self.mode.name == 'study':
1258 if self.mode.mode_switch_on_key(self, key):
1260 elif key == self.keys['toggle_map_mode']:
1261 self.toggle_map_mode()
1262 elif key in self.movement_keys:
1263 move_explorer(self.movement_keys[key])
1264 elif self.mode.name == 'play':
1265 if self.mode.mode_switch_on_key(self, key):
1267 elif key == self.keys['door'] and task_action_on('door'):
1268 self.send('TASK:DOOR')
1269 elif key == self.keys['consume'] and task_action_on('consume'):
1270 self.send('TASK:INTOXICATE')
1271 elif key == self.keys['wear'] and task_action_on('wear'):
1272 self.send('TASK:WEAR')
1273 elif key == self.keys['spin'] and task_action_on('spin'):
1274 self.send('TASK:SPIN')
1275 elif key == self.keys['teleport']:
1276 if self.game.player.position in self.game.portals:
1277 self.host = self.game.portals[self.game.player.position]
1281 self.log_msg('? not standing on portal')
1282 elif key in self.movement_keys and task_action_on('move'):
1283 self.send('TASK:MOVE ' + self.movement_keys[key])
1284 elif self.mode.name == 'write':
1285 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1286 self.switch_mode('edit')
1287 elif self.mode.name == 'control_tile_draw':
1288 if self.mode.mode_switch_on_key(self, key):
1290 elif key in self.movement_keys:
1291 move_explorer(self.movement_keys[key])
1292 elif key == self.keys['toggle_tile_draw']:
1293 self.tile_draw = False if self.tile_draw else True
1294 elif self.mode.name == 'admin':
1295 if self.mode.mode_switch_on_key(self, key):
1297 elif key in self.movement_keys and task_action_on('move'):
1298 self.send('TASK:MOVE ' + self.movement_keys[key])
1299 elif self.mode.name == 'edit':
1300 if self.mode.mode_switch_on_key(self, key):
1302 elif key == self.keys['flatten'] and task_action_on('flatten'):
1303 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1304 elif key == self.keys['install'] and task_action_on('install'):
1305 self.send('TASK:INSTALL %s' % quote(self.password))
1306 elif key == self.keys['toggle_map_mode']:
1307 self.toggle_map_mode()
1308 elif key in self.movement_keys and task_action_on('move'):
1309 self.send('TASK:MOVE ' + self.movement_keys[key])
1311 if len(sys.argv) != 2:
1312 raise ArgError('wrong number of arguments, need game host')