setage/setage.py

544 lines
20 KiB
Python
Raw Normal View History

2024-05-21 04:59:26 -04:00
#!/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"
2024-05-21 05:09:29 -04:00
__license__ = "GPL-3.0-only"
__version__ = "0.3.0"
2024-05-21 04:59:26 -04:00
__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`.")
2024-05-21 04:59:26 -04:00
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 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...")
2024-05-21 04:59:26 -04:00
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"])
2024-05-21 04:59:26 -04:00
items = dict(inventory)
items.update(rooms[current_room_id]["items"])
items.update(rooms[current_room_id]["interactables"])
2024-05-21 04:59:26 -04:00
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
2024-05-21 04:59:26 -04:00
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 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
2024-05-21 04:59:26 -04:00
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):
2024-05-21 04:59:26 -04:00
print("FATAL: "
"`endings` is not a table in scenario.")
2024-05-21 04:59:26 -04:00
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
2024-05-21 04:59:26 -04:00
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})
2024-05-21 04:59:26 -04:00
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()