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 carried thing.'
37 'long': 'Enter a command to the thing you carry. Enter nothing to return to play mode.'
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.'
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 carried thing.'
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..'
61 'intro': '@ enter hat line (enter nothing to abort):',
62 '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. Eat cookies to extend the ASCII characters available for drawing.'
67 '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.'
70 'short': 'change protection character password',
71 'intro': '@ enter protection character for which you want to change the password:',
72 '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.'
75 'short': 'change protection character password',
77 '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.'
79 'control_tile_type': {
80 'short': 'change tiles protection',
81 'intro': '@ enter protection character which you want to draw:',
82 '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.'
84 'control_tile_draw': {
85 'short': 'change tiles protection',
87 '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.'
90 'short': 'annotate tile',
92 '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.'
95 'short': 'edit portal',
97 '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.'
102 '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'
107 'long': 'Enter your player name.'
109 'waiting_for_server': {
110 'short': 'waiting for server response',
111 'intro': '@ waiting for server …',
112 'long': 'Waiting for a server response.'
115 'short': 'waiting for server response',
117 'long': 'Waiting for a server response.'
120 'short': 'set world edit password',
122 '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.'
125 'short': 'become admin',
126 'intro': '@ enter admin password:',
127 'long': 'This mode allows you to become admin if you know an admin password.'
132 'long': 'This mode allows you access to actions limited to administrators.'
136 from ws4py.client import WebSocketBaseClient
137 class WebSocketClient(WebSocketBaseClient):
139 def __init__(self, recv_handler, *args, **kwargs):
140 super().__init__(*args, **kwargs)
141 self.recv_handler = recv_handler
144 def received_message(self, message):
146 message = str(message)
147 self.recv_handler(message)
150 def plom_closed(self):
151 return self.client_terminated
153 from plomrogue.io_tcp import PlomSocket
154 class PlomSocketClient(PlomSocket):
156 def __init__(self, recv_handler, url):
158 self.recv_handler = recv_handler
159 host, port = url.split(':')
160 super().__init__(socket.create_connection((host, port)))
168 for msg in self.recv():
169 if msg == 'NEED_SSL':
170 self.socket = ssl.wrap_socket(self.socket)
172 self.recv_handler(msg)
173 except BrokenSocketConnection:
174 pass # we assume socket will be known as dead by now
176 def cmd_TURN(game, n):
177 game.turn_complete = False
178 cmd_TURN.argtypes = 'int:nonneg'
180 def cmd_OTHER_WIPE(game):
181 game.portals_new = {}
182 game.annotations_new = {}
184 cmd_OTHER_WIPE.argtypes = ''
186 def cmd_LOGIN_OK(game):
187 game.tui.switch_mode('post_login_wait')
188 game.tui.send('GET_GAMESTATE')
189 game.tui.log_msg('@ welcome')
190 cmd_LOGIN_OK.argtypes = ''
192 def cmd_ADMIN_OK(game):
193 game.tui.is_admin = True
194 game.tui.log_msg('@ you now have admin rights')
195 game.tui.switch_mode('admin')
196 game.tui.do_refresh = True
197 cmd_ADMIN_OK.argtypes = ''
199 def cmd_REPLY(game, msg):
200 game.tui.log_msg('#MUSICPLAYER: ' + msg)
201 game.tui.do_refresh = True
202 cmd_REPLY.argtypes = 'string'
204 def cmd_CHAT(game, msg):
205 game.tui.log_msg('# ' + msg)
206 game.tui.do_refresh = True
207 cmd_CHAT.argtypes = 'string'
209 def cmd_CHATFACE(game, thing_id):
210 game.tui.draw_face = thing_id
211 game.tui.do_refresh = True
212 cmd_CHATFACE.argtypes = 'int:pos'
214 def cmd_PLAYER_ID(game, player_id):
215 game.player_id = player_id
216 cmd_PLAYER_ID.argtypes = 'int:nonneg'
218 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
219 game.players_hat_chars_new = hat_chars
220 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
222 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
223 t = game.get_thing_temp(thing_id)
225 t = ThingBase(game, thing_id)
226 game.things_new += [t]
229 t.protection = protection
230 t.portable = portable
231 t.commandable = commandable
232 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
234 def cmd_THING_NAME(game, thing_id, name):
235 t = game.get_thing_temp(thing_id)
237 cmd_THING_NAME.argtypes = 'int:pos string'
239 def cmd_THING_FACE(game, thing_id, face):
240 t = game.get_thing_temp(thing_id)
242 cmd_THING_FACE.argtypes = 'int:pos string'
244 def cmd_THING_HAT(game, thing_id, hat):
245 t = game.get_thing_temp(thing_id)
247 cmd_THING_HAT.argtypes = 'int:pos string'
249 def cmd_THING_CHAR(game, thing_id, c):
250 t = game.get_thing_temp(thing_id)
252 cmd_THING_CHAR.argtypes = 'int:pos char'
254 def cmd_MAP(game, geometry, size, content):
255 map_geometry_class = globals()['MapGeometry' + geometry]
256 game.map_geometry_new = map_geometry_class(size)
257 game.map_content_new = content
258 if type(game.map_geometry_new) == MapGeometrySquare:
259 game.tui.movement_keys = {
260 game.tui.keys['square_move_up']: 'UP',
261 game.tui.keys['square_move_left']: 'LEFT',
262 game.tui.keys['square_move_down']: 'DOWN',
263 game.tui.keys['square_move_right']: 'RIGHT',
265 elif type(game.map_geometry_new) == MapGeometryHex:
266 game.tui.movement_keys = {
267 game.tui.keys['hex_move_upleft']: 'UPLEFT',
268 game.tui.keys['hex_move_upright']: 'UPRIGHT',
269 game.tui.keys['hex_move_right']: 'RIGHT',
270 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
271 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
272 game.tui.keys['hex_move_left']: 'LEFT',
274 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
276 def cmd_FOV(game, content):
277 game.fov_new = content
278 cmd_FOV.argtypes = 'string'
280 def cmd_MAP_CONTROL(game, content):
281 game.map_control_content_new = content
282 cmd_MAP_CONTROL.argtypes = 'string'
284 def cmd_GAME_STATE_COMPLETE(game):
285 game.tui.do_refresh = True
286 game.tui.info_cached = None
287 game.things = game.things_new
288 game.portals = game.portals_new
289 game.annotations = game.annotations_new
290 game.fov = game.fov_new
291 game.map_geometry = game.map_geometry_new
292 game.map_content = game.map_content_new
293 game.map_control_content = game.map_control_content_new
294 game.player = game.get_thing(game.player_id)
295 game.players_hat_chars = game.players_hat_chars_new
296 game.bladder_pressure = game.bladder_pressure_new
297 game.turn_complete = True
298 if game.tui.mode.name == 'post_login_wait':
299 game.tui.switch_mode('play')
300 cmd_GAME_STATE_COMPLETE.argtypes = ''
302 def cmd_PORTAL(game, position, msg):
303 game.portals_new[position] = msg
304 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
306 def cmd_PLAY_ERROR(game, msg):
307 game.tui.log_msg('? ' + msg)
308 game.tui.flash = True
309 game.tui.do_refresh = True
310 cmd_PLAY_ERROR.argtypes = 'string'
312 def cmd_GAME_ERROR(game, msg):
313 game.tui.log_msg('? game error: ' + msg)
314 game.tui.do_refresh = True
315 cmd_GAME_ERROR.argtypes = 'string'
317 def cmd_ARGUMENT_ERROR(game, msg):
318 game.tui.log_msg('? syntax error: ' + msg)
319 game.tui.do_refresh = True
320 cmd_ARGUMENT_ERROR.argtypes = 'string'
322 def cmd_ANNOTATION(game, position, msg):
323 game.annotations_new[position] = msg
324 if game.tui.mode.shows_info:
325 game.tui.do_refresh = True
326 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
328 def cmd_TASKS(game, tasks_comma_separated):
329 game.tasks = tasks_comma_separated.split(',')
330 game.tui.mode_write.legal = 'WRITE' in game.tasks
331 game.tui.mode_command_thing.legal = 'COMMAND' in game.tasks
332 game.tui.mode_take_thing.legal = 'PICK_UP' in game.tasks
333 game.tui.mode_drop_thing.legal = 'DROP' in game.tasks
334 cmd_TASKS.argtypes = 'string'
336 def cmd_THING_TYPE(game, thing_type, symbol_hint):
337 game.thing_types[thing_type] = symbol_hint
338 cmd_THING_TYPE.argtypes = 'string char'
340 def cmd_THING_INSTALLED(game, thing_id):
341 game.get_thing_temp(thing_id).installed = True
342 cmd_THING_INSTALLED.argtypes = 'int:pos'
344 def cmd_THING_CARRYING(game, thing_id, carried_id):
345 game.get_thing_temp(thing_id).carrying = game.get_thing_temp(carried_id)
346 cmd_THING_CARRYING.argtypes = 'int:pos int:pos'
348 def cmd_TERRAIN(game, terrain_char, terrain_desc):
349 game.terrains[terrain_char] = terrain_desc
350 cmd_TERRAIN.argtypes = 'char string'
354 cmd_PONG.argtypes = ''
356 def cmd_DEFAULT_COLORS(game):
357 game.tui.set_default_colors()
358 cmd_DEFAULT_COLORS.argtypes = ''
360 def cmd_RANDOM_COLORS(game):
361 game.tui.set_random_colors()
362 cmd_RANDOM_COLORS.argtypes = ''
364 def cmd_BLADDER_PRESSURE(game, bladder_pressure):
365 game.bladder_pressure_new = bladder_pressure
366 cmd_BLADDER_PRESSURE.argtypes = 'int:nonneg'
368 class Game(GameBase):
369 turn_complete = False
374 def __init__(self, *args, **kwargs):
375 super().__init__(*args, **kwargs)
376 self.register_command(cmd_LOGIN_OK)
377 self.register_command(cmd_ADMIN_OK)
378 self.register_command(cmd_PONG)
379 self.register_command(cmd_CHAT)
380 self.register_command(cmd_CHATFACE)
381 self.register_command(cmd_REPLY)
382 self.register_command(cmd_PLAYER_ID)
383 self.register_command(cmd_TURN)
384 self.register_command(cmd_OTHER_WIPE)
385 self.register_command(cmd_THING)
386 self.register_command(cmd_THING_TYPE)
387 self.register_command(cmd_THING_NAME)
388 self.register_command(cmd_THING_CHAR)
389 self.register_command(cmd_THING_FACE)
390 self.register_command(cmd_THING_HAT)
391 self.register_command(cmd_THING_CARRYING)
392 self.register_command(cmd_THING_INSTALLED)
393 self.register_command(cmd_TERRAIN)
394 self.register_command(cmd_MAP)
395 self.register_command(cmd_MAP_CONTROL)
396 self.register_command(cmd_PORTAL)
397 self.register_command(cmd_ANNOTATION)
398 self.register_command(cmd_GAME_STATE_COMPLETE)
399 self.register_command(cmd_PLAYERS_HAT_CHARS)
400 self.register_command(cmd_ARGUMENT_ERROR)
401 self.register_command(cmd_GAME_ERROR)
402 self.register_command(cmd_PLAY_ERROR)
403 self.register_command(cmd_TASKS)
404 self.register_command(cmd_FOV)
405 self.register_command(cmd_DEFAULT_COLORS)
406 self.register_command(cmd_RANDOM_COLORS)
407 self.register_command(cmd_BLADDER_PRESSURE)
408 self.map_content = ''
409 self.players_hat_chars = ''
411 self.annotations = {}
412 self.annotations_new = {}
414 self.portals_new = {}
417 self.bladder_pressure_new = 0
418 self.bladder_pressure = 0
420 def get_string_options(self, string_option_type):
421 if string_option_type == 'map_geometry':
422 return ['Hex', 'Square']
423 elif string_option_type == 'thing_type':
424 return self.thing_types.keys()
427 def get_command(self, command_name):
428 from functools import partial
429 f = partial(self.commands[command_name], self)
430 f.argtypes = self.commands[command_name].argtypes
433 def get_thing_temp(self, id_):
434 for thing in self.things_new:
441 def __init__(self, name, has_input_prompt=False, shows_info=False,
442 is_intro=False, is_single_char_entry=False):
444 self.short_desc = mode_helps[name]['short']
445 self.available_modes = []
446 self.available_actions = []
447 self.has_input_prompt = has_input_prompt
448 self.shows_info = shows_info
449 self.is_intro = is_intro
450 self.help_intro = mode_helps[name]['long']
451 self.intro_msg = mode_helps[name]['intro']
452 self.is_single_char_entry = is_single_char_entry
455 def iter_available_modes(self, tui):
456 for mode_name in self.available_modes:
457 mode = getattr(tui, 'mode_' + mode_name)
460 key = tui.keys['switch_to_' + mode.name]
463 def list_available_modes(self, tui):
465 if len(self.available_modes) > 0:
466 msg = 'Other modes available from here:\n'
467 for mode, key in self.iter_available_modes(tui):
468 msg += '[%s] – %s\n' % (key, mode.short_desc)
471 def mode_switch_on_key(self, tui, key_pressed):
472 for mode, key in self.iter_available_modes(tui):
473 if key_pressed == key:
474 tui.switch_mode(mode.name)
479 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
480 mode_admin = Mode('admin')
481 mode_play = Mode('play')
482 mode_study = Mode('study', shows_info=True)
483 mode_write = Mode('write', is_single_char_entry=True)
484 mode_edit = Mode('edit')
485 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
486 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
487 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
488 mode_control_tile_draw = Mode('control_tile_draw')
489 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
490 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
491 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
492 mode_chat = Mode('chat', has_input_prompt=True)
493 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
494 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
495 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
496 mode_password = Mode('password', has_input_prompt=True)
497 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
498 mode_command_thing = Mode('command_thing', has_input_prompt=True)
499 mode_take_thing = Mode('take_thing', has_input_prompt=True)
500 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
501 mode_enter_face = Mode('enter_face', has_input_prompt=True)
502 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
506 def __init__(self, host):
509 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
510 "command_thing", "take_thing",
512 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
513 "install", "wear", "spin"]
514 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
515 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
516 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
517 "control_tile_type", "chat",
518 "study", "play", "edit"]
519 self.mode_admin.available_actions = ["move"]
520 self.mode_control_tile_draw.available_modes = ["admin_enter"]
521 self.mode_control_tile_draw.available_actions = ["move_explorer",
523 self.mode_edit.available_modes = ["write", "annotate", "portal",
524 "name_thing", "enter_face", "enter_hat",
526 "chat", "study", "play", "admin_enter"]
527 self.mode_edit.available_actions = ["move", "flatten", "install",
533 self.parser = Parser(self.game)
535 self.do_refresh = True
536 self.queue = queue.Queue()
537 self.login_name = None
538 self.map_mode = 'terrain + things'
539 self.password = 'foo'
540 self.switch_mode('waiting_for_server')
542 'switch_to_chat': 't',
543 'switch_to_play': 'p',
544 'switch_to_password': 'P',
545 'switch_to_annotate': 'M',
546 'switch_to_portal': 'T',
547 'switch_to_study': '?',
548 'switch_to_edit': 'E',
549 'switch_to_write': 'm',
550 'switch_to_name_thing': 'N',
551 'switch_to_command_thing': 'O',
552 'switch_to_admin_enter': 'A',
553 'switch_to_control_pw_type': 'C',
554 'switch_to_control_tile_type': 'Q',
555 'switch_to_admin_thing_protect': 'T',
557 'switch_to_enter_face': 'f',
558 'switch_to_enter_hat': 'H',
559 'switch_to_take_thing': 'z',
560 'switch_to_drop_thing': 'u',
568 'toggle_map_mode': 'L',
569 'toggle_tile_draw': 'm',
570 'hex_move_upleft': 'w',
571 'hex_move_upright': 'e',
572 'hex_move_right': 'd',
573 'hex_move_downright': 'x',
574 'hex_move_downleft': 'y',
575 'hex_move_left': 'a',
576 'square_move_up': 'w',
577 'square_move_left': 'a',
578 'square_move_down': 's',
579 'square_move_right': 'd',
581 if os.path.isfile('config.json'):
582 with open('config.json', 'r') as f:
583 keys_conf = json.loads(f.read())
585 self.keys[k] = keys_conf[k]
586 self.show_help = False
587 self.disconnected = True
588 self.force_instant_connect = True
589 self.input_lines = []
593 self.ascii_draw_stage = 0
594 self.full_ascii_draw = ''
595 self.offset = YX(0,0)
596 curses.wrapper(self.loop)
600 def handle_recv(msg):
606 self.log_msg('@ attempting connect')
607 socket_client_class = PlomSocketClient
608 if self.host.startswith('ws://') or self.host.startswith('wss://'):
609 socket_client_class = WebSocketClient
611 self.socket = socket_client_class(handle_recv, self.host)
612 self.socket_thread = threading.Thread(target=self.socket.run)
613 self.socket_thread.start()
614 self.disconnected = False
615 self.game.thing_types = {}
616 self.game.terrains = {}
617 time.sleep(0.1) # give potential SSL negotation some time …
618 self.socket.send('TASKS')
619 self.socket.send('TERRAINS')
620 self.socket.send('THING_TYPES')
621 self.switch_mode('login')
622 except ConnectionRefusedError:
623 self.log_msg('@ server connect failure')
624 self.disconnected = True
625 self.switch_mode('waiting_for_server')
626 self.do_refresh = True
629 self.log_msg('@ attempting reconnect')
631 # necessitated by some strange SSL race conditions with ws4py
632 time.sleep(0.1) # FIXME find out why exactly necessary
633 self.switch_mode('waiting_for_server')
638 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
639 raise BrokenSocketConnection
640 self.socket.send(msg)
641 except (BrokenPipeError, BrokenSocketConnection):
642 self.log_msg('@ server disconnected :(')
643 self.disconnected = True
644 self.force_instant_connect = True
645 self.do_refresh = True
647 def log_msg(self, msg):
649 if len(self.log) > 100:
650 self.log = self.log[-100:]
652 def restore_input_values(self):
653 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
654 self.input_ = self.game.annotations[self.explorer]
655 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
656 self.input_ = self.game.portals[self.explorer]
657 elif self.mode.name == 'password':
658 self.input_ = self.password
659 elif self.mode.name == 'name_thing':
660 if hasattr(self.game.player.carrying, 'name'):
661 self.input_ = self.game.player.carrying.name
662 elif self.mode.name == 'admin_thing_protect':
663 if hasattr(self.game.player.carrying, 'protection'):
664 self.input_ = self.game.player.carrying.protection
665 elif self.mode.name in {'enter_face', 'enter_hat'}:
666 start = self.ascii_draw_stage * 6
667 end = (self.ascii_draw_stage + 1) * 6
668 if self.mode.name == 'enter_face':
669 self.input_ = self.game.player.face[start:end]
670 elif self.mode.name == 'enter_hat':
671 self.input_ = self.game.player.hat[start:end]
673 def send_tile_control_command(self):
674 self.send('SET_TILE_CONTROL %s %s' %
675 (self.explorer, quote(self.tile_control_char)))
677 def toggle_map_mode(self):
678 if self.map_mode == 'terrain only':
679 self.map_mode = 'terrain + annotations'
680 elif self.map_mode == 'terrain + annotations':
681 self.map_mode = 'terrain + things'
682 elif self.map_mode == 'terrain + things':
683 self.map_mode = 'protections'
684 elif self.map_mode == 'protections':
685 self.map_mode = 'terrain only'
687 def switch_mode(self, mode_name):
689 def fail(msg, return_mode='play'):
690 self.log_msg('? ' + msg)
692 self.switch_mode(return_mode)
694 if self.mode and self.mode.name == 'control_tile_draw':
695 self.log_msg('@ finished tile protection drawing.')
696 self.draw_face = False
697 self.tile_draw = False
698 if mode_name == 'command_thing' and\
699 (not self.game.player.carrying or
700 not self.game.player.carrying.commandable):
701 return fail('not carrying anything commandable')
702 if mode_name == 'name_thing' and not self.game.player.carrying:
703 return fail('not carrying anything to re-name')
704 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
705 return fail('not carrying anything to protect')
706 if mode_name == 'take_thing' and self.game.player.carrying:
707 return fail('already carrying something')
708 if mode_name == 'drop_thing' and not self.game.player.carrying:
709 return fail('not carrying anything droppable')
710 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
711 return fail('not wearing hat to edit', 'edit')
712 if mode_name == 'admin_enter' and self.is_admin:
714 self.mode = getattr(self, 'mode_' + mode_name)
715 if self.mode.name in {'control_tile_draw', 'control_tile_type',
717 self.map_mode = 'protections'
718 elif self.mode.name != 'edit':
719 self.map_mode = 'terrain + things'
720 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
721 self.explorer = YX(self.game.player.position.y,
722 self.game.player.position.x)
723 if self.mode.is_single_char_entry:
724 self.show_help = True
725 if len(self.mode.intro_msg) > 0:
726 self.log_msg(self.mode.intro_msg)
727 if self.mode.name == 'login':
729 self.send('LOGIN ' + quote(self.login_name))
731 self.log_msg('@ enter username')
732 elif self.mode.name == 'take_thing':
733 self.log_msg('Portable things in reach for pick-up:')
735 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
737 if type(self.game.map_geometry) == MapGeometrySquare:
738 directed_moves['UP'] = YX(-1, 0)
739 directed_moves['DOWN'] = YX(1, 0)
740 elif type(self.game.map_geometry) == MapGeometryHex:
741 if self.game.player.position.y % 2:
742 directed_moves['UPLEFT'] = YX(-1, 0)
743 directed_moves['UPRIGHT'] = YX(-1, 1)
744 directed_moves['DOWNLEFT'] = YX(1, 0)
745 directed_moves['DOWNRIGHT'] = YX(1, 1)
747 directed_moves['UPLEFT'] = YX(-1, -1)
748 directed_moves['UPRIGHT'] = YX(-1, 0)
749 directed_moves['DOWNLEFT'] = YX(1, -1)
750 directed_moves['DOWNRIGHT'] = YX(1, 0)
752 for direction in directed_moves:
753 move = directed_moves[direction]
754 select_range[direction] = self.game.player.position + move
755 self.selectables = []
757 for direction in select_range:
758 for t in [t for t in self.game.things
759 if t.portable and t.position == select_range[direction]]:
760 self.selectables += [t.id_]
761 directions += [direction]
762 if len(self.selectables) == 0:
763 return fail('nothing to pick-up')
765 for i in range(len(self.selectables)):
766 t = self.game.get_thing(self.selectables[i])
767 self.log_msg('%s %s: %s' % (i, directions[i],
768 self.get_thing_info(t)))
769 elif self.mode.name == 'drop_thing':
770 self.log_msg('Direction to drop thing to:')
772 ['HERE'] + list(self.game.tui.movement_keys.values())
773 for i in range(len(self.selectables)):
774 self.log_msg(str(i) + ': ' + self.selectables[i])
775 elif self.mode.name == 'enter_hat':
776 self.log_msg('legal characters: ' + self.game.players_hat_chars)
777 elif self.mode.name == 'command_thing':
778 self.send('TASK:COMMAND ' + quote('HELP'))
779 elif self.mode.name == 'control_pw_pw':
780 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
781 elif self.mode.name == 'control_tile_draw':
782 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']))
784 self.restore_input_values()
786 def set_default_colors(self):
787 curses.init_color(1, 1000, 1000, 1000)
788 curses.init_color(2, 0, 0, 0)
789 self.do_refresh = True
791 def set_random_colors(self):
795 return int(offset + random.random()*375)
797 curses.init_color(1, rand(625), rand(625), rand(625))
798 curses.init_color(2, rand(0), rand(0), rand(0))
799 self.do_refresh = True
803 return self.info_cached
804 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
806 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
807 info_to_cache += 'outside field of view'
809 for t in self.game.things:
810 if t.position == self.explorer:
811 info_to_cache += 'THING: %s' % self.get_thing_info(t)
812 protection = t.protection
813 if protection == '.':
815 info_to_cache += ' / protection: %s\n' % protection
816 if hasattr(t, 'hat'):
817 info_to_cache += t.hat[0:6] + '\n'
818 info_to_cache += t.hat[6:12] + '\n'
819 info_to_cache += t.hat[12:18] + '\n'
820 if hasattr(t, 'face'):
821 info_to_cache += t.face[0:6] + '\n'
822 info_to_cache += t.face[6:12] + '\n'
823 info_to_cache += t.face[12:18] + '\n'
824 terrain_char = self.game.map_content[pos_i]
826 if terrain_char in self.game.terrains:
827 terrain_desc = self.game.terrains[terrain_char]
828 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
830 protection = self.game.map_control_content[pos_i]
831 if protection == '.':
832 protection = 'unprotected'
833 info_to_cache += 'PROTECTION: %s\n' % protection
834 if self.explorer in self.game.portals:
835 info_to_cache += 'PORTAL: ' +\
836 self.game.portals[self.explorer] + '\n'
838 info_to_cache += 'PORTAL: (none)\n'
839 if self.explorer in self.game.annotations:
840 info_to_cache += 'ANNOTATION: ' +\
841 self.game.annotations[self.explorer]
842 self.info_cached = info_to_cache
843 return self.info_cached
845 def get_thing_info(self, t):
847 (t.type_, self.game.thing_types[t.type_])
848 if hasattr(t, 'thing_char'):
850 if hasattr(t, 'name'):
851 info += ' (%s)' % t.name
852 if hasattr(t, 'installed'):
853 info += ' / installed'
856 def loop(self, stdscr):
859 def safe_addstr(y, x, line):
860 if y < self.size.y - 1 or x + len(line) < self.size.x:
861 stdscr.addstr(y, x, line, curses.color_pair(1))
862 else: # workaround to <https://stackoverflow.com/q/7063128>
863 cut_i = self.size.x - x - 1
865 last_char = line[cut_i]
866 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
867 stdscr.insstr(y, self.size.x - 2, ' ')
868 stdscr.addstr(y, x, cut, curses.color_pair(1))
870 def handle_input(msg):
871 command, args = self.parser.parse(msg)
874 def task_action_on(action):
875 return action_tasks[action] in self.game.tasks
877 def msg_into_lines_of_width(msg, width):
881 for i in range(len(msg)):
882 if x >= width or msg[i] == "\n":
894 def reset_screen_size():
895 self.size = YX(*stdscr.getmaxyx())
896 self.size = self.size - YX(self.size.y % 4, 0)
897 self.size = self.size - YX(0, self.size.x % 4)
898 self.window_width = int(self.size.x / 2)
900 def recalc_input_lines():
901 if not self.mode.has_input_prompt:
902 self.input_lines = []
904 self.input_lines = msg_into_lines_of_width(input_prompt
908 def move_explorer(direction):
909 target = self.game.map_geometry.move_yx(self.explorer, direction)
911 self.info_cached = None
912 self.explorer = target
914 self.send_tile_control_command()
920 for line in self.log:
921 lines += msg_into_lines_of_width(line, self.window_width)
924 max_y = self.size.y - len(self.input_lines)
925 for i in range(len(lines)):
926 if (i >= max_y - height_header):
928 safe_addstr(max_y - i - 1, self.window_width, lines[i])
931 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
932 lines = msg_into_lines_of_width(info, self.window_width)
934 for i in range(len(lines)):
935 y = height_header + i
936 if y >= self.size.y - len(self.input_lines):
938 safe_addstr(y, self.window_width, lines[i])
941 y = self.size.y - len(self.input_lines)
942 for i in range(len(self.input_lines)):
943 safe_addstr(y, self.window_width, self.input_lines[i])
947 safe_addstr(0, self.window_width, 'BLADDER: ' + str(self.game.bladder_pressure))
950 help = "hit [%s] for help" % self.keys['help']
951 if self.mode.has_input_prompt:
952 help = "enter /help for help"
953 safe_addstr(1, self.window_width,
954 'MODE: %s – %s' % (self.mode.short_desc, help))
957 if (not self.game.turn_complete) and len(self.map_lines) == 0:
959 if self.game.turn_complete:
961 for y in range(self.game.map_geometry.size.y):
962 start = self.game.map_geometry.size.x * y
963 end = start + self.game.map_geometry.size.x
964 if self.map_mode == 'protections':
965 map_lines_split += [[c + ' ' for c
966 in self.game.map_control_content[start:end]]]
968 map_lines_split += [[c + ' ' for c
969 in self.game.map_content[start:end]]]
970 if self.map_mode == 'terrain + annotations':
971 for p in self.game.annotations:
972 map_lines_split[p.y][p.x] = 'A '
973 elif self.map_mode == 'terrain + things':
974 for p in self.game.portals.keys():
975 original = map_lines_split[p.y][p.x]
976 map_lines_split[p.y][p.x] = original[0] + 'P'
979 def draw_thing(t, used_positions):
980 symbol = self.game.thing_types[t.type_]
982 if hasattr(t, 'thing_char'):
983 meta_char = t.thing_char
984 if t.position in used_positions:
986 if hasattr(t, 'carrying') and t.carrying:
988 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
989 used_positions += [t.position]
991 for t in [t for t in self.game.things if t.type_ != 'Player']:
992 draw_thing(t, used_positions)
993 for t in [t for t in self.game.things if t.type_ == 'Player']:
994 draw_thing(t, used_positions)
995 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
996 map_lines_split[self.explorer.y][self.explorer.x] = '??'
997 elif self.map_mode != 'terrain + things':
998 map_lines_split[self.game.player.position.y]\
999 [self.game.player.position.x] = '??'
1001 if type(self.game.map_geometry) == MapGeometryHex:
1003 for line in map_lines_split:
1004 self.map_lines += [indent * ' ' + ''.join(line)]
1005 indent = 0 if indent else 1
1007 for line in map_lines_split:
1008 self.map_lines += [''.join(line)]
1009 window_center = YX(int(self.size.y / 2),
1010 int(self.window_width / 2))
1011 center = self.game.player.position
1012 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1013 center = self.explorer
1014 center = YX(center.y, center.x * 2)
1015 self.offset = center - window_center
1016 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1017 self.offset += YX(0, 1)
1018 term_y = max(0, -self.offset.y)
1019 term_x = max(0, -self.offset.x)
1020 map_y = max(0, self.offset.y)
1021 map_x = max(0, self.offset.x)
1022 while term_y < self.size.y and map_y < len(self.map_lines):
1023 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1024 safe_addstr(term_y, term_x, to_draw)
1028 def draw_face_popup():
1029 t = self.game.get_thing(self.draw_face)
1030 if not t or not hasattr(t, 'face'):
1031 self.draw_face = False
1034 start_x = self.window_width - 10
1036 if hasattr(t, 'thing_char'):
1037 t_char = t.thing_char
1038 def draw_body_part(body_part, end_y):
1039 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1040 safe_addstr(end_y - 3, start_x, '| |')
1041 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1042 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1043 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1045 if hasattr(t, 'face'):
1046 draw_body_part(t.face, self.size.y - 2)
1047 if hasattr(t, 'hat'):
1048 draw_body_part(t.hat, self.size.y - 5)
1049 safe_addstr(self.size.y - 1, start_x, '| |')
1052 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1053 self.mode.help_intro)
1054 if len(self.mode.available_actions) > 0:
1055 content += "Available actions:\n"
1056 for action in self.mode.available_actions:
1057 if action in action_tasks:
1058 if action_tasks[action] not in self.game.tasks:
1060 if action == 'move_explorer':
1062 if action == 'move':
1063 key = ','.join(self.movement_keys)
1065 key = self.keys[action]
1066 content += '[%s] – %s\n' % (key, action_descriptions[action])
1068 content += self.mode.list_available_modes(self)
1069 for i in range(self.size.y):
1071 self.window_width * (not self.mode.has_input_prompt),
1072 ' ' * self.window_width)
1074 for line in content.split('\n'):
1075 lines += msg_into_lines_of_width(line, self.window_width)
1076 for i in range(len(lines)):
1077 if i >= self.size.y:
1080 self.window_width * (not self.mode.has_input_prompt),
1085 stdscr.bkgd(' ', curses.color_pair(1))
1086 recalc_input_lines()
1087 if self.mode.has_input_prompt:
1089 if self.mode.shows_info:
1094 if not self.mode.is_intro:
1099 if self.draw_face and self.mode.name in {'chat', 'play'}:
1102 def pick_selectable(task_name):
1104 i = int(self.input_)
1105 if i < 0 or i >= len(self.selectables):
1106 self.log_msg('? invalid index, aborted')
1108 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1110 self.log_msg('? invalid index, aborted')
1112 self.switch_mode('play')
1114 def enter_ascii_art(command):
1115 if len(self.input_) != 6:
1116 self.log_msg('? wrong input length, must be 6; try again')
1118 self.log_msg(' ' + self.input_)
1119 self.full_ascii_draw += self.input_
1120 self.ascii_draw_stage += 1
1121 if self.ascii_draw_stage < 3:
1122 self.restore_input_values()
1124 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1125 self.full_ascii_draw = ""
1126 self.ascii_draw_stage = 0
1128 self.switch_mode('edit')
1130 action_descriptions = {
1132 'flatten': 'flatten surroundings',
1133 'teleport': 'teleport',
1134 'take_thing': 'pick up thing',
1135 'drop_thing': 'drop thing',
1136 'toggle_map_mode': 'toggle map view',
1137 'toggle_tile_draw': 'toggle protection character drawing',
1138 'install': '(un-)install',
1139 'wear': '(un-)wear',
1140 'door': 'open/close',
1141 'consume': 'consume',
1146 'flatten': 'FLATTEN_SURROUNDINGS',
1147 'take_thing': 'PICK_UP',
1148 'drop_thing': 'DROP',
1150 'install': 'INSTALL',
1153 'command': 'COMMAND',
1154 'consume': 'INTOXICATE',
1158 curses.curs_set(False) # hide cursor
1159 curses.start_color()
1160 self.set_default_colors()
1161 curses.init_pair(1, 1, 2)
1164 self.explorer = YX(0, 0)
1167 interval = datetime.timedelta(seconds=5)
1168 last_ping = datetime.datetime.now() - interval
1170 if self.disconnected and self.force_instant_connect:
1171 self.force_instant_connect = False
1173 now = datetime.datetime.now()
1174 if now - last_ping > interval:
1175 if self.disconnected:
1185 self.do_refresh = False
1188 msg = self.queue.get(block=False)
1193 key = stdscr.getkey()
1194 self.do_refresh = True
1195 except curses.error:
1200 self.show_help = False
1201 self.draw_face = False
1202 if key == 'KEY_RESIZE':
1204 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1205 self.input_ = self.input_[:-1]
1206 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1207 or (self.mode.has_input_prompt and key == '\n'
1208 and self.input_ == ''\
1209 and self.mode.name in {'chat', 'command_thing',
1210 'take_thing', 'drop_thing',
1212 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1213 self.log_msg('@ aborted')
1214 self.switch_mode('play')
1215 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1216 self.show_help = True
1218 self.restore_input_values()
1219 elif self.mode.has_input_prompt and key != '\n': # Return key
1221 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1222 if len(self.input_) > max_length:
1223 self.input_ = self.input_[:max_length]
1224 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1225 self.show_help = True
1226 elif self.mode.name == 'login' and key == '\n':
1227 self.login_name = self.input_
1228 self.send('LOGIN ' + quote(self.input_))
1230 elif self.mode.name == 'enter_face' and key == '\n':
1231 enter_ascii_art('PLAYER_FACE')
1232 elif self.mode.name == 'enter_hat' and key == '\n':
1233 enter_ascii_art('PLAYER_HAT')
1234 elif self.mode.name == 'take_thing' and key == '\n':
1235 pick_selectable('PICK_UP')
1236 elif self.mode.name == 'drop_thing' and key == '\n':
1237 pick_selectable('DROP')
1238 elif self.mode.name == 'command_thing' and key == '\n':
1239 self.send('TASK:COMMAND ' + quote(self.input_))
1241 elif self.mode.name == 'control_pw_pw' and key == '\n':
1242 if self.input_ == '':
1243 self.log_msg('@ aborted')
1245 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1246 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1247 self.switch_mode('admin')
1248 elif self.mode.name == 'password' and key == '\n':
1249 if self.input_ == '':
1251 self.password = self.input_
1252 self.switch_mode('edit')
1253 elif self.mode.name == 'admin_enter' and key == '\n':
1254 self.send('BECOME_ADMIN ' + quote(self.input_))
1255 self.switch_mode('play')
1256 elif self.mode.name == 'control_pw_type' and key == '\n':
1257 if len(self.input_) != 1:
1258 self.log_msg('@ entered non-single-char, therefore aborted')
1259 self.switch_mode('admin')
1261 self.tile_control_char = self.input_
1262 self.switch_mode('control_pw_pw')
1263 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1264 if len(self.input_) != 1:
1265 self.log_msg('@ entered non-single-char, therefore aborted')
1267 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1268 self.log_msg('@ sent new protection character for thing')
1269 self.switch_mode('admin')
1270 elif self.mode.name == 'control_tile_type' and key == '\n':
1271 if len(self.input_) != 1:
1272 self.log_msg('@ entered non-single-char, therefore aborted')
1273 self.switch_mode('admin')
1275 self.tile_control_char = self.input_
1276 self.switch_mode('control_tile_draw')
1277 elif self.mode.name == 'chat' and key == '\n':
1278 if self.input_ == '':
1280 if self.input_[0] == '/':
1281 if self.input_.startswith('/nick'):
1282 tokens = self.input_.split(maxsplit=1)
1283 if len(tokens) == 2:
1284 self.send('NICK ' + quote(tokens[1]))
1286 self.log_msg('? need login name')
1288 self.log_msg('? unknown command')
1290 self.send('ALL ' + quote(self.input_))
1292 elif self.mode.name == 'name_thing' and key == '\n':
1293 if self.input_ == '':
1295 self.send('THING_NAME %s %s' % (quote(self.input_),
1296 quote(self.password)))
1297 self.switch_mode('edit')
1298 elif self.mode.name == 'annotate' and key == '\n':
1299 if self.input_ == '':
1301 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1302 quote(self.password)))
1303 self.switch_mode('edit')
1304 elif self.mode.name == 'portal' and key == '\n':
1305 if self.input_ == '':
1307 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1308 quote(self.password)))
1309 self.switch_mode('edit')
1310 elif self.mode.name == 'study':
1311 if self.mode.mode_switch_on_key(self, key):
1313 elif key == self.keys['toggle_map_mode']:
1314 self.toggle_map_mode()
1315 elif key in self.movement_keys:
1316 move_explorer(self.movement_keys[key])
1317 elif self.mode.name == 'play':
1318 if self.mode.mode_switch_on_key(self, key):
1320 elif key == self.keys['door'] and task_action_on('door'):
1321 self.send('TASK:DOOR')
1322 elif key == self.keys['consume'] and task_action_on('consume'):
1323 self.send('TASK:INTOXICATE')
1324 elif key == self.keys['wear'] and task_action_on('wear'):
1325 self.send('TASK:WEAR')
1326 elif key == self.keys['spin'] and task_action_on('spin'):
1327 self.send('TASK:SPIN')
1328 elif key == self.keys['teleport']:
1329 if self.game.player.position in self.game.portals:
1330 self.host = self.game.portals[self.game.player.position]
1334 self.log_msg('? not standing on portal')
1335 elif key in self.movement_keys and task_action_on('move'):
1336 self.send('TASK:MOVE ' + self.movement_keys[key])
1337 elif self.mode.name == 'write':
1338 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1339 self.switch_mode('edit')
1340 elif self.mode.name == 'control_tile_draw':
1341 if self.mode.mode_switch_on_key(self, key):
1343 elif key in self.movement_keys:
1344 move_explorer(self.movement_keys[key])
1345 elif key == self.keys['toggle_tile_draw']:
1346 self.tile_draw = False if self.tile_draw else True
1347 elif self.mode.name == 'admin':
1348 if self.mode.mode_switch_on_key(self, key):
1350 elif key in self.movement_keys and task_action_on('move'):
1351 self.send('TASK:MOVE ' + self.movement_keys[key])
1352 elif self.mode.name == 'edit':
1353 if self.mode.mode_switch_on_key(self, key):
1355 elif key == self.keys['flatten'] and task_action_on('flatten'):
1356 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1357 elif key == self.keys['install'] and task_action_on('install'):
1358 self.send('TASK:INSTALL %s' % quote(self.password))
1359 elif key == self.keys['toggle_map_mode']:
1360 self.toggle_map_mode()
1361 elif key in self.movement_keys and task_action_on('move'):
1362 self.send('TASK:MOVE ' + self.movement_keys[key])
1364 if len(sys.argv) != 2:
1365 raise ArgError('wrong number of arguments, need game host')