From 2e50f61e57091a78a7e81409c7a607847acff607 Mon Sep 17 00:00:00 2001 From: Pagwin Date: Wed, 17 Jun 2026 15:41:05 -0400 Subject: [PATCH] more frontend refactoring --- .gitignore | 1 + index.html | 39 +++++++++++++++++++++++---------------- solve.py | 4 ++-- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 9f7550b..14a1b3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ .venv +*.pdf diff --git a/index.html b/index.html index 9fa757a..a0c8f80 100644 --- a/index.html +++ b/index.html @@ -162,10 +162,6 @@ display: flex; } - .card.log-on .linear-only { - opacity: .4; - } - /* Output tables: a CSS grid kept inside a scroll container so it never stretches the page wider than it should. */ .gtable-wrap { @@ -406,13 +402,20 @@ } // --- cities --- + // Pick a name not already used by an existing city: the lowest non-negative + // integer (as a string) that's free, matching the seeded "0", "1", … names. + function uniqueCityName() { + const taken = new Set( + [...document.getElementById("cities").children].map(r => r._get().name)); + for (let n = 0; ; n++) if (!taken.has(String(n))) return String(n); + } function addCity(c = {}) { const card = el("div", {class: "card"}); - const name = el("input", {value: c.name || ""}); + const name = el("input", {value: c.name || uniqueCityName()}); const type = selectEl(CITY_TYPES, c.type || "hub"); const renown = num(c.renown ?? "", {min: 1, placeholder: "auto"}); - const vs = num(c.vat_steel || 0, {min: 0}), vb = num(c.vat_brass || 0, {min: 0}), - ve = num(c.vat_electrum || 0, {min: 0}); + const vs = num(c.vat_steel || 1, {min: 0}), vb = num(c.vat_brass || 1, {min: 0}), + ve = num(c.vat_electrum || 1, {min: 0}); const reno = el("input", {type: "checkbox"}); reno.checked = c.can_renovate !== false; const adjacent = el("input", { value: (c.adjacent || []).join(", "), @@ -515,7 +518,6 @@ }; card.append( field("Agent", type), - field("Name", name), field("Effect", desc), field("Bastions (Baron only)", bastions), field("Forced city (turn:city, csv)", forced), @@ -535,6 +537,7 @@ const toggle = () => card.classList.toggle("log-on", isLog.checked); isLog.onchange = toggle; const expr = el("textarea", {rows: 1, placeholder: "(x) => Math.log2(x + 1)"}); + expr.innerText = "(x) => Math.log2(x + 1)"; if (t._expr) expr.value = t._expr; card._get = () => { const o = {resource: res.value, scalar: +scalar.value}; @@ -555,6 +558,10 @@ document.getElementById("terms").append(card); toggle(); } + function addTerms(terms = {}) { + Object.entries(terms).forEach(([resource, scalar]) => + addTerm({resource, scalar})); + } // Eval the expression ONCE into a function, then call it over the amounts // the lookup table needs (0..max_resource). @@ -666,7 +673,7 @@ out.append(el("h2", {}, "Solution")); out.append(el("p", { html: - `Status: ${s.status}   Objective: ${s.objective_value}   ` + + `Status: ${s.status}   Objective: ${s.objective_value ?? "—"}   ` + `Final renown total: ${s.final_renown_total}` })); @@ -711,19 +718,19 @@ } // --- seed with the example problem --- - addCity({name: "Aridias", type: "hub", renown: 2, adjacent: ["Bearhearth"]}); - addCity({name: "Bearhearth", type: "foundry", renown: 2, vat_steel: 3, vat_brass: 2, vat_electrum: 1, adjacent: ["Kingsland"]}); - addCity({name: "Kingsland", type: "metropolis", renown: 4, can_renovate: false, adjacent: ["Roseward"]}); - addCity({name: "Roseward", type: "monument", renown: 2}); + addCity({name: "0", type: "hub", renown: 2}); + addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1}); + addCity({name: "2", type: "hub", renown: 2}); + addCity({name: "3", type: "foundry", renown: 2}); + addCity({name: "4", type: "monument", renown: 2}); addAgent({kind: "Planner"}); - Object.entries({ + addTerms({ "renown": 0, "luxuries": 1, "steel": 2, "brass": 1, "electrum": 2 - }).forEach(([resource, scalar]) => - addTerm({resource, scalar})); + }) diff --git a/solve.py b/solve.py index 011754e..5eda9d9 100644 --- a/solve.py +++ b/solve.py @@ -394,7 +394,7 @@ class CityTurnPlan: @dataclass class Solution: status: str - objective_value: float + objective_value: float | None final_resources: dict[str, float] final_renown_total: int plan: list[CityTurnPlan] = field(default_factory=list) @@ -1277,7 +1277,7 @@ def solve(problem: Problem, max_time_seconds: float = 30.0, if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE): return Solution( - status=status_name, objective_value=float("nan"), + status=status_name, objective_value=None, final_resources={}, final_renown_total=0, plan=[], )