more frontend refactoring

This commit is contained in:
Pagwin 2026-06-17 15:41:05 -04:00
parent dd99ed8a23
commit 2e50f61e57
3 changed files with 26 additions and 18 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
__pycache__ __pycache__
.venv .venv
*.pdf

View file

@ -162,10 +162,6 @@
display: flex; display: flex;
} }
.card.log-on .linear-only {
opacity: .4;
}
/* Output tables: a CSS grid kept inside a scroll container so it never /* Output tables: a CSS grid kept inside a scroll container so it never
stretches the page wider than it should. */ stretches the page wider than it should. */
.gtable-wrap { .gtable-wrap {
@ -406,13 +402,20 @@
} }
// --- cities --- // --- 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 = {}) { function addCity(c = {}) {
const card = el("div", {class: "card"}); 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 type = selectEl(CITY_TYPES, c.type || "hub");
const renown = num(c.renown ?? "", {min: 1, placeholder: "auto"}); 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}), const vs = num(c.vat_steel || 1, {min: 0}), vb = num(c.vat_brass || 1, {min: 0}),
ve = num(c.vat_electrum || 0, {min: 0}); ve = num(c.vat_electrum || 1, {min: 0});
const reno = el("input", {type: "checkbox"}); reno.checked = c.can_renovate !== false; const reno = el("input", {type: "checkbox"}); reno.checked = c.can_renovate !== false;
const adjacent = el("input", { const adjacent = el("input", {
value: (c.adjacent || []).join(", "), value: (c.adjacent || []).join(", "),
@ -515,7 +518,6 @@
}; };
card.append( card.append(
field("Agent", type), field("Agent", type),
field("Name", name),
field("Effect", desc), field("Effect", desc),
field("Bastions (Baron only)", bastions), field("Bastions (Baron only)", bastions),
field("Forced city (turn:city, csv)", forced), field("Forced city (turn:city, csv)", forced),
@ -535,6 +537,7 @@
const toggle = () => card.classList.toggle("log-on", isLog.checked); const toggle = () => card.classList.toggle("log-on", isLog.checked);
isLog.onchange = toggle; isLog.onchange = toggle;
const expr = el("textarea", {rows: 1, placeholder: "(x) => Math.log2(x + 1)"}); 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; if (t._expr) expr.value = t._expr;
card._get = () => { card._get = () => {
const o = {resource: res.value, scalar: +scalar.value}; const o = {resource: res.value, scalar: +scalar.value};
@ -555,6 +558,10 @@
document.getElementById("terms").append(card); document.getElementById("terms").append(card);
toggle(); 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 // Eval the expression ONCE into a function, then call it over the amounts
// the lookup table needs (0..max_resource). // the lookup table needs (0..max_resource).
@ -666,7 +673,7 @@
out.append(el("h2", {}, "Solution")); out.append(el("h2", {}, "Solution"));
out.append(el("p", { out.append(el("p", {
html: html:
`<b>Status:</b> ${s.status} &nbsp; <b>Objective:</b> ${s.objective_value} &nbsp; ` + `<b>Status:</b> ${s.status} &nbsp; <b>Objective:</b> ${s.objective_value ?? "—"} &nbsp; ` +
`<b>Final renown total:</b> ${s.final_renown_total}` `<b>Final renown total:</b> ${s.final_renown_total}`
})); }));
@ -711,19 +718,19 @@
} }
// --- seed with the example problem --- // --- seed with the example problem ---
addCity({name: "Aridias", type: "hub", renown: 2, adjacent: ["Bearhearth"]}); addCity({name: "0", type: "hub", renown: 2});
addCity({name: "Bearhearth", type: "foundry", renown: 2, vat_steel: 3, vat_brass: 2, vat_electrum: 1, adjacent: ["Kingsland"]}); addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1});
addCity({name: "Kingsland", type: "metropolis", renown: 4, can_renovate: false, adjacent: ["Roseward"]}); addCity({name: "2", type: "hub", renown: 2});
addCity({name: "Roseward", type: "monument", renown: 2}); addCity({name: "3", type: "foundry", renown: 2});
addCity({name: "4", type: "monument", renown: 2});
addAgent({kind: "Planner"}); addAgent({kind: "Planner"});
Object.entries({ addTerms({
"renown": 0, "renown": 0,
"luxuries": 1, "luxuries": 1,
"steel": 2, "steel": 2,
"brass": 1, "brass": 1,
"electrum": 2 "electrum": 2
}).forEach(([resource, scalar]) => })
addTerm({resource, scalar}));
</script> </script>
</body> </body>

View file

@ -394,7 +394,7 @@ class CityTurnPlan:
@dataclass @dataclass
class Solution: class Solution:
status: str status: str
objective_value: float objective_value: float | None
final_resources: dict[str, float] final_resources: dict[str, float]
final_renown_total: int final_renown_total: int
plan: list[CityTurnPlan] = field(default_factory=list) 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): if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
return Solution( return Solution(
status=status_name, objective_value=float("nan"), status=status_name, objective_value=None,
final_resources={}, final_renown_total=0, plan=[], final_resources={}, final_renown_total=0, plan=[],
) )