#!/usr/bin/env python3 # -*- coding: utf-8 -*- from argparse import ArgumentParser, FileType from sys import version_info from tomllib import loads as toml_loads from tomllib import TOMLDecodeError from cmd import Cmd from base64 import b64decode __author__ = "Cheri Dawn" __copyright__ = "Copyright 2024, Cheri Dawn" __license__ = "GPL-3.0-only" __version__ = "0.3.0" __maintainer__ = "Cheri Dawn" __status__ = "Prototype" manifest = {} scenario = {} rooms = {} current_room_id = "" inventory = [] def print_items(cur_room): if "items" not in cur_room: return items = cur_room["items"] for k, v in items.items(): if not manifest["items"][k]["hidden"]: print(f"You notice {v} [{k}].") def print_interactables(cur_room): if "interactables" not in cur_room: return inters = cur_room["interactables"] if len(inters) == 0: return for inter in inters: inter_dict = cur_room["interactables"][inter] if "hidden" not in inter_dict: if "name" not in inter_dict: print("FATAL: " "`name` doesn't exist " f"in interactable '{inter}'.") exit(2) print(f"You see {inter_dict["name"]} [{inter}].") elif not inter_dict["hidden"]: print(f"You see {inter_dict["name"]} [{inter}].") def print_exits(cur_room): if "exits" not in cur_room: return exits = cur_room["exits"] for k, v in exits.items(): if v not in rooms: print("FATAL: " f"room '{v}' does not exist.") exit(2) if "description" not in rooms[v]: print("WARNING: " f"room '{v}' ({k}) does not have a `description`.") continue print(f"Looking [{k}] you see {rooms[v]["description"]}") class SetageShell(Cmd): intro = "" prompt = "> " ruler = "~" def cmdloop(self, intro=None): print(self.intro) while True: try: super().cmdloop(intro="") break except KeyboardInterrupt: print("^C") def emptyline(self): return def do_EOF(self, arg): """Exit the game (does NOT save state).""" return True def do_exit(self, arg): """Exit the game (does NOT save state).""" return True def default(self, arg): print("> I don't know how to do that...") def do_examine(self, arg): """Examine a """ global current_room_id if current_room_id not in rooms: print("FATAL: " f"room '{current_room_id}' does not exist.") exit(2) cur_room = rooms[current_room_id] if arg == "": print("> What should I examine?") return elif arg in ("room", "this room"): if "examine" not in cur_room: print("This room doesn't seem to be all that interesting...") return print(cur_room["examine"]) print_items(cur_room) print_interactables(cur_room) print_exits(cur_room) elif arg in ("inv", "inventory"): if len(inventory) == 0: print("Your inventory is empty.") for k, v in inventory: print(f"You have {v} [{k}].") elif arg in dict(inventory).keys(): if "examine" in manifest["items"][arg]: print(manifest["items"][arg]["examine"]) else: print("It doesn't seem to be all that interesting...") elif arg in cur_room["items"].keys(): manifest["items"][arg]["hidden"] = False if "examine" in manifest["items"][arg]: print(manifest["items"][arg]["examine"]) else: print("It doesn't seem to be all that interesting...") elif arg in cur_room["interactables"].keys(): cur_room["interactables"][arg]["hidden"] = False if "examine" in cur_room["interactables"][arg]: print(cur_room["interactables"][arg]["examine"]) else: print("It doesn't seem to be all that interesting...") else: print("> I don't see anything like that...") def complete_examine(self, text, line, bidx, eidx): global current_room_id all_items = manifest["items"] all_items.update(rooms[current_room_id]["interactables"]) items = dict(inventory) items.update(rooms[current_room_id]["items"]) items.update(rooms[current_room_id]["interactables"]) items = [x for x in items if (not all_items[x]["hidden"])] if not text: return list(items) else: return [comp for comp in items if comp.startswith(text)] def do_ex(self, arg): """Examine a """ self.do_examine(arg) def complete_ex(self, text, line, bidx, eidx): return self.complete_examine(text, line, bidx, eidx) def do_ls(self, arg): """Examine the current room""" self.do_examine("room") def do_interact(self, arg): """Interact with a """ global current_room_id, manifest cur_room = manifest["rooms"][current_room_id] inters = cur_room["interactables"] if arg in inters.keys(): inter = inters[arg] can_activate = False if "type" not in inter: inter["type"] = "bare" if inter["type"] == "bare": if "interactable" in inter: if inter["interactable"]: can_activate = True else: can_activate = True if can_activate: if "target_room" not in inter: target_room = current_room_id else: target_room = inter["target_room"] if "action" not in inter: print("FATAL: " "`action` doesn't exist " f"in interactable '{arg}'") exit(2) match inter["action"]: case "add exit": if "target_exit" not in inter: print("FATAL: " "table `target_exit` doesn't exist " f"in interactable '{arg}'") exit(2) if not isinstance(inter["target_exit"], dict): print("FATAL: " "`target_exit` is not a table " f"in interactable '{arg}'") if "exits" not in manifest["rooms"][target_room]: manifest["rooms"][target_room]["exits"] = dict() manifest["rooms"][target_room]["exits"].update( inter["target_exit"]) case "add exits": if "target_exits" not in inter: print("FATAL: " "table `target_exits` doesn't exist " f"in interactable '{arg}'") exit(2) if not (isinstance(inter["target_exits"], dict) or isinstance(inter["target_exits"], list)): print("FATAL: " "`target_exits` is not a table or a list " f"in interactable '{arg}'") if "exits" not in manifest["rooms"][target_room]: manifest["rooms"][target_room]["exits"] = dict() if isinstance(inter["target_exits"], dict): manifest["rooms"][target_room]["exits"].update( inter["target_exits"]) elif isinstance(inter["target_exits"], list): for ex in inter["target_exits"]: manifest["rooms"][target_room]["exits"].update( ex) case "add item": if "target_item" not in inter: print("FATAL: " "table `target_item` doesn't exist " f"in interactable '{arg}'") exit(2) if not isinstance(inter["target_item"], dict): print("FATAL: " "`target_item` is not a table " f"interactable '{arg}'") if "items" not in manifest["rooms"][target_room]: manifest["rooms"][target_room]["items"] = dict() manifest["rooms"][target_room]["items"].update( inter["target_item"]) case "add items": if "target_items" not in inter: print("FATAL: " "table `target_items` doesn't exist " f"in interactable '{arg}'") exit(2) if not (isinstance(inter["target_items"], dict) or isinstance(inter["target_items"], list)): print("FATAL: " "`target_items` is not a table or a list " f"in interactable '{arg}'") if "items" not in manifest["rooms"][target_room]: manifest["rooms"][target_room]["items"] = dict() if isinstance(inter["target_items"], dict): manifest["rooms"][target_room]["items"].update( inter["target_items"]) elif isinstance(inter["target_items"], list): for item in inter["target_items"]: manifest["rooms"][target_room]["items"].update( item) case "reveal interactable": if "target_interactable" not in inter: print("FATAL: " "id `target_interactable` doesn't exist " f"in interactable '{arg}'") exit(2) target_interactable = inter["target_interactable"] if not isinstance(target_interactable, str): print("FATAL: " "`target_interactable` is not a string id " f"in interactable '{arg}'") if target_interactable not in \ manifest["rooms"][target_room]["interactables"]: print("FATAL: " f"'{target_interactable}' does not exist " f"in target_room '{target_room}' " f"in interactable '{arg}'") trg = manifest["rooms"][target_room] trg["interactables"][target_interactable]["hidden"] = \ False case _ as a: print(f"ERROR: no such action exists: '{a}'") if "message" not in inter: print("WARNING: no message defined for this action.") else: print(inter["message"]) if "times" not in inter: inter["times"] = "one" if inter["times"] == "one": cur_room = manifest["rooms"][current_room_id] cur_room["interactables"][arg]["interactable"] = False elif inter["times"] == "remove": cur_room = manifest["rooms"][current_room_id] cur_room["interactables"].pop(arg) if not can_activate: print("> I can't interact with that...") elif arg == "": print("> What should I interact with?") else: print("> I don't see anything like that") def complete_interact(self, text, line, bidx, eidx): global current_room_id if "interactables" not in rooms[current_room_id]: return inters = rooms[current_room_id]["interactables"] if not text: return list(inters.keys()) else: return [comp for comp in inters.keys() if comp.startswith(text)] def do_int(self, arg): """Interact with a """ self.do_interact(arg) def complete_int(self, text, line, bidx, eidx): return self.complete_interact(text, line, bidx, eidx) def do_go(self, arg): """Go to another """ global current_room_id cur_room = rooms[current_room_id] if arg == "": print("> Where should I go?") return elif arg in cur_room["exits"].keys(): current_room_id = cur_room["exits"][arg] if "go" in rooms[current_room_id]: print(rooms[current_room_id]["go"]) else: print(f"You went {arg}.") else: print("> I can't go there...") return self.check_win() def complete_go(self, text, line, bidx, eidx): global current_room_id exits = rooms[current_room_id]["exits"] if not text: return list(exits.keys()) else: return [comp for comp in exits.keys() if comp.startswith(text)] def do_take(self, arg): """Take an from the current room.""" global current_room_id cur_room = rooms[current_room_id] if arg == "": print("> What should I take?") return elif arg in cur_room["items"].keys(): inventory.append((arg, cur_room["items"].pop(arg))) print(f"{arg} added to inventory.") else: print("> I don't see anything like that...") return self.check_win() def complete_take(self, text, line, bidx, eidx): global current_room_id all_items = manifest["items"] items = [x for x in rooms[current_room_id]["items"] if (not all_items[x]["hidden"])] if not text: return list(items) else: return [comp for comp in items if comp.startswith(text)] def do_inventory(self, arg): """Examine your inventory.""" self.do_examine("inventory") def do_inv(self, arg): """Check your inventory.""" self.do_inventory(arg) def do_credits(self, arg): """Print credits.""" if "credits" not in manifest: print("No credits :(") return cred = manifest["credits"] print("Manifest credits:") if "author" in cred and "year" in cred: print(f"\tMade by {cred["author"]} in {cred["year"]}.") elif "author" in cred and "year" not in cred: print(f"\tMade by {cred["author"]}.") if "contributors" in cred: print(f"\tWith contributions by {", ".join(cred["contributors"])}") if "license" in cred: print(f"\tLicense: {cred["license"]}") print("\nSETAGE credits:") print(f"\tMade by {__author__}") print(f"\t{__copyright__}") print(f"\tLicense: {__license__}") def check_win(self, arg=""): """Check if you have won.""" endings = scenario["endings"] for ending in endings: end = endings[ending] if end["trigger"] == "item": if end["target"] in dict(inventory).keys(): if "title" in end: print(end["title"]) if "message" not in end: print(f"WARNING: no message for ending '{ending}'") else: print(end["message"]) if "exit" not in end: continue if end["exit"]: return True if end["trigger"] == "room": if current_room_id == end["target"]: if "title" in end: print(end["title"]) if "message" not in end: print(f"WARNING: no message for ending '{ending}'") else: print(end["message"]) if "exit" not in end: continue if end["exit"]: return True if __name__ == "__main__": if version_info < (3, 11): print( f"Sorry, this is only usable with python 3.11 as of {__version__}") exit(1) print("WARNING: It's a prototype. Beware of bugs. Report them") parser = ArgumentParser( description="SETAGE, the Simple Extensible Text Adventure Game Engine") parser.add_argument("file", nargs="?", default="manifest.setage", type=FileType("r"), help="The manifest.setage file to use.") parser.add_argument("--obfuscated", "-b", action="store_true", help="Treat file as an obfuscated file.") args = parser.parse_args() data = args.file.read() if args.obfuscated: data = b64decode(data.encode("utf-8")).decode("utf-8") try: manifest = toml_loads(data) except (TOMLDecodeError) as e: ec = e.__class__ fatal = f"FATAL: file '{args.file.name}'" if ec == TOMLDecodeError: print(f"{fatal} isn't a valid toml file. " "Try running with `-b` flag.") print(f"FATAL: the following error occured: {e}") exit(1) if "scenario" not in manifest: print("FATAL: " "table `scenario` does not exist in manifest.") exit(2) if "rooms" not in manifest: print("FATAL: " "table `rooms` does not exist in manifest.") exit(2) if "title" not in manifest["scenario"]: print("FATAL: " "string `title` does not exist in scenario.") exit(2) if "intro" not in manifest["scenario"]: print("FATAL: " "string `intro` does not exist in scenario.") exit(2) if "start" not in manifest["scenario"]: print("FATAL: " "room id `start` does not exist in scenario.") exit(2) if "endings" not in manifest["scenario"]: print("FATAL: " "table `endings` does not exist in scenario.") exit(2) if not isinstance(manifest["rooms"], dict): print("FATAL: " "`rooms` is not a table in scenario.") exit(2) if not isinstance(manifest["items"], dict): print("FATAL: " "`items` is not a table in scenario.") exit(2) if not isinstance(manifest["scenario"]["endings"], dict): print("FATAL: " "`endings` is not a table in scenario.") exit(2) if "items" in manifest: for k, v in manifest["items"].items(): if "hidden" not in v: v.update({"hidden": False}) manifest["items"][k] = v for k, v in manifest["rooms"].items(): if "exits" not in v: v.update({"exits": dict()}) for room in manifest["rooms"]: if "interactables" in manifest["rooms"][room]: for k, v in manifest["rooms"][room]["interactables"].items(): if "hidden" not in v: v.update({"hidden": False}) scenario = manifest["scenario"] rooms = manifest["rooms"] current_room_id = scenario["start"] SetageShell.intro = scenario["intro"] if "prompt" in scenario: SetageShell.prompt = scenario["prompt"] if "playtested" not in scenario: print("WARNING: This manifest has not been playtested.") elif not scenario["playtested"]: print("WARNING: This manifest has not been playtested.") print() print("? for help. Try ls.") print() print(scenario["title"]) print() SetageShell().cmdloop()