setage/setage.py

706 lines
27 KiB
Python
Executable file

#!/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.1.1"
__maintainer__ = "Cheri Dawn"
__status__ = "Prototype"
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 <thing>"""
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 <thing>"""
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 <thing>"""
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...")
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 <thing>"""
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 <room>"""
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 <item> from the current room."""
global current_room_id
cur_room = rooms[current_room_id]
if arg == "":
print("> What should I take?")
return
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:
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 __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.")
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()