From 1b72e899afe530902b52e654cd4288ebb4d0464f Mon Sep 17 00:00:00 2001 From: Nicole O'Connor Date: Tue, 5 Aug 2025 17:57:43 -0700 Subject: [PATCH] state sync; the basic early game works --- .gitignore | 3 +- .rsync-include | 4 ++ app/desktop.py | 4 +- app/index.wsgi | 2 +- app/pylocal/actions.py | 19 +++++++ app/pylocal/items.py | 10 +++- app/pylocal/jsonizer.py | 11 ++++ app/pylocal/tick.py | 8 +-- app/rant/food/humans.rant | 6 +- app/rules/items/humans.xml | 10 ++-- app/rules/schemas/catalog.xml | 4 ++ app/rules/schemas/items.xsd | 37 +++++++++++++ build-desktop.sh | 9 +-- gamestate | 1 - static/js/seagull-desktop.js | 4 ++ static/js/seagull.js | 100 +++++++++++++++++++++++++++++++++- 16 files changed, 209 insertions(+), 23 deletions(-) create mode 100644 .rsync-include create mode 100644 app/pylocal/actions.py create mode 100644 app/pylocal/jsonizer.py create mode 100644 app/rules/schemas/catalog.xml create mode 100644 app/rules/schemas/items.xsd delete mode 100644 gamestate diff --git a/.gitignore b/.gitignore index 2cac875..10232ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode/** scratch.ipynb .docker-login-credentials -seagull \ No newline at end of file +seagull +build/** \ No newline at end of file diff --git a/.rsync-include b/.rsync-include new file mode 100644 index 0000000..c3962d1 --- /dev/null +++ b/.rsync-include @@ -0,0 +1,4 @@ +app/** +ext/imsky/wordlists/** +static/** +seagull.spec \ No newline at end of file diff --git a/app/desktop.py b/app/desktop.py index 68aebfe..d623af1 100755 --- a/app/desktop.py +++ b/app/desktop.py @@ -9,13 +9,13 @@ import webview import flask -from pylocal import core, desktop, dev, items, tick +from pylocal import core, actions, desktop, dev, items, tick core.desktop_mode = True sig_exit = threading.Event() argp = argparse.ArgumentParser("seagull") -argp.add_argument("-d", "--debug", action="store_true") +argp.add_argument("-d", "--debug", action="store_true", help="Launches the game in \"debug mode\".") argo = argp.parse_args() @core.app.route("/") diff --git a/app/index.wsgi b/app/index.wsgi index cfdfd24..4851d55 100755 --- a/app/index.wsgi +++ b/app/index.wsgi @@ -10,7 +10,7 @@ import threading import flask from gevent.pywsgi import WSGIServer -from pylocal import core, dev, items, tick +from pylocal import core, actions, dev, items, tick sig_exit = threading.Event() diff --git a/app/pylocal/actions.py b/app/pylocal/actions.py new file mode 100644 index 0000000..ada9259 --- /dev/null +++ b/app/pylocal/actions.py @@ -0,0 +1,19 @@ +import json +import random + +import flask + +from . import core + +def dice_roll(min=0, max=100, modifiers=[]): + result = random.randint(min, max) + for _, mod in modifiers: + result += mod + + return result + +@core.app.route("/act/steal/", methods=["POST"]) +def steal_resource(resource): + return flask.Response(json.dumps({ + "success": (dice_roll() >= 50) + }), status=200, content_type="application/json") \ No newline at end of file diff --git a/app/pylocal/items.py b/app/pylocal/items.py index 3682c0a..d0be5b4 100644 --- a/app/pylocal/items.py +++ b/app/pylocal/items.py @@ -1,3 +1,4 @@ +import os import subprocess import xml.etree.ElementTree as xmltree @@ -7,13 +8,18 @@ valid_resources = [ "food", "shinies", "psi" # early game ] +rant_env = os.environ.copy() +rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "rant").as_posix() + def generate_item(resource, target): if core.desktop_mode: 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 / f"rant/{resource}/{target}.rant").as_posix()], capture_output=True) - return proc_rant.stdout.decode() + proc_rant = subprocess.run([rant_path, (core.path_appdir / f"rant/{resource}/{target}.rant").as_posix()], env=rant_env, capture_output=True) + if proc_rant.stderr: + core.log.warning("rant is throwing up:\n" + proc_rant.stderr.decode()) + return proc_rant.stdout.decode().strip() class TickItem(object): def __init__(self, resource, amount, target): diff --git a/app/pylocal/jsonizer.py b/app/pylocal/jsonizer.py new file mode 100644 index 0000000..f88d8a7 --- /dev/null +++ b/app/pylocal/jsonizer.py @@ -0,0 +1,11 @@ +import json + +from . import items + +class JSONizer(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, items.TickItem): + return {"resource": obj.resource, "amount": obj.amount, "desc": obj.desc} + else: + # if no encoding here, fall back to stdlib + return super().default(obj) \ No newline at end of file diff --git a/app/pylocal/tick.py b/app/pylocal/tick.py index 84d940c..7e499b3 100644 --- a/app/pylocal/tick.py +++ b/app/pylocal/tick.py @@ -4,7 +4,7 @@ import subprocess import flask -from . import core, items +from . import core, items, jsonizer def generate_flavor_text(): if core.desktop_mode: @@ -47,10 +47,10 @@ def tick(): case 10: # ENCHUMAN result["items"] = { # TODO: read ranges from XML rule files - "food": [items.TickItem("food", random.uniform(0.0, 20.0), "humans")], - "shinies": [items.TickItem("food", random.uniform(0.0, 20.0), "humans")] + "food": [items.TickItem("food", round(random.uniform(0.0, 20.0), 2), "humans") for i in range(random.randint(0, 3))], + "shinies": [items.TickItem("shinies", round(random.uniform(0.0, 20.0), 2), "humans") for i in range(random.randint(0, 3))] } case _: core.log.warning("undefined tick: {0}".format(result["event_type"])) - return flask.Response(json.dumps(result), status=200, content_type="application/json") \ No newline at end of file + return flask.Response(json.dumps(result, cls=jsonizer.JSONizer), status=200, content_type="application/json") \ No newline at end of file diff --git a/app/rant/food/humans.rant b/app/rant/food/humans.rant index 2ef4620..54507c7 100644 --- a/app/rant/food/humans.rant +++ b/app/rant/food/humans.rant @@ -1,4 +1,4 @@ -@require "../wordlist" +@require "wordlist" [$desc_food] @text { { [pick: ] @weight 1.25 | @@ -31,6 +31,7 @@ } } +## [$mod_order] @text { { add | no | sub | extra | @@ -43,11 +44,12 @@ [pick: ] } } +## { a piece of `[pick: ] | a `{ - [if: [%maybe]]{[desc_food]} [get_entree] | + [if: [maybe]{[desc_food]}] [get_entree] | [pick: ] } } \ No newline at end of file diff --git a/app/rules/items/humans.xml b/app/rules/items/humans.xml index 42927ca..1f76d6c 100644 --- a/app/rules/items/humans.xml +++ b/app/rules/items/humans.xml @@ -1,5 +1,5 @@ - - + + ENCHUMAN 0 @@ -9,15 +9,15 @@ 0 20 - + 0 20 - + 0 50 - + 0 15 diff --git a/app/rules/schemas/catalog.xml b/app/rules/schemas/catalog.xml new file mode 100644 index 0000000..dfda357 --- /dev/null +++ b/app/rules/schemas/catalog.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/rules/schemas/items.xsd b/app/rules/schemas/items.xsd new file mode 100644 index 0000000..141ddfa --- /dev/null +++ b/app/rules/schemas/items.xsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-desktop.sh b/build-desktop.sh index 43e15d0..89979a3 100755 --- a/build-desktop.sh +++ b/build-desktop.sh @@ -1,6 +1,8 @@ #!/bin/bash -BUILD_DIR=${BUILD_DIR:-./build} +srcdir=$(pwd) +BUILD_DIR=${BUILD_DIR:-$srcdir/build} +echo "$srcdir => $BUILD_DIR" die () { echo "$@" >&2 @@ -15,14 +17,13 @@ findcmd cargo findcmd python findcmd rsync -srcdir=$(pwd) mkdir -p $BUILD_DIR && cd $BUILD_DIR -rsync -rv $srcdir/ $BUILD_DIR/ +rsync -rv --include-from=$srcdir/.rsync-include $srcdir/ $BUILD_DIR/ # rant mkdir -p opt/rant cargo install rant --version 4.0.0-alpha.33 --root $BUILD_DIR/opt/rant --features cli -python $srcdir/ext/imsky/wordlists/render.py -i ext/imsky/wordlists -o rant $BUILD_DIR/app/rant/wordlist.rant +python $srcdir/render-wordlists.py -i $srcdir/ext/imsky/wordlists -o rant $BUILD_DIR/app/rant/wordlist.rant # python venv python -m venv pyvenv diff --git a/gamestate b/gamestate deleted file mode 100644 index 7907e94..0000000 --- a/gamestate +++ /dev/null @@ -1 +0,0 @@ -{"statever":"1","tick":211,"name":"Jeff","level":1,"shinies":0,"colony":1,"food":0,"autosave":35,"story_beat":0,"xp":0,"xp_next":50} \ No newline at end of file diff --git a/static/js/seagull-desktop.js b/static/js/seagull-desktop.js index 445248e..cfd627b 100644 --- a/static/js/seagull-desktop.js +++ b/static/js/seagull-desktop.js @@ -21,6 +21,10 @@ async function prepare_gamestate() { } tick_meter_running = true; + + if (window.pywebview.api.debug_mode) { + dev_toolbox(true); + } } function save_game() { diff --git a/static/js/seagull.js b/static/js/seagull.js index a6d6134..7bfc334 100644 --- a/static/js/seagull.js +++ b/static/js/seagull.js @@ -45,6 +45,44 @@ function record_log(text) { page_elements["div_log"].append(div_logrow); } +function record_log_with_choices() { + const div_logrow = document.createElement("div"); + if (bool_log_alt) { div_logrow.className = "log-line"; } + else { div_logrow.className = "log-line-alt"; } + bool_log_alt = !bool_log_alt; + + const div_logtick = document.createElement("div"); + div_logtick.className = "log-tick" + div_logtick.innerHTML = "Day " + gamestate["tick"]; + div_logrow.append(div_logtick); + + const div_logdata = document.createElement("div"); + + const div_logmsg = document.createElement("div"); + div_logmsg.innerHTML = arguments[0]; + div_logmsg.className = "log-msg"; + div_logdata.append(div_logmsg); + + const div_logactions = document.createElement("div"); + div_logactions.className = "log-button-row"; + + for (var i = 1; i < arguments.length; i += 2) { + console.log(i) + var label = arguments[i]; + var callback = arguments[i+1]; + + var btn_action = document.createElement("button"); + btn_action.innerHTML = label; + btn_action.setAttribute("onclick", callback + "; tick_meter_running = true;"); + div_logactions.append(btn_action); + } + div_logdata.append(div_logactions); + + div_logrow.append(div_logdata); + page_elements["div_log"].append(div_logrow); + tick_meter_running = false; +} + function update_ui() { page_elements["lbl_name"].innerHTML = gamestate["name"]; page_elements["lbl_tick"].innerHTML = gamestate["tick"]; @@ -78,6 +116,18 @@ function dev_toolbox(open) { dev_toolbox_open = open; } +async function steal_resource(resource, amount, items) { + var stealdata = await fetch("/act/steal/" + resource, {method: "POST", body: JSON.stringify({gamestate: gamestate})}) + .then(res => { return res.json(); }) + .catch(e => { throw e; }); + + if (stealdata["success"]) { + gamestate[resource] += amount; + record_log("Stole " + resource + " from a human: " + items.join(", ")); + } + else { record_log("Didn't steal " + resource + " from a human"); } +} + async function game_tick() { gamestate["tick"] += 1; ticks_since_last_save += 1; @@ -102,7 +152,55 @@ async function game_tick() { // Flavor event - no gameplay effect, but occasionally says something fun. record_log(tickdata["log"]); } else if (tickdata["event_type"] == 10) { // ENCHUMAN - + var total_food = 0; + var food_descs = []; + var total_shinies = 0; + var shinies_descs = []; + + switch (page_elements["menu_enc_human"].value) { + case "pause": + tickdata.items.food.forEach((item) => { + total_food += item["amount"]; + food_descs.push(item["desc"]); + }) + tickdata.items.shinies.forEach((item) => { + total_shinies += item["amount"]; + shinies_descs.push(item["desc"]); + }) + var logstring = "You have encountered a human. It is carrying these resources:\n\n" + logstring += "
    \n" + if (total_food > 0) { + logstring += "
  1. " + total_food + " food: " + food_descs.join(", ") + "
  2. \n"; + } + if (total_shinies > 0) { + logstring += "
  3. " + total_shinies + " shinies: " + shinies_descs.join(", ") + "
  4. \n"; + } + logstring += "
\nWhat would you like to do?"; + + record_log_with_choices(logstring, + "Steal food", `steal_resource('food', ${total_food}, ${JSON.stringify(food_descs)})`, + "Steal shinies", `steal_resource('shinies', ${total_shinies}, ${JSON.stringify(shinies_descs)})` + ) + + break; + case "steal-food": + tickdata.items.food.forEach((item) => { + total_food += item["amount"]; + food_descs.push(item["desc"]); + }) + steal_resource("food", total_food, food_descs); + break; + case "steal-shinies": + tickdata.items.shinies.forEach((item) => { + total_shinies += item["amount"]; + shinies_descs.push(item["desc"]); + }) + steal_resource("shinies", total_shinies, shinies_descs); + break; + default: + console.error("undefined action " + page_elements["menu_enc_human"]); + break; + } } // sanity check