571 lines
22 KiB
Python
Executable file
571 lines
22 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.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 <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:
|
|
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:
|
|
if "items" not in cur_room and "interactables" not in cur_room:
|
|
print("> I don't see anything to examine here...")
|
|
return
|
|
no_items = False
|
|
if "items" in cur_room:
|
|
if len(cur_room["items"]) == 0:
|
|
no_items = True
|
|
no_interactables = False
|
|
if "interactables" in cur_room:
|
|
if len(cur_room["interactables"]) == 0:
|
|
no_interactables = True
|
|
if no_items and no_interactables:
|
|
print("> I don't see anything to examine here...")
|
|
if 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...")
|
|
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 <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
|
|
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 <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}.")
|
|
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.append((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_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 "version" in cred:
|
|
print(f"\tVersion: {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 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()
|