From 8f7f173108b202ae54740c5526e17e5fde3ac8cf Mon Sep 17 00:00:00 2001 From: Nicole O'Connor Date: Thu, 7 Aug 2025 14:10:20 -0700 Subject: [PATCH] human theft now reads from xml rules file --- app/pylocal/core.py | 4 ++++ app/pylocal/items.py | 35 +++++++++++++++++++++++++++++--- app/pylocal/tick.py | 4 ++-- app/rant/food/humans.rant | 2 +- app/requirements.txt | 3 +-- app/rules/items/humans.xml | 7 +++---- app/rules/items/seagulls.xml | 23 +++++++++++++++++++++ app/rules/schemas/items.xsd | 1 - seagull.spec | 1 + static/js/seagull.js | 39 +++++++++++++++++++++++++----------- 10 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 app/rules/items/seagulls.xml diff --git a/app/pylocal/core.py b/app/pylocal/core.py index 233f203..0b80967 100644 --- a/app/pylocal/core.py +++ b/app/pylocal/core.py @@ -21,6 +21,10 @@ desktop_mode = False app = flask.Flask("seagull-game", root_path=path_appdir) orig_url_for = app.url_for +xml_namespaces = { + "items": "seagull:rules/items" +} + #REDIS_HOST="stub-implementation.example.net" #REDIS_PORT=6379 #REDIS_USER="seagull" diff --git a/app/pylocal/items.py b/app/pylocal/items.py index d0be5b4..20ee43c 100644 --- a/app/pylocal/items.py +++ b/app/pylocal/items.py @@ -1,9 +1,14 @@ import os +import random import subprocess -import xml.etree.ElementTree as xmltree + +import lxml.etree as xmltree from . import core +# NOTE: due to how XML libraries handle namespaces, you have to prepend "{seagull:rules/items}" to every tag value +# this is merely the price you pay for editor autocompletions + valid_resources = [ "food", "shinies", "psi" # early game ] @@ -11,7 +16,11 @@ valid_resources = [ rant_env = os.environ.copy() rant_env["RANT_MODULES_PATH"] = (core.path_appdir / "rant").as_posix() -def generate_item(resource, target): +fd_item_schema = xmltree.parse(core.path_appdir / "rules/schemas/items.xsd") +item_schema = xmltree.XMLSchema(fd_item_schema) +item_schema_parser = xmltree.XMLParser(schema=item_schema) + +def generate_item_description(resource, target): if core.desktop_mode: rant_path = core.path_appdir / "opt/rant/bin/rant" else: @@ -21,6 +30,26 @@ def generate_item(resource, target): core.log.warning("rant is throwing up:\n" + proc_rant.stderr.decode()) return proc_rant.stdout.decode().strip() +def generate_item_list(resource, target, min, max, storybeat=0): + count = random.randint(min, max) + result = [] + rulefile = xmltree.parse(core.path_appdir / f"rules/items/{target}.xml", item_schema_parser) + ruleset = rulefile.getroot() + resource_rules = [] + for res_rule in ruleset.iter(f"{{seagull:rules/items}}{resource.title()}"): + if int(res_rule.get("StoryBeat", "0")) > storybeat: + continue + + mindata = res_rule.xpath("./items:Min", namespaces=core.xml_namespaces)[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]}") + result.append(TickItem(resource, round(random.uniform(resource_rules[0][1], resource_rules[0][2]), 2), target)) + + return result + class TickItem(object): def __init__(self, resource, amount, target): if resource not in valid_resources: @@ -29,4 +58,4 @@ class TickItem(object): self.resource = resource self.amount = amount self.target = target - self.desc = generate_item(resource, target) \ No newline at end of file + self.desc = generate_item_description(resource, target) \ No newline at end of file diff --git a/app/pylocal/tick.py b/app/pylocal/tick.py index 7e499b3..92cffb7 100644 --- a/app/pylocal/tick.py +++ b/app/pylocal/tick.py @@ -47,8 +47,8 @@ def tick(): case 10: # ENCHUMAN result["items"] = { # TODO: read ranges from XML rule files - "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))] + "food": items.generate_item_list("food", "humans", 0, 2), + "shinies": items.generate_item_list("shinies", "humans", 0, 2) } case _: core.log.warning("undefined tick: {0}".format(result["event_type"])) diff --git a/app/rant/food/humans.rant b/app/rant/food/humans.rant index 54507c7..7855c46 100644 --- a/app/rant/food/humans.rant +++ b/app/rant/food/humans.rant @@ -47,7 +47,7 @@ ## { - a piece of `[pick: ] | + a piece of `[pick: ] cheese | a `{ [if: [maybe]{[desc_food]}] [get_entree] | [pick: ] diff --git a/app/requirements.txt b/app/requirements.txt index 6a1db7a..ffc29d9 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,4 +1,3 @@ Flask==3.1.1 gevent==25.5.1 -hiredis>=1.0.0 -redis==6.2.0 \ No newline at end of file +lxml>=6.0.0 \ No newline at end of file diff --git a/app/rules/items/humans.xml b/app/rules/items/humans.xml index 1f76d6c..02984fa 100644 --- a/app/rules/items/humans.xml +++ b/app/rules/items/humans.xml @@ -1,16 +1,15 @@ - ENCHUMAN - 0 + 1 10 - 0 + 1 20 - 0 + 1 20 diff --git a/app/rules/items/seagulls.xml b/app/rules/items/seagulls.xml new file mode 100644 index 0000000..e967143 --- /dev/null +++ b/app/rules/items/seagulls.xml @@ -0,0 +1,23 @@ + + + + 1 + 5 + + + 1 + 10 + + + 5 + 20 + + + 5 + 50 + + + 0 + 15 + + \ No newline at end of file diff --git a/app/rules/schemas/items.xsd b/app/rules/schemas/items.xsd index 141ddfa..f47a998 100644 --- a/app/rules/schemas/items.xsd +++ b/app/rules/schemas/items.xsd @@ -3,7 +3,6 @@ - diff --git a/seagull.spec b/seagull.spec index 97617b9..b9a2061 100644 --- a/seagull.spec +++ b/seagull.spec @@ -7,6 +7,7 @@ a = Analysis( binaries=[], datas=[ ('app/templates', './templates'), + ('app/rules', './rules'), ('static', './static'), ('app/rant', './rant'), ('opt', './opt') diff --git a/static/js/seagull.js b/static/js/seagull.js index 23e00b3..aba8740 100644 --- a/static/js/seagull.js +++ b/static/js/seagull.js @@ -88,8 +88,8 @@ function update_ui() { page_elements["lbl_name"].innerHTML = gamestate["name"]; page_elements["lbl_tick"].innerHTML = gamestate["tick"]; page_elements["lbl_colony"].innerHTML = gamestate["colony"]; - page_elements["lbl_shinies"].innerHTML = gamestate["shinies"]; - page_elements["lbl_food"].innerHTML = gamestate["food"]; + page_elements["lbl_shinies"].innerHTML = gamestate["shinies"].toFixed(2); + page_elements["lbl_food"].innerHTML = gamestate["food"].toFixed(2); page_elements["lbl_class"].innerHTML = gamestate["class"]; page_elements["lbl_xp"].innerHTML = gamestate["xp"]; page_elements["lbl_xp_next"].innerHTML = gamestate["xp_next"]; @@ -118,13 +118,25 @@ function dev_toolbox(open) { dev_toolbox_open = open; } -async function steal_resource(resource, amount, items) { +function reward_xp(amount) { + gamestate["xp"] += amount; + if (gamestate["xp"] >= gamestate["xp_next"]) { + old_xp_next = gamestate["xp_next"]; + gamestate["xp"] -= old_xp_next; + gamestate["level"] += 1; + gamestate["xp_next"] = (old_xp_next * 1.5) + (gamestate["level"] * 5); + } +} + +async function steal_resource(resource, amount, itemstr) { + var items = itemstr.split(",") 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"] && amount > 0) { gamestate[resource] += amount; + reward_xp(2); record_log("Stole " + resource + " from a human: " + items.join(", ")); } else { record_log("Didn't steal " + resource + " from a human"); } @@ -164,24 +176,25 @@ async function game_tick() { 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`; + logstring += `
  3. ${total_food.toFixed(2)} food: ${food_descs.join(", ")}
  4. \n`; } if (total_shinies > 0) { - logstring += `
  5. ${total_shinies} shinies: ${shinies_descs.join(", ")}
  6. \n`; + logstring += `
  7. ${total_shinies.toFixed(2)} shinies: ${shinies_descs.join(", ")}
  8. \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)})` + "Steal food", `steal_resource('food', ${total_food}, "${food_descs.toString()}")`, + "Steal shinies", `steal_resource('shinies', ${total_shinies}, "${shinies_descs.toString()}")` ) break; @@ -190,14 +203,14 @@ async function game_tick() { total_food += item["amount"]; food_descs.push(item["desc"]); }) - steal_resource("food", total_food, food_descs); + steal_resource("food", total_food, food_descs.toString()); 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); + steal_resource("shinies", total_shinies, shinies_descs.toString()); break; default: console.error("undefined action " + page_elements["menu_enc_human"]); @@ -249,13 +262,15 @@ target.addEventListener(start_event, function (ev) { page_elements["menu_enc_human"] = document.querySelector("#menu-enc-human"); page_elements["menu_enc_seagull"] = document.querySelector("#menu-enc-seagull"); - prepare_gamestate().then(update_ui()); + prepare_gamestate(); record_log("seagull game ver. " + ver_string); const interval = setInterval(() => { if (tick_meter_running) { game_tick(); } }, 1200); + + update_ui(); }); function change_seagull_name() {