giant freakin documentation and reorganization pass, it also uses cmake because all the building was getting too complicated for shell scripts
This commit is contained in:
@@ -5,6 +5,15 @@ import flask
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief Rolls a random integer between `min` and `max`, and applies all described `modifiers`.
|
||||
# \param min The lowest possible number. Default is 0.
|
||||
# \param max The highest possible number. Default is 100.
|
||||
# \param modifiers
|
||||
# \return The result of the dice roll, after modifiers.
|
||||
#
|
||||
# Dice roll is actually a slight misnomer; the game primarily uses percentages for chance
|
||||
# calculations. However, the recognized term for these chance calculations, "dice rolls",
|
||||
# is D&D old, so is used here for mental ease.
|
||||
def dice_roll(min=0, max=100, modifiers=[]):
|
||||
result = random.randint(min, max)
|
||||
for _, mod in modifiers:
|
||||
@@ -12,12 +21,29 @@ def dice_roll(min=0, max=100, modifiers=[]):
|
||||
|
||||
return result
|
||||
|
||||
def calculate_speed(agl):
|
||||
return 3+(agl * 1.5)
|
||||
|
||||
## \brief Attempts to steal a given resource from a target.
|
||||
# \param resource The resource to steal.
|
||||
# \param target The class of target to steal from.
|
||||
# \api{POST} /act/steal/`<resource>`/`<target>`
|
||||
# \apidata Gamestate.
|
||||
@core.app.route("/act/steal/<resource>/<target>", methods=["POST"])
|
||||
def steal_resource(resource, target):
|
||||
core.log.debug(flask.request.get_data())
|
||||
gamestate = flask.request.get_json()["gamestate"]
|
||||
agl = gamestate["agility"]
|
||||
roll = dice_roll()
|
||||
speed = calculate_speed(agl)
|
||||
result = (roll + speed) + (agl/4)
|
||||
return flask.Response(json.dumps({
|
||||
"success": (dice_roll() >= 50)
|
||||
"success": (result >= 50)
|
||||
}), status=200, content_type="application/json")
|
||||
|
||||
## \brief Attempts to recruit a seagull.
|
||||
# \api{POST} /act/recruit
|
||||
# \apidata Gamestate.
|
||||
@core.app.route("/act/recruit", methods=["POST"])
|
||||
def recruit():
|
||||
return flask.Response(json.dumps({
|
||||
|
@@ -4,6 +4,7 @@ import pathlib
|
||||
import sys
|
||||
|
||||
import flask
|
||||
from flask_cors import CORS
|
||||
|
||||
log = logging.getLogger()
|
||||
pipe_stderr = logging.StreamHandler(sys.stderr)
|
||||
@@ -16,11 +17,16 @@ else:
|
||||
path_appdir = pathlib.Path.cwd()
|
||||
log.critical(path_appdir)
|
||||
|
||||
## \internal
|
||||
# \brief Signals whether we are a desktop application (as opposed to a Docker container).
|
||||
desktop_mode = False
|
||||
|
||||
from . import gamedata
|
||||
|
||||
## \internal
|
||||
# \brief The Flask instance. See <a href="https://flask.palletsprojects.com/en/stable/api/">Flask documentation</a>.
|
||||
app = flask.Flask("seagull-game", root_path=path_appdir, template_folder="templates", static_folder="static")
|
||||
CORS(app)
|
||||
orig_url_for = app.url_for
|
||||
|
||||
xml_namespaces = {
|
||||
@@ -34,6 +40,10 @@ xml_namespaces = {
|
||||
#REDIS_PASS="i am not a real password"
|
||||
#state_cache = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, username=REDIS_USER, password=REDIS_PASS)
|
||||
|
||||
# ham5 standing by...
|
||||
|
||||
## \internal
|
||||
# \brief Returns CDN URLs for files not present within the container itself, or copies VFS data out so Flask etc can use it.
|
||||
def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=None, _external=None, self=app, **values):
|
||||
if endpoint == "static":
|
||||
if not gamedata.vfs.exists(f"static/{values["filename"]}"):
|
||||
@@ -48,17 +58,25 @@ def url_for_override(endpoint, *posargs, _anchor=None, _method=None, _scheme=Non
|
||||
|
||||
app.url_for = url_for_override
|
||||
|
||||
## \internal
|
||||
# \brief Base Flask rendering context. Generated with render_base_context().
|
||||
base_context = {}
|
||||
base_context_live = False
|
||||
|
||||
## \brief Renders a dialog template and sends it to the client.
|
||||
# \param dialog The dialog to render.
|
||||
# \api{GET} /dialog/`<dialog>`
|
||||
@app.route("/dialog/<dialog>")
|
||||
def render_dialog(dialog):
|
||||
if gamedata.vfs.exists(f"templates/{dialog}.j2"):
|
||||
gamedata.vfs.copy_out(f"templates/{dialog}.j2", dest=path_appdir.as_posix())
|
||||
if gamedata.vfs.exists(f"static/js/dlg-{dialog}.js"):
|
||||
gamedata.vfs.copy_out(f"static/js/dlg-{dialog}.js", dest=path_appdir.as_posix())
|
||||
return flask.render_template(f"{dialog}.j2")
|
||||
else:
|
||||
return "", 404
|
||||
|
||||
## \brief Prepares the base rendering context for Flask to serve our content.
|
||||
def render_base_context():
|
||||
global base_context
|
||||
global base_context_live
|
||||
@@ -68,19 +86,32 @@ def render_base_context():
|
||||
domain_components = flask.request.host.split(".")
|
||||
base_domain = ".".join(domain_components[-2:])
|
||||
|
||||
gamedata.vfs.copy_out("static/js/mermaid.esm.min.mjs", dest=path_appdir.as_posix())
|
||||
|
||||
# all this wind up for...
|
||||
if base_domain == "otl-hga.net": # production, use assets from S3
|
||||
base_context["styles"] = ["https://cdn.otl-hga.net/seagull/css/seagull.css"]
|
||||
base_context["scripts"] = ["https://cdn.otl-hga.net/seagull/js/seagull.js"]
|
||||
base_context["scripts"] = ["https://cdn.otl-hga.net/seagull/js/seagull.js", "https://cdn.otl-hga.net/seagull/js/konami.js"]
|
||||
base_context["seagull_pic"] = "https://cdn.otl-hga.net/seagull/image/seagull.jpg"
|
||||
else: # dev, serve files from here
|
||||
#print(base_domain)
|
||||
base_context["styles"] = [app.url_for("static", filename="css/seagull.css")]
|
||||
base_context["scripts"] = [app.url_for("static", filename="js/seagull.js")]
|
||||
base_context["scripts"] = [(app.url_for("static", filename="js/konami.js"), True), (app.url_for("static", filename="js/seagull.js"), True)]
|
||||
base_context["seagull_pic"] = app.url_for("static", filename="image/seagull.jpg")
|
||||
|
||||
base_context_live = True
|
||||
|
||||
## \brief Returns OK. Useful for health checks.
|
||||
# \api{GET} /core/ping
|
||||
@app.route("/core/ping")
|
||||
def healthcheck_ping():
|
||||
return flask.Response("OK", content_type="text/plain")
|
||||
|
||||
## \brief Informs the game we're about to request a file from JavaScript.
|
||||
# \internal
|
||||
# \api{POST} /core/ready_file
|
||||
# \apidata Plaintext path to the intended file.
|
||||
@app.route("/core/ready_file", methods=["POST"])
|
||||
def ready_file():
|
||||
gamedata.vfs.copy_out(flask.request.data)
|
||||
return flask.Response("OK", content_type="text/plain")
|
@@ -2,14 +2,29 @@ import pathlib
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief A <a href="https://docs.python.org/3/library/pathlib.html#pathlib.Path">standard Path object</a> pointing to where the user's local storage is.
|
||||
#
|
||||
# Exactly where this is depends on your operating system:
|
||||
# - On Windows, this is defined as `%%APPDATA%\seagull`, which typically evaluates to something like `"C:\Users\YourNameHere\AppData\Roaming\seagull"`
|
||||
# - On MacOS, this is defined as `~/Library/Application Support/seagull`, where ~ is your home directory.
|
||||
# - On Linux, this is defined as `~/.local/share/seagull`.
|
||||
# \note On Linux, the XDG specification for defining alternative data directories is not currently respected. This will likely change in a future version.
|
||||
path_storagedir = pathlib.Path()
|
||||
|
||||
## \internal
|
||||
# \brief Defines JavaScript API functions for pywebview.
|
||||
class JS_API:
|
||||
# \brief Whether or not we're in debug mode.
|
||||
debug_mode = False
|
||||
|
||||
def __init__(self, debug_mode=False):
|
||||
self.debug_mode = debug_mode
|
||||
|
||||
## \brief Loads data stored under a specific "key" in the local filesystem.
|
||||
# \param key The key to load.
|
||||
# \return File contents, or `None`.
|
||||
# In the current implementation, key is just a raw filename loaded relative to path_storagedir,
|
||||
# which is calculated as part of main thread startup.
|
||||
def load_data(self, key):
|
||||
if not (path_storagedir / key).exists():
|
||||
return None
|
||||
@@ -21,6 +36,11 @@ class JS_API:
|
||||
core.log.error(f"problem loading {key} (from {path_storagedir}): {exc}")
|
||||
return None
|
||||
|
||||
## \brief Saves data stored under a specific "key" in the local filesystem.
|
||||
# \param key The key to save to.
|
||||
# \param data The data to write.
|
||||
# In the current implementation, key is just a raw filename loaded relative to path_storagedir,
|
||||
# which is calculated as part of main thread startup.
|
||||
def save_data(self, key, data):
|
||||
with open(path_storagedir / key, "w") as fd_datafile:
|
||||
try:
|
||||
@@ -28,6 +48,8 @@ class JS_API:
|
||||
except Exception as exc:
|
||||
core.log.error(f"problem saving {key} (to {path_storagedir}): {exc}")
|
||||
|
||||
## \brief Deletes all data stored at key.
|
||||
# \param key The key to delete.
|
||||
def delete_data(self, key):
|
||||
if (path_storagedir / key).exists():
|
||||
(path_storagedir / key).unlink()
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import flask
|
||||
|
||||
from . import core
|
||||
from . import core, gamedata
|
||||
|
||||
## \brief Renders the dev toolbox for the client
|
||||
# \api{GET} /dev/get-toolbox
|
||||
@core.app.route("/dev/get-toolbox")
|
||||
def dev_toolbox():
|
||||
gamedata.vfs.copy_out("static/sound/open_dev_toolkit.wav", dest=core.path_appdir.as_posix())
|
||||
gamedata.vfs.copy_out("templates/dev_toolbox.j2", dest=core.path_appdir.as_posix())
|
||||
return flask.render_template("dev_toolbox.j2", ipaddr=flask.request.remote_addr, desktop=core.desktop_mode)
|
@@ -12,6 +12,8 @@ import fs.osfs
|
||||
|
||||
from . import core
|
||||
|
||||
## \brief Handler for virtual filesystem.
|
||||
# \internal
|
||||
class GameVFSHandler(object):
|
||||
vfs = None
|
||||
log = logging.getLogger().getChild("vfs")
|
||||
@@ -43,6 +45,10 @@ class GameVFSHandler(object):
|
||||
except:
|
||||
raise
|
||||
|
||||
## \brief Load files from a given data source.
|
||||
# \param source The data source. This can be a pyfilesystem2 object, or any string or path-like
|
||||
# object that can reasonably be interpreted as a directory or zip file.
|
||||
# \param proto The <a href="https://pyfilesystem2.readthedocs.io/en/latest/builtin.html">PyFilesystem2 filesystem protocol</a> to use. Defaults to `"osfs"`, which loads directories.
|
||||
def load_data_source(self, source: pathlib.Path | fs.base.FS | typing.Text, proto="osfs"):
|
||||
print(f"data source: {source} ({proto})")
|
||||
assert self.vfs is not None
|
||||
@@ -63,6 +69,10 @@ class GameVFSHandler(object):
|
||||
self.log.info(f"loading vfs source: {fs_source} (pyfilesystem2 handler)")
|
||||
fs.copy.copy_fs(fs_source, self.vfs)
|
||||
|
||||
## \brief Copies a file out of VFS into the real filesystem.
|
||||
# \param filepath The source file path to copy out.
|
||||
# \param dest The path to write the file to. Defaults to temporary directory.
|
||||
# \return The full destination file path.
|
||||
def copy_out(self, filepath, dest=None):
|
||||
if not dest:
|
||||
self.osfs_temp.makedirs(pathlib.Path(filepath).parent.as_posix(), recreate=True)
|
||||
@@ -76,4 +86,6 @@ class GameVFSHandler(object):
|
||||
fs.copy.copy_file(self.vfs, filepath, dest, filepath)
|
||||
return (pth_dest / filepath).as_posix()
|
||||
|
||||
## \brief Primary VFS handler.
|
||||
# \internal
|
||||
vfs = GameVFSHandler()
|
@@ -10,6 +10,8 @@ valid_resources = [
|
||||
"food", "shinies", "psi" # early game
|
||||
]
|
||||
|
||||
## \internal
|
||||
# \brief The environment variable map to run Rant with.
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
|
||||
@@ -18,6 +20,12 @@ doc_item_schema = xmltree.parse(pth_item_schema.as_posix())
|
||||
item_schema = xmltree.XMLSchema(doc_item_schema)
|
||||
item_schema_parser = xmltree.XMLParser(schema=item_schema)
|
||||
|
||||
## \brief Generates an absolutely reasonable description of a given item that a target might have.
|
||||
# \param resource The resource to generate a description for.
|
||||
# \param target The poor soul carrying the item.
|
||||
# \return An absolutely reasonable description of an item.
|
||||
#
|
||||
# \include{doc,local} res/doc/python/items.generate_item_description.mdpart
|
||||
def generate_item_description(resource, target):
|
||||
if core.desktop_mode:
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
@@ -29,6 +37,13 @@ def generate_item_description(resource, target):
|
||||
core.log.warning("rant is throwing up:\n" + proc_rant.stderr.decode())
|
||||
return proc_rant.stdout.decode().strip()
|
||||
|
||||
## \brief Generates a list of items worth `min`-`max` `resource` that `target` would reasonably have.
|
||||
# \param resource The resource to generate a description for.
|
||||
# \param target The poor soul carrying the item.
|
||||
# \param min The lowest possible value, per item.
|
||||
# \param max The highest possible value, per item.
|
||||
# \param storybeat Inform the rule parsing engine where we are in the story.
|
||||
# \return A list of TickItem instances.
|
||||
def generate_item_list(resource, target, min, max, storybeat=0):
|
||||
count = random.randint(min, max)
|
||||
result = []
|
||||
@@ -43,12 +58,12 @@ def generate_item_list(resource, target, min, max, storybeat=0):
|
||||
maxdata = res_rule.xpath("./items:Max", namespaces=core.xml_namespaces)[0]
|
||||
resource_rules.append((res_rule, int(mindata.text), int(maxdata.text)))
|
||||
for i in range(0, count):
|
||||
core.log.warning("TODO: we don't know which rule this parses yet")
|
||||
core.log.warning(f"{resource} vs humans: {resource_rules[0]}")
|
||||
core.log.warning(f"{resource} vs humans: {resource_rules[0][1]} <-> {resource_rules[0][2]} (sb: {resource_rules[0][0].get("StoryBeat", "0")})")
|
||||
result.append(TickItem(resource, round(random.uniform(resource_rules[0][1], resource_rules[0][2]), 2), target))
|
||||
|
||||
return result
|
||||
|
||||
## \brief A tick item.
|
||||
class TickItem(object):
|
||||
def __init__(self, resource, amount, target):
|
||||
if resource not in valid_resources:
|
||||
|
@@ -2,6 +2,8 @@ import json
|
||||
|
||||
from . import items
|
||||
|
||||
## \brief Extended JSON encoder object to handle seagull-specific items.
|
||||
# \internal
|
||||
class JSONizer(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, items.TickItem):
|
||||
|
@@ -5,7 +5,7 @@ import subprocess
|
||||
|
||||
import flask
|
||||
|
||||
from . import core, items, jsonizer
|
||||
from . import core, gamedata, items, jsonizer
|
||||
|
||||
rant_env = os.environ.copy()
|
||||
rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "basepak/rant").as_posix()
|
||||
@@ -15,7 +15,8 @@ def generate_flavor_text():
|
||||
rant_path = core.path_appdir / "opt/rant/bin/rant"
|
||||
else:
|
||||
rant_path = "rant" # rely on OS PATH
|
||||
proc_rant = subprocess.run([rant_path, (core.path_appdir / "basepak/rant/flavor.rant").as_posix()], env=rant_env, capture_output=True)
|
||||
flavor_file = gamedata.vfs.copy_out("rant/flavor.rant")
|
||||
proc_rant = subprocess.run([rant_path, flavor_file], env=rant_env, capture_output=True)
|
||||
return proc_rant.stdout.decode()
|
||||
|
||||
class TickEvent(object):
|
||||
@@ -30,6 +31,9 @@ tick_event_list.append(TickEvent(1, 1, "FLAVOR")) # procedurally generated
|
||||
tick_event_list.append(TickEvent(10, 2, "ENCHUMAN")) # encounter: human
|
||||
tick_event_list.append(TickEvent(11, 2, "ENCGULL"))
|
||||
|
||||
## \brief Returns the results of a game tick.
|
||||
# \api{GET} /tick
|
||||
# \return Game tick events and results.
|
||||
@core.app.route("/tick")
|
||||
def tick():
|
||||
#return random.choices([json.dumps({"code": 200, "event_type": 0}), json.dumps({"code": 200, "event_type": 1, "log": generate_flavor_text()})], weights=[16, 1])[0]
|
||||
@@ -67,10 +71,28 @@ def tick():
|
||||
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
|
||||
## \brief Returns the results of a colony tick.
|
||||
# \api{GET} /tick/colony
|
||||
# \return The result of a colony tick.
|
||||
@core.app.route("/tick/colony", methods=["POST"])
|
||||
def tick_colony() -> flask.Response:
|
||||
req = flask.request.json
|
||||
if not req:
|
||||
return flask.make_response("Bad request", 400)
|
||||
|
||||
colony = req["colony"]
|
||||
modifiers = req["modifiers"]
|
||||
inc_food = req["avg_food"]
|
||||
inc_shinies = req["avg_shinies"]
|
||||
|
||||
out_food = (inc_food * 0.35) * (colony / 2)
|
||||
out_shinies = (inc_shinies * 0.25) * (colony / 3)
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"food": out_food,
|
||||
"shinies": out_shinies
|
||||
}
|
||||
|
||||
return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json")
|
||||
|
93
app/pylocal/upgrades.py
Normal file
93
app/pylocal/upgrades.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import io
|
||||
import logging
|
||||
import textwrap
|
||||
from collections import namedtuple
|
||||
|
||||
import flask
|
||||
import lxml.etree as xmltree
|
||||
import mermaid
|
||||
|
||||
from . import core, gamedata
|
||||
|
||||
pth_upgrade_schema = core.path_appdir / "basepak/rules/schemas/upgrades.xsd"
|
||||
doc_upgrade_schema = xmltree.parse(pth_upgrade_schema.as_posix())
|
||||
upgrade_schema = xmltree.XMLSchema(doc_upgrade_schema)
|
||||
upgrade_schema_parser = xmltree.XMLParser(schema=upgrade_schema)
|
||||
|
||||
UpgradeData = namedtuple("UpgradeData", ["id", "name", "desc", "requires"])
|
||||
|
||||
## \brief Renders an upgrade tree.
|
||||
# \param tree The upgrade tree to retrieve.
|
||||
# \api{GET} /upgrades/`<tree>`
|
||||
# \return An SVG.
|
||||
@core.app.route("/upgrades/<tree>")
|
||||
def get_upgrade_tree(tree):
|
||||
buf_mmd = io.StringIO()
|
||||
rulefile = xmltree.parse(gamedata.vfs.open(f"/rules/upgrades/{tree}.xml"), upgrade_schema_parser)
|
||||
ruleset = rulefile.getroot()
|
||||
|
||||
hnd_treedata = ruleset.xpath("./upgrades:TreeData", namespaces=core.xml_namespaces)[0]
|
||||
hnd_name = hnd_treedata.xpath("./upgrades:Name", namespaces=core.xml_namespaces)[0]
|
||||
hnd_primary_color = hnd_treedata.xpath("./upgrades:PrimaryColor", namespaces=core.xml_namespaces)[0]
|
||||
|
||||
buf_mmd.write(textwrap.dedent(f"""
|
||||
----
|
||||
title: {hnd_name.text}
|
||||
theme: base
|
||||
themeVariables:
|
||||
primaryColor: {hnd_primary_color.text}
|
||||
----
|
||||
flowchart LR
|
||||
"""))
|
||||
|
||||
tree_upgrades = []
|
||||
for hnd_upgrade in ruleset.iter("{seagull:rules/upgrades}Upgrade"):
|
||||
hnd_id = hnd_upgrade.xpath("./upgrades:Id", namespaces=core.xml_namespaces)[0]
|
||||
hnd_upgrade_name = hnd_upgrade.xpath("./upgrades:Name", namespaces=core.xml_namespaces)[0]
|
||||
hnd_desc = hnd_upgrade.xpath("./upgrades:Desc", namespaces=core.xml_namespaces)[0]
|
||||
try:
|
||||
hnd_requires = hnd_upgrade.xpath("./upgrades:Requirements", namespaces=core.xml_namespaces)[0]
|
||||
require_list = [elem.text for elem in hnd_requires.iter("{seagull:rules/upgrades}Require")]
|
||||
except IndexError:
|
||||
require_list = []
|
||||
|
||||
upgrade = UpgradeData(
|
||||
id=hnd_id.text,
|
||||
name=hnd_upgrade_name.text,
|
||||
desc=hnd_desc.text,
|
||||
requires=require_list
|
||||
)
|
||||
|
||||
tree_upgrades.append(upgrade)
|
||||
|
||||
tiers = {}
|
||||
dependency_lines = []
|
||||
for upgrade in tree_upgrades:
|
||||
deptier = 0
|
||||
buf_mmd.write(f" {upgrade.id}@{{label: \"{upgrade.name}\"}}\n")
|
||||
collected_tiers = []
|
||||
for require in upgrade.requires:
|
||||
if require in tiers:
|
||||
collected_tiers.append(tiers[require])
|
||||
|
||||
dependency_lines.append(f" {require} --> {upgrade.id}\n")
|
||||
|
||||
if len(collected_tiers) > 0:
|
||||
deptier = max(collected_tiers)
|
||||
|
||||
tiers[upgrade.id] = deptier + 1
|
||||
|
||||
for line in dependency_lines:
|
||||
buf_mmd.write(line)
|
||||
|
||||
buf_mmd.seek(0)
|
||||
print(buf_mmd.read())
|
||||
return get_upgrade_tree_mmd(tree) # TEMP
|
||||
|
||||
def get_upgrade_tree_mmd(tree):
|
||||
if not gamedata.vfs.exists(f"upgrades/{tree}.mmd"):
|
||||
return flask.make_response("No Upgrade Tree", 404)
|
||||
|
||||
with gamedata.vfs.open(f"upgrades/{tree}.mmd") as fd_upgradetree:
|
||||
mmd_upgradetree = mermaid.Mermaid(fd_upgradetree.read(), height=400)
|
||||
return mmd_upgradetree.svg_response.content
|
Reference in New Issue
Block a user