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):
178 game.turn_complete = False
179 cmd_TURN.argtypes = 'int:nonneg'
181 def cmd_OTHER_WIPE(game):
182 game.portals_new = {}
183 game.annotations_new = {}
185 cmd_OTHER_WIPE.argtypes = ''
187 def cmd_LOGIN_OK(game):
188 game.tui.switch_mode('post_login_wait')
189 game.tui.send('GET_GAMESTATE')
190 game.tui.log_msg('@ welcome')
191 cmd_LOGIN_OK.argtypes = ''
193 def cmd_ADMIN_OK(game):
194 game.tui.is_admin = True
195 game.tui.log_msg('@ you now have admin rights')
196 game.tui.switch_mode('admin')
197 game.tui.do_refresh = True
198 cmd_ADMIN_OK.argtypes = ''
200 def cmd_REPLY(game, msg):
201 game.tui.log_msg('#MUSICPLAYER: ' + msg)
202 game.tui.do_refresh = True
203 cmd_REPLY.argtypes = 'string'
205 def cmd_CHAT(game, msg):
206 game.tui.log_msg('# ' + msg)
207 game.tui.do_refresh = True
208 cmd_CHAT.argtypes = 'string'
210 def cmd_CHATFACE(game, thing_id):
211 game.tui.draw_face = thing_id
212 game.tui.do_refresh = True
213 cmd_CHATFACE.argtypes = 'int:pos'
215 def cmd_PLAYER_ID(game, player_id):
216 game.player_id = player_id
217 cmd_PLAYER_ID.argtypes = 'int:nonneg'
219 def cmd_PLAYERS_HAT_CHARS(game, hat_chars):
220 game.players_hat_chars_new = hat_chars
221 cmd_PLAYERS_HAT_CHARS.argtypes = 'string'
223 def cmd_THING(game, yx, thing_type, protection, thing_id, portable, commandable):
224 t = game.get_thing_temp(thing_id)
226 t = ThingBase(game, thing_id)
227 game.things_new += [t]
230 t.protection = protection
231 t.portable = portable
232 t.commandable = commandable
233 cmd_THING.argtypes = 'yx_tuple:nonneg string:thing_type char int:nonneg bool bool'
235 def cmd_THING_NAME(game, thing_id, name):
236 t = game.get_thing_temp(thing_id)
238 cmd_THING_NAME.argtypes = 'int:pos string'
240 def cmd_THING_FACE(game, thing_id, face):
241 t = game.get_thing_temp(thing_id)
243 cmd_THING_FACE.argtypes = 'int:pos string'
245 def cmd_THING_HAT(game, thing_id, hat):
246 t = game.get_thing_temp(thing_id)
248 cmd_THING_HAT.argtypes = 'int:pos string'
250 def cmd_THING_CHAR(game, thing_id, c):
251 t = game.get_thing_temp(thing_id)
253 cmd_THING_CHAR.argtypes = 'int:pos char'
255 def cmd_MAP(game, geometry, size, content):
256 map_geometry_class = globals()['MapGeometry' + geometry]
257 game.map_geometry_new = map_geometry_class(size)
258 game.map_content_new = content
259 if type(game.map_geometry_new) == MapGeometrySquare:
260 game.tui.movement_keys = {
261 game.tui.keys['square_move_up']: 'UP',
262 game.tui.keys['square_move_left']: 'LEFT',
263 game.tui.keys['square_move_down']: 'DOWN',
264 game.tui.keys['square_move_right']: 'RIGHT',
266 elif type(game.map_geometry_new) == MapGeometryHex:
267 game.tui.movement_keys = {
268 game.tui.keys['hex_move_upleft']: 'UPLEFT',
269 game.tui.keys['hex_move_upright']: 'UPRIGHT',
270 game.tui.keys['hex_move_right']: 'RIGHT',
271 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
272 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
273 game.tui.keys['hex_move_left']: 'LEFT',
275 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
277 def cmd_FOV(game, content):
278 game.fov_new = content
279 cmd_FOV.argtypes = 'string'
281 def cmd_MAP_CONTROL(game, content):
282 game.map_control_content_new = content
283 cmd_MAP_CONTROL.argtypes = 'string'
285 def cmd_GAME_STATE_COMPLETE(game):
286 game.tui.do_refresh = True
287 game.tui.info_cached = None
288 game.things = game.things_new
289 game.portals = game.portals_new
290 game.annotations = game.annotations_new
291 game.fov = game.fov_new
292 game.map_geometry = game.map_geometry_new
293 game.map_content = game.map_content_new
294 game.map_control_content = game.map_control_content_new
295 game.player = game.get_thing(game.player_id)
296 game.players_hat_chars = game.players_hat_chars_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 class Game(GameBase):
365 turn_complete = False
370 def __init__(self, *args, **kwargs):
371 super().__init__(*args, **kwargs)
372 self.register_command(cmd_LOGIN_OK)
373 self.register_command(cmd_ADMIN_OK)
374 self.register_command(cmd_PONG)
375 self.register_command(cmd_CHAT)
376 self.register_command(cmd_CHATFACE)
377 self.register_command(cmd_REPLY)
378 self.register_command(cmd_PLAYER_ID)
379 self.register_command(cmd_TURN)
380 self.register_command(cmd_OTHER_WIPE)
381 self.register_command(cmd_THING)
382 self.register_command(cmd_THING_TYPE)
383 self.register_command(cmd_THING_NAME)
384 self.register_command(cmd_THING_CHAR)
385 self.register_command(cmd_THING_FACE)
386 self.register_command(cmd_THING_HAT)
387 self.register_command(cmd_THING_CARRYING)
388 self.register_command(cmd_THING_INSTALLED)
389 self.register_command(cmd_TERRAIN)
390 self.register_command(cmd_MAP)
391 self.register_command(cmd_MAP_CONTROL)
392 self.register_command(cmd_PORTAL)
393 self.register_command(cmd_ANNOTATION)
394 self.register_command(cmd_GAME_STATE_COMPLETE)
395 self.register_command(cmd_PLAYERS_HAT_CHARS)
396 self.register_command(cmd_ARGUMENT_ERROR)
397 self.register_command(cmd_GAME_ERROR)
398 self.register_command(cmd_PLAY_ERROR)
399 self.register_command(cmd_TASKS)
400 self.register_command(cmd_FOV)
401 self.register_command(cmd_DEFAULT_COLORS)
402 self.register_command(cmd_RANDOM_COLORS)
403 self.map_content = ''
404 self.players_hat_chars = ''
406 self.annotations = {}
407 self.annotations_new = {}
409 self.portals_new = {}
413 def get_string_options(self, string_option_type):
414 if string_option_type == 'map_geometry':
415 return ['Hex', 'Square']
416 elif string_option_type == 'thing_type':
417 return self.thing_types.keys()
420 def get_command(self, command_name):
421 from functools import partial
422 f = partial(self.commands[command_name], self)
423 f.argtypes = self.commands[command_name].argtypes
426 def get_thing_temp(self, id_):
427 for thing in self.things_new:
434 def __init__(self, name, has_input_prompt=False, shows_info=False,
435 is_intro=False, is_single_char_entry=False):
437 self.short_desc = mode_helps[name]['short']
438 self.available_modes = []
439 self.available_actions = []
440 self.has_input_prompt = has_input_prompt
441 self.shows_info = shows_info
442 self.is_intro = is_intro
443 self.help_intro = mode_helps[name]['long']
444 self.intro_msg = mode_helps[name]['intro']
445 self.is_single_char_entry = is_single_char_entry
448 def iter_available_modes(self, tui):
449 for mode_name in self.available_modes:
450 mode = getattr(tui, 'mode_' + mode_name)
453 key = tui.keys['switch_to_' + mode.name]
456 def list_available_modes(self, tui):
458 if len(self.available_modes) > 0:
459 msg = 'Other modes available from here:\n'
460 for mode, key in self.iter_available_modes(tui):
461 msg += '[%s] – %s\n' % (key, mode.short_desc)
464 def mode_switch_on_key(self, tui, key_pressed):
465 for mode, key in self.iter_available_modes(tui):
466 if key_pressed == key:
467 tui.switch_mode(mode.name)
472 mode_admin_enter = Mode('admin_enter', has_input_prompt=True)
473 mode_admin = Mode('admin')
474 mode_play = Mode('play')
475 mode_study = Mode('study', shows_info=True)
476 mode_write = Mode('write', is_single_char_entry=True)
477 mode_edit = Mode('edit')
478 mode_control_pw_type = Mode('control_pw_type', has_input_prompt=True)
479 mode_control_pw_pw = Mode('control_pw_pw', has_input_prompt=True)
480 mode_control_tile_type = Mode('control_tile_type', has_input_prompt=True)
481 mode_control_tile_draw = Mode('control_tile_draw')
482 mode_admin_thing_protect = Mode('admin_thing_protect', has_input_prompt=True)
483 mode_annotate = Mode('annotate', has_input_prompt=True, shows_info=True)
484 mode_portal = Mode('portal', has_input_prompt=True, shows_info=True)
485 mode_chat = Mode('chat', has_input_prompt=True)
486 mode_waiting_for_server = Mode('waiting_for_server', is_intro=True)
487 mode_login = Mode('login', has_input_prompt=True, is_intro=True)
488 mode_post_login_wait = Mode('post_login_wait', is_intro=True)
489 mode_password = Mode('password', has_input_prompt=True)
490 mode_name_thing = Mode('name_thing', has_input_prompt=True, shows_info=True)
491 mode_command_thing = Mode('command_thing', has_input_prompt=True)
492 mode_take_thing = Mode('take_thing', has_input_prompt=True)
493 mode_drop_thing = Mode('drop_thing', has_input_prompt=True)
494 mode_enter_face = Mode('enter_face', has_input_prompt=True)
495 mode_enter_hat = Mode('enter_hat', has_input_prompt=True)
499 def __init__(self, host):
502 self.mode_play.available_modes = ["chat", "study", "edit", "admin_enter",
503 "command_thing", "take_thing",
505 self.mode_play.available_actions = ["move", "teleport", "door", "consume",
506 "install", "wear", "spin"]
507 self.mode_study.available_modes = ["chat", "play", "admin_enter", "edit"]
508 self.mode_study.available_actions = ["toggle_map_mode", "move_explorer"]
509 self.mode_admin.available_modes = ["admin_thing_protect", "control_pw_type",
510 "control_tile_type", "chat",
511 "study", "play", "edit"]
512 self.mode_admin.available_actions = ["move"]
513 self.mode_control_tile_draw.available_modes = ["admin_enter"]
514 self.mode_control_tile_draw.available_actions = ["move_explorer",
516 self.mode_edit.available_modes = ["write", "annotate", "portal",
517 "name_thing", "enter_face", "enter_hat",
519 "chat", "study", "play", "admin_enter"]
520 self.mode_edit.available_actions = ["move", "flatten", "install",
526 self.parser = Parser(self.game)
528 self.do_refresh = True
529 self.queue = queue.Queue()
530 self.login_name = None
531 self.map_mode = 'terrain + things'
532 self.password = 'foo'
533 self.switch_mode('waiting_for_server')
535 'switch_to_chat': 't',
536 'switch_to_play': 'p',
537 'switch_to_password': 'P',
538 'switch_to_annotate': 'M',
539 'switch_to_portal': 'T',
540 'switch_to_study': '?',
541 'switch_to_edit': 'E',
542 'switch_to_write': 'm',
543 'switch_to_name_thing': 'N',
544 'switch_to_command_thing': 'O',
545 'switch_to_admin_enter': 'A',
546 'switch_to_control_pw_type': 'C',
547 'switch_to_control_tile_type': 'Q',
548 'switch_to_admin_thing_protect': 'T',
550 'switch_to_enter_face': 'f',
551 'switch_to_enter_hat': 'H',
552 'switch_to_take_thing': 'z',
553 'switch_to_drop_thing': 'u',
561 'toggle_map_mode': 'L',
562 'toggle_tile_draw': 'm',
563 'hex_move_upleft': 'w',
564 'hex_move_upright': 'e',
565 'hex_move_right': 'd',
566 'hex_move_downright': 'x',
567 'hex_move_downleft': 'y',
568 'hex_move_left': 'a',
569 'square_move_up': 'w',
570 'square_move_left': 'a',
571 'square_move_down': 's',
572 'square_move_right': 'd',
574 if os.path.isfile('config.json'):
575 with open('config.json', 'r') as f:
576 keys_conf = json.loads(f.read())
578 self.keys[k] = keys_conf[k]
579 self.show_help = False
580 self.disconnected = True
581 self.force_instant_connect = True
582 self.input_lines = []
586 self.ascii_draw_stage = 0
587 self.full_ascii_draw = ''
588 self.offset = YX(0,0)
589 curses.wrapper(self.loop)
593 def handle_recv(msg):
599 self.log_msg('@ attempting connect')
600 socket_client_class = PlomSocketClient
601 if self.host.startswith('ws://') or self.host.startswith('wss://'):
602 socket_client_class = WebSocketClient
604 self.socket = socket_client_class(handle_recv, self.host)
605 self.socket_thread = threading.Thread(target=self.socket.run)
606 self.socket_thread.start()
607 self.disconnected = False
608 self.game.thing_types = {}
609 self.game.terrains = {}
610 time.sleep(0.1) # give potential SSL negotation some time …
611 self.socket.send('TASKS')
612 self.socket.send('TERRAINS')
613 self.socket.send('THING_TYPES')
614 self.switch_mode('login')
615 except ConnectionRefusedError:
616 self.log_msg('@ server connect failure')
617 self.disconnected = True
618 self.switch_mode('waiting_for_server')
619 self.do_refresh = True
622 self.log_msg('@ attempting reconnect')
624 # necessitated by some strange SSL race conditions with ws4py
625 time.sleep(0.1) # FIXME find out why exactly necessary
626 self.switch_mode('waiting_for_server')
631 if hasattr(self.socket, 'plom_closed') and self.socket.plom_closed:
632 raise BrokenSocketConnection
633 self.socket.send(msg)
634 except (BrokenPipeError, BrokenSocketConnection):
635 self.log_msg('@ server disconnected :(')
636 self.disconnected = True
637 self.force_instant_connect = True
638 self.do_refresh = True
640 def log_msg(self, msg):
642 if len(self.log) > 100:
643 self.log = self.log[-100:]
645 def restore_input_values(self):
646 if self.mode.name == 'annotate' and self.explorer in self.game.annotations:
647 self.input_ = self.game.annotations[self.explorer]
648 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
649 self.input_ = self.game.portals[self.explorer]
650 elif self.mode.name == 'password':
651 self.input_ = self.password
652 elif self.mode.name == 'name_thing':
653 if hasattr(self.game.player.carrying, 'name'):
654 self.input_ = self.game.player.carrying.name
655 elif self.mode.name == 'admin_thing_protect':
656 if hasattr(self.game.player.carrying, 'protection'):
657 self.input_ = self.game.player.carrying.protection
658 elif self.mode.name in {'enter_face', 'enter_hat'}:
659 start = self.ascii_draw_stage * 6
660 end = (self.ascii_draw_stage + 1) * 6
661 if self.mode.name == 'enter_face':
662 self.input_ = self.game.player.face[start:end]
663 elif self.mode.name == 'enter_hat':
664 self.input_ = self.game.player.hat[start:end]
666 def send_tile_control_command(self):
667 self.send('SET_TILE_CONTROL %s %s' %
668 (self.explorer, quote(self.tile_control_char)))
670 def toggle_map_mode(self):
671 if self.map_mode == 'terrain only':
672 self.map_mode = 'terrain + annotations'
673 elif self.map_mode == 'terrain + annotations':
674 self.map_mode = 'terrain + things'
675 elif self.map_mode == 'terrain + things':
676 self.map_mode = 'protections'
677 elif self.map_mode == 'protections':
678 self.map_mode = 'terrain only'
680 def switch_mode(self, mode_name):
682 def fail(msg, return_mode='play'):
683 self.log_msg('? ' + msg)
685 self.switch_mode(return_mode)
687 if self.mode and self.mode.name == 'control_tile_draw':
688 self.log_msg('@ finished tile protection drawing.')
689 self.draw_face = False
690 self.tile_draw = False
691 if mode_name == 'command_thing' and\
692 (not self.game.player.carrying or
693 not self.game.player.carrying.commandable):
694 return fail('not carrying anything commandable')
695 if mode_name == 'name_thing' and not self.game.player.carrying:
696 return fail('not carrying anything to re-name')
697 if mode_name == 'admin_thing_protect' and not self.game.player.carrying:
698 return fail('not carrying anything to protect')
699 if mode_name == 'take_thing' and self.game.player.carrying:
700 return fail('already carrying something')
701 if mode_name == 'drop_thing' and not self.game.player.carrying:
702 return fail('not carrying anything droppable')
703 if mode_name == 'enter_hat' and not hasattr(self.game.player, 'hat'):
704 return fail('not wearing hat to edit', 'edit')
705 if mode_name == 'admin_enter' and self.is_admin:
707 self.mode = getattr(self, 'mode_' + mode_name)
708 if self.mode.name in {'control_tile_draw', 'control_tile_type',
710 self.map_mode = 'protections'
711 elif self.mode.name != 'edit':
712 self.map_mode = 'terrain + things'
713 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
714 self.explorer = YX(self.game.player.position.y,
715 self.game.player.position.x)
716 if self.mode.is_single_char_entry:
717 self.show_help = True
718 if len(self.mode.intro_msg) > 0:
719 self.log_msg(self.mode.intro_msg)
720 if self.mode.name == 'login':
722 self.send('LOGIN ' + quote(self.login_name))
724 self.log_msg('@ enter username')
725 elif self.mode.name == 'take_thing':
726 self.log_msg('Portable things in reach for pick-up:')
728 'HERE': YX(0, 0), 'LEFT': YX(0, -1), 'RIGHT': YX(0, 1)
730 if type(self.game.map_geometry) == MapGeometrySquare:
731 directed_moves['UP'] = YX(-1, 0)
732 directed_moves['DOWN'] = YX(1, 0)
733 elif type(self.game.map_geometry) == MapGeometryHex:
734 if self.game.player.position.y % 2:
735 directed_moves['UPLEFT'] = YX(-1, 0)
736 directed_moves['UPRIGHT'] = YX(-1, 1)
737 directed_moves['DOWNLEFT'] = YX(1, 0)
738 directed_moves['DOWNRIGHT'] = YX(1, 1)
740 directed_moves['UPLEFT'] = YX(-1, -1)
741 directed_moves['UPRIGHT'] = YX(-1, 0)
742 directed_moves['DOWNLEFT'] = YX(1, -1)
743 directed_moves['DOWNRIGHT'] = YX(1, 0)
745 for direction in directed_moves:
746 move = directed_moves[direction]
747 select_range[direction] = self.game.player.position + move
748 self.selectables = []
750 for direction in select_range:
751 for t in [t for t in self.game.things
752 if t.portable and t.position == select_range[direction]]:
753 self.selectables += [t.id_]
754 directions += [direction]
755 if len(self.selectables) == 0:
756 return fail('nothing to pick-up')
758 for i in range(len(self.selectables)):
759 t = self.game.get_thing(self.selectables[i])
760 self.log_msg('%s %s: %s' % (i, directions[i],
761 self.get_thing_info(t)))
762 elif self.mode.name == 'drop_thing':
763 self.log_msg('Direction to drop thing to:')
765 ['HERE'] + list(self.game.tui.movement_keys.values())
766 for i in range(len(self.selectables)):
767 self.log_msg(str(i) + ': ' + self.selectables[i])
768 elif self.mode.name == 'enter_hat':
769 self.log_msg('legal characters: ' + self.game.players_hat_chars)
770 elif self.mode.name == 'command_thing':
771 self.send('TASK:COMMAND ' + quote('HELP'))
772 elif self.mode.name == 'control_pw_pw':
773 self.log_msg('@ enter protection password for "%s":' % self.tile_control_char)
774 elif self.mode.name == 'control_tile_draw':
775 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']))
777 self.restore_input_values()
779 def set_default_colors(self):
780 curses.init_color(1, 1000, 1000, 1000)
781 curses.init_color(2, 0, 0, 0)
782 self.do_refresh = True
784 def set_random_colors(self):
788 return int(offset + random.random()*375)
790 curses.init_color(1, rand(625), rand(625), rand(625))
791 curses.init_color(2, rand(0), rand(0), rand(0))
792 self.do_refresh = True
796 return self.info_cached
797 pos_i = self.explorer.y * self.game.map_geometry.size.x + self.explorer.x
799 if len(self.game.fov) > pos_i and self.game.fov[pos_i] != '.':
800 info_to_cache += 'outside field of view'
802 for t in self.game.things:
803 if t.position == self.explorer:
804 info_to_cache += 'THING: %s' % self.get_thing_info(t)
805 protection = t.protection
806 if protection == '.':
808 info_to_cache += ' / protection: %s\n' % protection
809 if hasattr(t, 'hat'):
810 info_to_cache += t.hat[0:6] + '\n'
811 info_to_cache += t.hat[6:12] + '\n'
812 info_to_cache += t.hat[12:18] + '\n'
813 if hasattr(t, 'face'):
814 info_to_cache += t.face[0:6] + '\n'
815 info_to_cache += t.face[6:12] + '\n'
816 info_to_cache += t.face[12:18] + '\n'
817 terrain_char = self.game.map_content[pos_i]
819 if terrain_char in self.game.terrains:
820 terrain_desc = self.game.terrains[terrain_char]
821 info_to_cache += 'TERRAIN: "%s" / %s\n' % (terrain_char,
823 protection = self.game.map_control_content[pos_i]
824 if protection == '.':
825 protection = 'unprotected'
826 info_to_cache += 'PROTECTION: %s\n' % protection
827 if self.explorer in self.game.portals:
828 info_to_cache += 'PORTAL: ' +\
829 self.game.portals[self.explorer] + '\n'
831 info_to_cache += 'PORTAL: (none)\n'
832 if self.explorer in self.game.annotations:
833 info_to_cache += 'ANNOTATION: ' +\
834 self.game.annotations[self.explorer]
835 self.info_cached = info_to_cache
836 return self.info_cached
838 def get_thing_info(self, t):
840 (t.type_, self.game.thing_types[t.type_])
841 if hasattr(t, 'thing_char'):
843 if hasattr(t, 'name'):
844 info += ' (%s)' % t.name
845 if hasattr(t, 'installed'):
846 info += ' / installed'
849 def loop(self, stdscr):
852 def safe_addstr(y, x, line):
853 if y < self.size.y - 1 or x + len(line) < self.size.x:
854 stdscr.addstr(y, x, line, curses.color_pair(1))
855 else: # workaround to <https://stackoverflow.com/q/7063128>
856 cut_i = self.size.x - x - 1
858 last_char = line[cut_i]
859 stdscr.addstr(y, self.size.x - 2, last_char, curses.color_pair(1))
860 stdscr.insstr(y, self.size.x - 2, ' ')
861 stdscr.addstr(y, x, cut, curses.color_pair(1))
863 def handle_input(msg):
864 command, args = self.parser.parse(msg)
867 def task_action_on(action):
868 return action_tasks[action] in self.game.tasks
870 def msg_into_lines_of_width(msg, width):
874 for i in range(len(msg)):
875 if x >= width or msg[i] == "\n":
887 def reset_screen_size():
888 self.size = YX(*stdscr.getmaxyx())
889 self.size = self.size - YX(self.size.y % 4, 0)
890 self.size = self.size - YX(0, self.size.x % 4)
891 self.window_width = int(self.size.x / 2)
893 def recalc_input_lines():
894 if not self.mode.has_input_prompt:
895 self.input_lines = []
897 self.input_lines = msg_into_lines_of_width(input_prompt
901 def move_explorer(direction):
902 target = self.game.map_geometry.move_yx(self.explorer, direction)
904 self.info_cached = None
905 self.explorer = target
907 self.send_tile_control_command()
913 for line in self.log:
914 lines += msg_into_lines_of_width(line, self.window_width)
917 max_y = self.size.y - len(self.input_lines)
918 for i in range(len(lines)):
919 if (i >= max_y - height_header):
921 safe_addstr(max_y - i - 1, self.window_width, lines[i])
924 info = 'MAP VIEW: %s\n%s' % (self.map_mode, self.get_info())
925 lines = msg_into_lines_of_width(info, self.window_width)
927 for i in range(len(lines)):
928 y = height_header + i
929 if y >= self.size.y - len(self.input_lines):
931 safe_addstr(y, self.window_width, lines[i])
934 y = self.size.y - len(self.input_lines)
935 for i in range(len(self.input_lines)):
936 safe_addstr(y, self.window_width, self.input_lines[i])
940 if not self.game.turn_complete:
942 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
945 help = "hit [%s] for help" % self.keys['help']
946 if self.mode.has_input_prompt:
947 help = "enter /help for help"
948 safe_addstr(1, self.window_width,
949 'MODE: %s – %s' % (self.mode.short_desc, help))
952 if (not self.game.turn_complete) and len(self.map_lines) == 0:
954 if self.game.turn_complete:
956 for y in range(self.game.map_geometry.size.y):
957 start = self.game.map_geometry.size.x * y
958 end = start + self.game.map_geometry.size.x
959 if self.map_mode == 'protections':
960 map_lines_split += [[c + ' ' for c
961 in self.game.map_control_content[start:end]]]
963 map_lines_split += [[c + ' ' for c
964 in self.game.map_content[start:end]]]
965 if self.map_mode == 'terrain + annotations':
966 for p in self.game.annotations:
967 map_lines_split[p.y][p.x] = 'A '
968 elif self.map_mode == 'terrain + things':
969 for p in self.game.portals.keys():
970 original = map_lines_split[p.y][p.x]
971 map_lines_split[p.y][p.x] = original[0] + 'P'
974 def draw_thing(t, used_positions):
975 symbol = self.game.thing_types[t.type_]
977 if hasattr(t, 'thing_char'):
978 meta_char = t.thing_char
979 if t.position in used_positions:
981 if hasattr(t, 'carrying') and t.carrying:
983 map_lines_split[t.position.y][t.position.x] = symbol + meta_char
984 used_positions += [t.position]
986 for t in [t for t in self.game.things if t.type_ != 'Player']:
987 draw_thing(t, used_positions)
988 for t in [t for t in self.game.things if t.type_ == 'Player']:
989 draw_thing(t, used_positions)
990 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
991 map_lines_split[self.explorer.y][self.explorer.x] = '??'
992 elif self.map_mode != 'terrain + things':
993 map_lines_split[self.game.player.position.y]\
994 [self.game.player.position.x] = '??'
996 if type(self.game.map_geometry) == MapGeometryHex:
998 for line in map_lines_split:
999 self.map_lines += [indent * ' ' + ''.join(line)]
1000 indent = 0 if indent else 1
1002 for line in map_lines_split:
1003 self.map_lines += [''.join(line)]
1004 window_center = YX(int(self.size.y / 2),
1005 int(self.window_width / 2))
1006 center = self.game.player.position
1007 if self.mode.shows_info or self.mode.name == 'control_tile_draw':
1008 center = self.explorer
1009 center = YX(center.y, center.x * 2)
1010 self.offset = center - window_center
1011 if type(self.game.map_geometry) == MapGeometryHex and self.offset.y % 2:
1012 self.offset += YX(0, 1)
1013 term_y = max(0, -self.offset.y)
1014 term_x = max(0, -self.offset.x)
1015 map_y = max(0, self.offset.y)
1016 map_x = max(0, self.offset.x)
1017 while term_y < self.size.y and map_y < len(self.map_lines):
1018 to_draw = self.map_lines[map_y][map_x:self.window_width + self.offset.x]
1019 safe_addstr(term_y, term_x, to_draw)
1023 def draw_face_popup():
1024 t = self.game.get_thing(self.draw_face)
1025 if not t or not hasattr(t, 'face'):
1026 self.draw_face = False
1029 start_x = self.window_width - 10
1031 if hasattr(t, 'thing_char'):
1032 t_char = t.thing_char
1033 def draw_body_part(body_part, end_y):
1034 safe_addstr(end_y - 4, start_x, ' _[ @' + t_char + ' ]_ ')
1035 safe_addstr(end_y - 3, start_x, '| |')
1036 safe_addstr(end_y - 2, start_x, '| ' + body_part[0:6] + ' |')
1037 safe_addstr(end_y - 1, start_x, '| ' + body_part[6:12] + ' |')
1038 safe_addstr(end_y, start_x, '| ' + body_part[12:18] + ' |')
1040 if hasattr(t, 'face'):
1041 draw_body_part(t.face, self.size.y - 2)
1042 if hasattr(t, 'hat'):
1043 draw_body_part(t.hat, self.size.y - 5)
1044 safe_addstr(self.size.y - 1, start_x, '| |')
1047 content = "%s help\n\n%s\n\n" % (self.mode.short_desc,
1048 self.mode.help_intro)
1049 if len(self.mode.available_actions) > 0:
1050 content += "Available actions:\n"
1051 for action in self.mode.available_actions:
1052 if action in action_tasks:
1053 if action_tasks[action] not in self.game.tasks:
1055 if action == 'move_explorer':
1057 if action == 'move':
1058 key = ','.join(self.movement_keys)
1060 key = self.keys[action]
1061 content += '[%s] – %s\n' % (key, action_descriptions[action])
1063 content += self.mode.list_available_modes(self)
1064 for i in range(self.size.y):
1066 self.window_width * (not self.mode.has_input_prompt),
1067 ' ' * self.window_width)
1069 for line in content.split('\n'):
1070 lines += msg_into_lines_of_width(line, self.window_width)
1071 for i in range(len(lines)):
1072 if i >= self.size.y:
1075 self.window_width * (not self.mode.has_input_prompt),
1080 stdscr.bkgd(' ', curses.color_pair(1))
1081 recalc_input_lines()
1082 if self.mode.has_input_prompt:
1084 if self.mode.shows_info:
1089 if not self.mode.is_intro:
1094 if self.draw_face and self.mode.name in {'chat', 'play'}:
1097 def pick_selectable(task_name):
1099 i = int(self.input_)
1100 if i < 0 or i >= len(self.selectables):
1101 self.log_msg('? invalid index, aborted')
1103 self.send('TASK:%s %s' % (task_name, self.selectables[i]))
1105 self.log_msg('? invalid index, aborted')
1107 self.switch_mode('play')
1109 def enter_ascii_art(command):
1110 if len(self.input_) != 6:
1111 self.log_msg('? wrong input length, must be 6; try again')
1113 self.log_msg(' ' + self.input_)
1114 self.full_ascii_draw += self.input_
1115 self.ascii_draw_stage += 1
1116 if self.ascii_draw_stage < 3:
1117 self.restore_input_values()
1119 self.send('%s %s' % (command, quote(self.full_ascii_draw)))
1120 self.full_ascii_draw = ""
1121 self.ascii_draw_stage = 0
1123 self.switch_mode('edit')
1125 action_descriptions = {
1127 'flatten': 'flatten surroundings',
1128 'teleport': 'teleport',
1129 'take_thing': 'pick up thing',
1130 'drop_thing': 'drop thing',
1131 'toggle_map_mode': 'toggle map view',
1132 'toggle_tile_draw': 'toggle protection character drawing',
1133 'install': '(un-)install',
1134 'wear': '(un-)wear',
1135 'door': 'open/close',
1136 'consume': 'consume',
1141 'flatten': 'FLATTEN_SURROUNDINGS',
1142 'take_thing': 'PICK_UP',
1143 'drop_thing': 'DROP',
1145 'install': 'INSTALL',
1148 'command': 'COMMAND',
1149 'consume': 'INTOXICATE',
1153 curses.curs_set(False) # hide cursor
1154 curses.start_color()
1155 self.set_default_colors()
1156 curses.init_pair(1, 1, 2)
1159 self.explorer = YX(0, 0)
1162 interval = datetime.timedelta(seconds=5)
1163 last_ping = datetime.datetime.now() - interval
1165 if self.disconnected and self.force_instant_connect:
1166 self.force_instant_connect = False
1168 now = datetime.datetime.now()
1169 if now - last_ping > interval:
1170 if self.disconnected:
1180 self.do_refresh = False
1183 msg = self.queue.get(block=False)
1188 key = stdscr.getkey()
1189 self.do_refresh = True
1190 except curses.error:
1195 self.show_help = False
1196 self.draw_face = False
1197 if key == 'KEY_RESIZE':
1199 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
1200 self.input_ = self.input_[:-1]
1201 elif (((not self.mode.is_intro) and keycode == 27) # Escape
1202 or (self.mode.has_input_prompt and key == '\n'
1203 and self.input_ == ''\
1204 and self.mode.name in {'chat', 'command_thing',
1205 'take_thing', 'drop_thing',
1207 if self.mode.name not in {'chat', 'play', 'study', 'edit'}:
1208 self.log_msg('@ aborted')
1209 self.switch_mode('play')
1210 elif self.mode.has_input_prompt and key == '\n' and self.input_ == '/help':
1211 self.show_help = True
1213 self.restore_input_values()
1214 elif self.mode.has_input_prompt and key != '\n': # Return key
1216 max_length = self.window_width * self.size.y - len(input_prompt) - 1
1217 if len(self.input_) > max_length:
1218 self.input_ = self.input_[:max_length]
1219 elif key == self.keys['help'] and not self.mode.is_single_char_entry:
1220 self.show_help = True
1221 elif self.mode.name == 'login' and key == '\n':
1222 self.login_name = self.input_
1223 self.send('LOGIN ' + quote(self.input_))
1225 elif self.mode.name == 'enter_face' and key == '\n':
1226 enter_ascii_art('PLAYER_FACE')
1227 elif self.mode.name == 'enter_hat' and key == '\n':
1228 enter_ascii_art('PLAYER_HAT')
1229 elif self.mode.name == 'take_thing' and key == '\n':
1230 pick_selectable('PICK_UP')
1231 elif self.mode.name == 'drop_thing' and key == '\n':
1232 pick_selectable('DROP')
1233 elif self.mode.name == 'command_thing' and key == '\n':
1234 self.send('TASK:COMMAND ' + quote(self.input_))
1236 elif self.mode.name == 'control_pw_pw' and key == '\n':
1237 if self.input_ == '':
1238 self.log_msg('@ aborted')
1240 self.send('SET_MAP_CONTROL_PASSWORD ' + quote(self.tile_control_char) + ' ' + quote(self.input_))
1241 self.log_msg('@ sent new password for protection character "%s"' % self.tile_control_char)
1242 self.switch_mode('admin')
1243 elif self.mode.name == 'password' and key == '\n':
1244 if self.input_ == '':
1246 self.password = self.input_
1247 self.switch_mode('edit')
1248 elif self.mode.name == 'admin_enter' and key == '\n':
1249 self.send('BECOME_ADMIN ' + quote(self.input_))
1250 self.switch_mode('play')
1251 elif self.mode.name == 'control_pw_type' and key == '\n':
1252 if len(self.input_) != 1:
1253 self.log_msg('@ entered non-single-char, therefore aborted')
1254 self.switch_mode('admin')
1256 self.tile_control_char = self.input_
1257 self.switch_mode('control_pw_pw')
1258 elif self.mode.name == 'admin_thing_protect' and key == '\n':
1259 if len(self.input_) != 1:
1260 self.log_msg('@ entered non-single-char, therefore aborted')
1262 self.send('THING_PROTECTION %s' % (quote(self.input_)))
1263 self.log_msg('@ sent new protection character for thing')
1264 self.switch_mode('admin')
1265 elif self.mode.name == 'control_tile_type' and key == '\n':
1266 if len(self.input_) != 1:
1267 self.log_msg('@ entered non-single-char, therefore aborted')
1268 self.switch_mode('admin')
1270 self.tile_control_char = self.input_
1271 self.switch_mode('control_tile_draw')
1272 elif self.mode.name == 'chat' and key == '\n':
1273 if self.input_ == '':
1275 if self.input_[0] == '/':
1276 if self.input_.startswith('/nick'):
1277 tokens = self.input_.split(maxsplit=1)
1278 if len(tokens) == 2:
1279 self.send('NICK ' + quote(tokens[1]))
1281 self.log_msg('? need login name')
1283 self.log_msg('? unknown command')
1285 self.send('ALL ' + quote(self.input_))
1287 elif self.mode.name == 'name_thing' and key == '\n':
1288 if self.input_ == '':
1290 self.send('THING_NAME %s %s' % (quote(self.input_),
1291 quote(self.password)))
1292 self.switch_mode('edit')
1293 elif self.mode.name == 'annotate' and key == '\n':
1294 if self.input_ == '':
1296 self.send('ANNOTATE %s %s %s' % (self.explorer, quote(self.input_),
1297 quote(self.password)))
1298 self.switch_mode('edit')
1299 elif self.mode.name == 'portal' and key == '\n':
1300 if self.input_ == '':
1302 self.send('PORTAL %s %s %s' % (self.explorer, quote(self.input_),
1303 quote(self.password)))
1304 self.switch_mode('edit')
1305 elif self.mode.name == 'study':
1306 if self.mode.mode_switch_on_key(self, key):
1308 elif key == self.keys['toggle_map_mode']:
1309 self.toggle_map_mode()
1310 elif key in self.movement_keys:
1311 move_explorer(self.movement_keys[key])
1312 elif self.mode.name == 'play':
1313 if self.mode.mode_switch_on_key(self, key):
1315 elif key == self.keys['door'] and task_action_on('door'):
1316 self.send('TASK:DOOR')
1317 elif key == self.keys['consume'] and task_action_on('consume'):
1318 self.send('TASK:INTOXICATE')
1319 elif key == self.keys['wear'] and task_action_on('wear'):
1320 self.send('TASK:WEAR')
1321 elif key == self.keys['spin'] and task_action_on('spin'):
1322 self.send('TASK:SPIN')
1323 elif key == self.keys['teleport']:
1324 if self.game.player.position in self.game.portals:
1325 self.host = self.game.portals[self.game.player.position]
1329 self.log_msg('? not standing on portal')
1330 elif key in self.movement_keys and task_action_on('move'):
1331 self.send('TASK:MOVE ' + self.movement_keys[key])
1332 elif self.mode.name == 'write':
1333 self.send('TASK:WRITE %s %s' % (key, quote(self.password)))
1334 self.switch_mode('edit')
1335 elif self.mode.name == 'control_tile_draw':
1336 if self.mode.mode_switch_on_key(self, key):
1338 elif key in self.movement_keys:
1339 move_explorer(self.movement_keys[key])
1340 elif key == self.keys['toggle_tile_draw']:
1341 self.tile_draw = False if self.tile_draw else True
1342 elif self.mode.name == 'admin':
1343 if self.mode.mode_switch_on_key(self, key):
1345 elif key in self.movement_keys and task_action_on('move'):
1346 self.send('TASK:MOVE ' + self.movement_keys[key])
1347 elif self.mode.name == 'edit':
1348 if self.mode.mode_switch_on_key(self, key):
1350 elif key == self.keys['flatten'] and task_action_on('flatten'):
1351 self.send('TASK:FLATTEN_SURROUNDINGS ' + quote(self.password))
1352 elif key == self.keys['install'] and task_action_on('install'):
1353 self.send('TASK:INSTALL %s' % quote(self.password))
1354 elif key == self.keys['toggle_map_mode']:
1355 self.toggle_map_mode()
1356 elif key in self.movement_keys and task_action_on('move'):
1357 self.send('TASK:MOVE ' + self.movement_keys[key])
1359 if len(sys.argv) != 2:
1360 raise ArgError('wrong number of arguments, need game host')