From 05c000990afb5463542b056f1500504e18ec2103 Mon Sep 17 00:00:00 2001 From: Cheri Dawn Date: Tue, 21 May 2024 11:59:26 +0300 Subject: [PATCH] Ready. Set. Go! --- README.md | 8 + docs.toml | 57 ++++ setage-standalone-gen.py | 43 +++ setage-standalone.py.template | 489 +++++++++++++++++++++++++++++++++ setage.py | 501 ++++++++++++++++++++++++++++++++++ test-game/manifest.setage | 92 +++++++ 6 files changed, 1190 insertions(+) create mode 100644 README.md create mode 100644 docs.toml create mode 100644 setage-standalone-gen.py create mode 100644 setage-standalone.py.template create mode 100755 setage.py create mode 100644 test-game/manifest.setage diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e7d4d9 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# SETAGE +A **S**imple **E**xtensible **T**ext **A**dventure **G**ame **E**ngine. + +## Manifest +see docs.toml + +## Example +see test-game/manifest.setage diff --git a/docs.toml b/docs.toml new file mode 100644 index 0000000..09e31e6 --- /dev/null +++ b/docs.toml @@ -0,0 +1,57 @@ +[credits] # Optional but highly recommended. +author = "who made the manifest for the game" +year = "when it was made" +license = "license for this manifest" + + +[scenario] # Required. +start = "id" # room where the player starts. any valid room id. +title = "title of the game" +intro = "intro to the game" +prompt = "> " # optional, default shown. +playtested = false # optional, default. displays a warning if this is set to false. + +[scenario.win] # Required. +trigger = "item" # one of "item", "room" ("interactable" in future releases). +target = "id of the trigger" # which item, room (or interactable) should trigger the win. +message = "message when the win is triggered" +end = false # optional, default; exit the game after triggering? + + +[items] # Required. +item.examine = "message when the `item` is examined" # `item` can be any item id. +item.hidden = false # optional, default. + + +[rooms.id] # Required, id can be any other room id. +description = "description of the room as it appears in the listing of exits from other rooms" +examine = "description of the room when you examine it" +go = "message when you `go` to this room" + +[rooms.id.items] # Optional. +item = "description" # item can be any valid id. description is a short description as it appears in the room listing. + +[rooms.id.interactables.id] # Optional. Any valid id. +name = "name of the interactable" # appears in the room listing. +type = "bare" # optional, default; options: "bare". +times = "one" # optional, default; options: "one", "change", "remove", "many". +hidden = false # optional, default. +action = "add exit" # any of "add exit", "add exits", "add item", "add items", "reveal interactable". +target_room = "id" # any valid room id; changes will be made to that room. +target_exit.name = "id" # Required if action is "add exit"; name of the exit as it appears in the room listing & id of the room it points to. +# Required if action is "add items"; pick one of the following two: +target_exits = [ # list of inline tables. +{name = "id"}, # similar to a regular exit decl. +] +target_exits.name = "id" # another way to do that. +target_item = {"id" = "description"} # Required if action is "add item"; any valid item id & short description as it appears in the room listing. +# Required if action is "add items"; pick one of the following two: +target_items = [ # list of inline tables. +{id = "description"}, # similar to a regular item decl. +] +target_items.id = "description" # another way to do that. + +message = "message after the interactable is activated" # Required. + +[rooms.id.exits] # Technically optional. +direction = "id" # direction as it appears in the listing. \ No newline at end of file diff --git a/setage-standalone-gen.py b/setage-standalone-gen.py new file mode 100644 index 0000000..8733a46 --- /dev/null +++ b/setage-standalone-gen.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from argparse import ArgumentParser, FileType +from base64 import b64encode + +__author__ = "Cheri Dawn" +__copyright__ = "Copyright 2024, Cheri Dawn" +__license__ = "GPLv3" +__version__ = "0.2.0" +__maintainer__ = "Cheri Dawn" +__status__ = "Prototype" + +if __name__ == "__main__": + 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 generate a script for.") + parser.add_argument("template", nargs="?", + default="setage-standalone.py.template", + type=FileType("r"), + help="The template for the script.") + parser.add_argument("output", nargs="?", default="setage-standalone.py", + type=FileType("w"), + help="The output file.") + + args = parser.parse_args() + print(f"Reading manifest '{args.file.name}'...") + data = args.file.read() + print(f"Read manifest '{args.file.name}'.") + + print(f"Reading template '{args.template.name}'...") + template = args.template.read() + print(f"Read template '{args.template.name}'.") + + out = template.replace("{{data}}", + b64encode(data.encode("utf-8")).decode("utf-8")) + + print(f"Writing output '{args.output.name}'...") + output = args.output.write(out) + print(f"Wrote output '{args.output.name}'.") diff --git a/setage-standalone.py.template b/setage-standalone.py.template new file mode 100644 index 0000000..f9e336f --- /dev/null +++ b/setage-standalone.py.template @@ -0,0 +1,489 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +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__ = "GPLv3" +__version__ = "0.2.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}' 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...") + 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"] + items = dict(inventory) + items.update(rooms[current_room_id]["items"]) + 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.""" + win = scenario["win"] + if win["trigger"] == "item": + if win["target"] in dict(inventory).keys(): + print(win["message"]) + if win["end"]: + return True + if win["trigger"] == "room": + if current_room_id == win["target"]: + print(win["message"]) + if win["end"]: + 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") + + data = "{{data}}" # noqa: E501 + data = b64decode(data.encode("utf-8")).decode("utf-8") + + try: + manifest = toml_loads(data) + except (TOMLDecodeError) as e: + ec = e.__class__ + fatal = "FATAL: embedded manifest" + 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 "items" in manifest: + for k, v in manifest["items"].items(): + if "hidden" not in v: + v.update({"hidden": False}) + manifest["items"][k] = v + + 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 "win" not in manifest["scenario"]: + print("FATAL: " + "table `win` does not exist in scenario.") + exit(2) + + if "end" not in manifest["scenario"]["win"]: + manifest["scenario"]["win"]["end"] = False + + for k, v in manifest["rooms"].items(): + if "exits" not in v: + v.update({"exits": dict()}) + + 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() diff --git a/setage.py b/setage.py new file mode 100755 index 0000000..a66542f --- /dev/null +++ b/setage.py @@ -0,0 +1,501 @@ +#!/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__ = "GPLv3" +__version__ = "0.2.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}' 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...") + 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"] + items = dict(inventory) + items.update(rooms[current_room_id]["items"]) + 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.""" + win = scenario["win"] + if win["trigger"] == "item": + if win["target"] in dict(inventory).keys(): + print(win["message"]) + if win["end"]: + return True + if win["trigger"] == "room": + if current_room_id == win["target"]: + print(win["message"]) + if win["end"]: + 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 "items" in manifest: + for k, v in manifest["items"].items(): + if "hidden" not in v: + v.update({"hidden": False}) + manifest["items"][k] = v + + 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 "win" not in manifest["scenario"]: + print("FATAL: " + "table `win` does not exist in scenario.") + exit(2) + + if "end" not in manifest["scenario"]["win"]: + manifest["scenario"]["win"]["end"] = False + + for k, v in manifest["rooms"].items(): + if "exits" not in v: + v.update({"exits": dict()}) + + 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() diff --git a/test-game/manifest.setage b/test-game/manifest.setage new file mode 100644 index 0000000..ef37366 --- /dev/null +++ b/test-game/manifest.setage @@ -0,0 +1,92 @@ +[credits] +author = "Cheri Dawn" +year = "2024" +license = "CC0" + + +[scenario] +start = "1" +title = "Dust Bunnies" +intro = "You wake up in a very dusty and old-looking room.\nYou think you can get out of here if you find something that could help you remember..." +prompt = ": " + +[scenario.win] +trigger = "item" +target = "shiny" +message = "Congrats! You have found the old locket of your grandma." + + +[items] +brush.examine = "A quite uninteresting toothbrush." +detergent.examine = "A bottle of detergent. Half-filled. Stinks." +shiny.examine = "A heart-shaped shiny locket with a picture of a horse inside." +shiny.hidden = true +note.examine = "The note reads: there is a [shiny] hidden in one of the rooms of this house." + + +[rooms.1] +description = "a dark room with trash in it." +go = "You walk into a room full of trash, garbage and other junk." +examine = "This room doesn't have much to it, just a lot of dust, old wood trash and a couple torn paintings. This room stinks!" + +[rooms.1.items] + +[rooms.1.interactables.lever] +name = "a rustic lever" +type = "bare" +times = "one" +hidden = false +action = "add exit" +target_room = "1" +target_exit.down = "4" +message = "Pulling the lever reveals a staircase downwards." + +[rooms.1.exits] +north = "1" +south = "2" + + +[rooms.2] +description = "a bright white shining room." +go = "You walk into a room that is perfectly clean." +examine = "This room has everything neatly organized. Not a spec of dust. This room smells of caustic chemicals." + +[rooms.2.interactables.cupboard] +name = "a shiny white cupboard" +times = "remove" +action = "add item" +target_item.id = "detergent" +target_item.description = "a half-filled bottle of laundry detergent" +message = "You open a cupboard, there is something there." + +[rooms.2.items] +brush = "a new toothbrush" + +[rooms.2.exits] +north = "1" +west = "3" + + +[rooms.3] +description = "a room lit by a single candle." +go = "You walk into a room lit by a single candle." +examine = "The candle illuminates the room very poorly, only a table and two chairs are visible." + +[rooms.3.items] +shiny = "a shiny heart-shaped locket" + +[rooms.3.exits] +north = "1" +east = "2" + + +[rooms.4] +description = "a room with yellowed wallpapers." +go = "You walk into a room with yellowed wallpapers." +examine = "The room has yellowed wallpapers. They stink of old age. There's a faint hum coming from the flourescent lights." + +[rooms.4.items] +note = "a torn, yellowed note" + +[rooms.4.exits] +up = "1" \ No newline at end of file