airship constraint

This commit is contained in:
Pagwin 2026-06-20 21:53:54 -04:00
parent 76c4a3555b
commit db9eb00a6f
2 changed files with 37 additions and 3 deletions

View file

@ -422,6 +422,10 @@
// Must match ELECTRUM_SCALE in solve.py.
const ELECTRUM_SCALE = 10;
const SCORE_KEYS = RESOURCES.concat(["renown"]);
// Constraints may additionally pin "airships": the cumulative count of
// Airships produced via Launch by the end of the chosen Turn. Not a
// scorable resource, so it's only offered for constraints.
const CONSTRAINT_KEYS = SCORE_KEYS.concat(["airships"]);
const CITY_TYPES = ["hub", "foundry", "monument", "metropolis"];
const ACTIONS = ["idle", "collect", "renovate", "upgrade", "launch"];
// Upgrades that apply to any city type, plus the type-specific "3rd" upgrade.
@ -851,7 +855,7 @@
// --- resource constraints ---
function addConstraint(c = {}) {
const card = el("div", {class: "card"});
const res = selectEl(SCORE_KEYS, c.resource || "capital");
const res = selectEl(CONSTRAINT_KEYS, c.resource || "capital");
const op = selectEl([">=", "<=", "=="], c.op || ">=");
const value = num(c.value ?? 0, {step: resStep(res.value)});
res.onchange = () => {value.step = resStep(res.value);};

View file

@ -387,7 +387,10 @@ class Problem:
max_vat: int = DEFAULT_MAX_VAT
# Hard constraints on a resource's amount at the END of a specific Turn.
# Each entry: {"turn": int, "resource": str, "op": one of >=/<=/==, "value": int}.
# Turns are 0-indexed (None or omitted => final Turn).
# ``resource`` is a stockpiled resource, "renown" (final Turn only), or
# "airships" (cumulative count of Airships produced by end of that Turn,
# incl. any ``airships_launched`` before the horizon). Turns are 0-indexed
# (None or omitted => final Turn).
resource_constraints: list[dict] = field(default_factory=list)
# Forced resource conversions on specific Turns. These are NOT decisions the
# optimizer makes - each one deterministically spends ``from_amount`` of one
@ -495,6 +498,12 @@ class _Builder:
self.city_final_renown: list[cp_model.IntVar] = []
# airship launch booleans (each adds AIRSHIP_RENOWN to asset renown)
self.launches: list[cp_model.IntVar] = []
# launch booleans indexed by the Turn they occur on, so a cumulative
# "airships produced by end of Turn t" count can be built for constraints.
self.launch_by_turn: dict[int, list] = {}
# cumulative airships produced by END of each Turn (incl. any launched
# before the horizon). Populated by _build_airship_counts.
self.airships_cum: list[cp_model.IntVar] = []
# bookkeeping for solution extraction
self._collect_detail: dict[tuple[int, int], dict] = {}
self._upgrade_choice: dict[tuple[int, int], dict] = {}
@ -524,6 +533,7 @@ class _Builder:
self._build_adjacency()
self._build_actions_and_governors()
self._build_city_dynamics()
self._build_airship_counts()
self._build_trade_conversion()
self._build_conversions()
self._build_optional_conversions()
@ -856,6 +866,7 @@ class _Builder:
launch = self.act[(ci, t, Action.LAUNCH)]
self._add_delta("steel", t, -AIRSHIP_COST_STEEL * launch)
self.launches.append(launch)
self.launch_by_turn.setdefault(t, []).append(launch)
steel_spent = steel_spent + AIRSHIP_COST_STEEL * launch
# Prodigy Governor: when Upgrading or Launching an Airship, refund
@ -1358,7 +1369,8 @@ class _Builder:
"""Resource amount var at END of ``turn`` (None => final Turn).
``resource`` may be "renown" only for the final Turn, since per-Turn
Renown is not tracked."""
Renown is not tracked. It may also be "airships" (the cumulative count
of Airships produced by the end of ``turn``), valid on any Turn."""
T = self.T
t = T - 1 if turn is None else turn
if not (0 <= t < T):
@ -1367,6 +1379,9 @@ class _Builder:
if turn is not None and turn != T - 1:
raise ValueError("'renown' is only available on the final Turn")
return self.renown_total
if resource == "airships":
# Cumulative Airships produced by END of this Turn (whole units).
return self.airships_cum[t]
if resource not in RESOURCES:
raise ValueError(f"Unknown resource: {resource!r}")
if resource == "electrum":
@ -1374,6 +1389,21 @@ class _Builder:
return self.electrum_eff[t]
return self.res[resource][t]
def _build_airship_counts(self):
"""Cumulative count of Airships produced by the END of each Turn.
``airships_cum[t]`` = ``airships_launched`` (produced before the
horizon) + every LAUNCH taken on Turns 0..t across all Cities. This is
the quantity a caller constrains via a ``resource_constraints`` entry
whose resource is ``"airships"`` (see ``_resource_at``)."""
m = self.m
cum_prev = self.p.airships_launched
for t in range(self.T):
cum = m.NewIntVar(0, AIRSHIP_MAX, f"airships_cum_t{t}")
m.Add(cum == cum_prev + sum(self.launch_by_turn.get(t, [])))
cum_prev = cum
self.airships_cum.append(cum)
def _build_renown_total(self):
m = self.m
# Each Faction can Launch at most 3 Airships total (incl. any already