#!/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.4.2.0" __maintainer__ = "Cheri Dawn" __status__ = "Alpha" manifest = {} metadata = {} scenario = {} rooms = {} current_room_id = "" inventory = {} def parse_version(ver): if len(ver) < 1: raise ValueError("empty version string") if ver[0] == "v": ver = ver[1:] try: ver = [int(x) for x in ver.split(".")] except ValueError: raise ValueError("non integer version") return tuple(ver) VERSION = parse_version(__version__) def print_items(cur_room): if "items" not in cur_room: return items = cur_room["items"] for k, v in items.items(): if k not in manifest["items"]: print(f"You notice {v} [{k}].") elif 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 = "~" interacted_with = set() rooms_been_in = set() 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.items(): print(f"You have {v} [{k}].") elif arg in 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: if "items" not in cur_room and "interactables" not in cur_room: print("> I don't see anything to examine here...") return if "items" in cur_room: if arg in cur_room["items"].keys(): manifest["items"][arg]["hidden"] = False if "examine" in manifest["items"][arg]: print(manifest["items"][arg]["examine"]) return else: print("It doesn't seem to be all that interesting...") return if "interactables" in cur_room: if 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"]) return else: print("It doesn't seem to be all that interesting...") return print("> I don't see anything like that...") 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"] if "interactables" in rooms[current_room_id]: all_items.update(rooms[current_room_id]["interactables"]) items = inventory.copy() if "items" in rooms[current_room_id]: items.update(rooms[current_room_id]["items"]) if "interactables" in rooms[current_room_id]: 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] if "interactables" not in cur_room: print("> Nothing to interact with here...") return 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 elif inter["type"] == "item check": if "check" not in inter: print("FATAL: " "`check` id not in interactable with type `item` " f"in interactable '{arg}'") exit(2) if inter["check"] in inventory.keys(): can_activate = True else: can_activate = False elif inter["type"] == "item take": if "check" not in inter: print("FATAL: " "`check` id not in interactable with type `item` " f"in interactable '{arg}'") exit(2) if inter["check"] in inventory.keys(): can_activate = True inventory.pop(inter["check"]) else: can_activate = False elif inter["type"] == "input": if "check" not in inter: print("FATAL: " "`check` string not in interactable " "with type `input` " f"in interactable '{arg}'") exit(2) if "prompt" not in inter: prompt = "> " else: prompt = inter["prompt"] try: i = input(prompt).strip().casefold() except (KeyboardInterrupt, EOFError): print() if i == inter["check"]: can_activate = True elif inter["type"] == "exact input": if "check" not in inter: print("FATAL: " "`check` string not in interactable " "with type `exact input` " f"in interactable '{arg}'") exit(2) if "prompt" not in inter: prompt = "> " else: prompt = inter["prompt"] try: i = input(prompt) except (KeyboardInterrupt, EOFError): print() if i == inter["check"]: can_activate = True else: print("FATAL: " f"invalid `type` '{inter["type"]}' " f"in interactable '{arg}'") if can_activate: self.interacted_with.add(arg) 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) else: if "message_failed" in inter: print(inter["message_failed"]) else: print( "> I don't seem to be able to do anything with that..." ) return self.check_win(arg) 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}.") self.rooms_been_in.add(current_room_id) 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 len(arg.split(",")) > 1: for item in [a.strip() for a in arg.split(",")]: if "items" not in cur_room: print("> I don't see anything that I could take...") return if len(cur_room["items"]) == 0: print("> I don't see anything that I could take...") return if item in cur_room["items"].keys(): inventory.update({item: cur_room["items"].pop(item)}) print(f"{item} added to inventory.") else: print("> I don't see anything like that...") elif arg: if "items" not in cur_room: print("> I don't see anything that I could take...") return if len(cur_room["items"]) == 0: print("> I don't see anything that I could take...") return if arg in cur_room["items"].keys(): inventory.update({arg: cur_room["items"].pop(arg)}) print(f"{arg} added to inventory.") else: print("> I don't see anything like that...") 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: # FIXME: handle multiple items 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_combine(self, arg): """Combine items: combine item1,item2""" recipes = manifest["recipes"] items = [a.strip() for a in arg.split(",")] if len(items) < 2: print("> Not enough items to combine...") return for item in items: if item not in inventory: print(f"> {item} not in inventory...") return for recipe in recipes.items(): rec = recipe[1] can = True if rec["lock"]: can = False if rec["lock"] == "item": if rec["lock_target"] in inventory: can = True elif rec["lock"] == "interactable": if rec["lock_target"] in self.interacted_with: can = True elif rec["lock"] == "room": if rec["lock_target"] in self.rooms_been_in: can = True else: print(f"ERROR: Invalid lock type for recipe `{recipe[0]}`") return True if can: # FIXME: multiple recipes with same ingredients??? if set(items) == set(rec["ingredients"]): print(rec["text"]) for item in items: inventory.pop(item) if "result_ids" in rec: for idx, _id in enumerate(rec["result_ids"]): inventory[_id] = rec["result_texts"][idx] else: inventory[rec["result_id"]] = rec["result_text"] return self.check_win() print("> I can't combine these...") def do_credits(self, arg): """Print credits.""" if "credits" not in metadata: print("No credits :(") return cred = metadata["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 "version" in cred: print(f"\tManifest version: {cred["version"]}") 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 inventory.keys(): if "message" not in end: print(f"WARNING: no message for ending '{ending}'") else: print(end["message"]) if "title" in end: print(end["title"]) if "exit" not in end: continue if end["exit"]: return True if end["trigger"] == "room": if current_room_id == end["target"]: if "message" not in end: print(f"WARNING: no message for ending '{ending}'") else: print(end["message"]) if "title" in end: print(end["title"]) if "exit" not in end: continue if end["exit"]: return True if end["trigger"] == "interactable": if arg == end["target"]: if "message" not in end: print(f"WARNING: no message for ending '{ending}'") else: print(end["message"]) if "title" in end: print(end["title"]) 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 "metadata" not in manifest: print("FATAL: " "table `metadata` does not exist in manifest.") exit(2) if "min_version" not in manifest["metadata"]: print("WARNING: no minimum SETAGE version specified in manifest.") else: if VERSION < parse_version(manifest["metadata"]["min_version"]): print("FATAL: SETAGE version doesn't match required version.") exit(2) if "rooms" not in manifest: print("FATAL: " "table `rooms` does not exist in manifest.") exit(2) if "title" not in manifest["metadata"]: 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 "items" in manifest: 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}) metadata = manifest["metadata"] 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 metadata: print("WARNING: This manifest has not been playtested.") elif not metadata["playtested"]: print("WARNING: This manifest has not been playtested.") print() print("? for help. Try ls.") print() print(metadata["title"]) print() SetageShell().cmdloop()