Ready. Set. Go!
This commit is contained in:
commit
05c000990a
6 changed files with 1190 additions and 0 deletions
8
README.md
Normal file
8
README.md
Normal file
|
@ -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
|
57
docs.toml
Normal file
57
docs.toml
Normal file
|
@ -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.
|
43
setage-standalone-gen.py
Normal file
43
setage-standalone-gen.py
Normal file
|
@ -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}'.")
|
489
setage-standalone.py.template
Normal file
489
setage-standalone.py.template
Normal file
|
@ -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 <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...")
|
||||
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 <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]
|
||||
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."""
|
||||
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()
|
501
setage.py
Executable file
501
setage.py
Executable file
|
@ -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 <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...")
|
||||
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 <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]
|
||||
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."""
|
||||
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()
|
92
test-game/manifest.setage
Normal file
92
test-game/manifest.setage
Normal file
|
@ -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"
|
Loading…
Reference in a new issue