added adjacency for industrialist
This commit is contained in:
parent
1a27e9048b
commit
e350036b1b
2 changed files with 66 additions and 11 deletions
19
main.py
19
main.py
|
|
@ -100,7 +100,9 @@ INDEX_HTML = r"""<!DOCTYPE html>
|
|||
<legend>Cities</legend>
|
||||
<p class="help">The <b>name</b> is the city's unique identifier — it labels the
|
||||
output plan and is what an agent's "forced city" refers to. <b>Upgrades</b> are the
|
||||
ones already installed at the start; the available choices follow the city's type.</p>
|
||||
ones already installed at the start; the available choices follow the city's type.
|
||||
<b>Adjacent cities</b> lists neighbours by name (adjacency is symmetric); an
|
||||
Industrialist Governor grants free Infrastructure to its city and every adjacent one.</p>
|
||||
<div id="cities" class="cards"></div>
|
||||
<button class="mini" type="button" onclick="addCity()">+ city</button>
|
||||
</fieldset>
|
||||
|
|
@ -279,6 +281,8 @@ function addCity(c={}) {
|
|||
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 reno = el("input", {type:"checkbox"}); reno.checked = c.can_renovate !== false;
|
||||
const adjacent = el("input", {value:(c.adjacent||[]).join(", "),
|
||||
placeholder:"Bearhearth, Kingsland"});
|
||||
const forced = el("input", {value:"", placeholder:"0:upgrade"});
|
||||
const avail = el("input", {value:"", placeholder:""});
|
||||
|
||||
|
|
@ -315,6 +319,8 @@ function addCity(c={}) {
|
|||
const u = allowedUpgrades().filter(name => upBoxes[name] && upBoxes[name].checked);
|
||||
if (u.length) o.upgrades = u;
|
||||
if (!reno.checked) o.can_renovate = false;
|
||||
const adj = parseStrs(adjacent.value);
|
||||
if (adj) o.adjacent = adj;
|
||||
const fa = parsePairs(forced.value);
|
||||
if (Object.keys(fa).length) o.forced_action = fa;
|
||||
const at = parseInts(avail.value);
|
||||
|
|
@ -330,6 +336,7 @@ function addCity(c={}) {
|
|||
field("Vat electrum", ve),
|
||||
field("Upgrades (already installed)", upWrap),
|
||||
checkField("Can renovate", reno),
|
||||
field("Adjacent cities (csv of names)", adjacent),
|
||||
field("Forced actions (turn:action, csv)", forced),
|
||||
field("Avail turns (csv, blank=all)", avail),
|
||||
el("div", {class:"card-actions"}, removeBtn(card)));
|
||||
|
|
@ -460,6 +467,10 @@ function parseInts(s) {
|
|||
const out = s.split(",").map(x=>x.trim()).filter(Boolean).map(Number);
|
||||
return out.length ? out : null;
|
||||
}
|
||||
function parseStrs(s) {
|
||||
const out = s.split(",").map(x=>x.trim()).filter(Boolean);
|
||||
return out.length ? out : null;
|
||||
}
|
||||
function parsePairs(s, valueIsString=false) {
|
||||
const o = {};
|
||||
for (const part of s.split(",").map(x=>x.trim()).filter(Boolean)) {
|
||||
|
|
@ -563,9 +574,9 @@ function fmtNum(v) {
|
|||
}
|
||||
|
||||
// --- seed with the example problem ---
|
||||
addCity({name:"Aridias", type:"hub", renown:2});
|
||||
addCity({name:"Bearhearth", type:"foundry", renown:2, vat_steel:3, vat_brass:2, vat_electrum:1});
|
||||
addCity({name:"Kingsland", type:"metropolis", renown:4, can_renovate:false});
|
||||
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});
|
||||
addAgent({kind:"Planner"});
|
||||
[["renown",5],["capital",1],["luxuries",1],["steel",1],["brass",1],
|
||||
|
|
|
|||
58
solve.py
58
solve.py
|
|
@ -47,7 +47,8 @@ Governors / Overwork:
|
|||
Baron (+Trade Goods/Bastion), Builder (free type-specific Upgrade),
|
||||
Capitalist (+2 Capital), Vinter (+2 Luxuries), Artificer (+1 Trade Good),
|
||||
Metallurgist (Overflow Vats on a Foundry), Industrialist (free
|
||||
Infrastructure), Foreman (Renovate without spending the Action), Prodigy
|
||||
Infrastructure for the governed City *and every adjacent City*), Foreman
|
||||
(Renovate without spending the Action), Prodigy
|
||||
(refund <=2 Steel on Upgrade/Launch), Provisioner (+1.5 Electrum per
|
||||
governing Turn), Courier (one-time +3 Capital/Steel/Brass).
|
||||
|
||||
|
|
@ -165,6 +166,11 @@ class City:
|
|||
vat_brass: int = 0
|
||||
vat_electrum: int = 0
|
||||
upgrades: list[str] = field(default_factory=list) # already-installed
|
||||
# Names of Cities adjacent to this one on the map. Adjacency is treated as
|
||||
# symmetric (declaring A adjacent to B implies B adjacent to A) and is used
|
||||
# by the Industrialist Governor, whose free Infrastructure Upgrade spreads
|
||||
# to every City adjacent to the one being governed.
|
||||
adjacent: list[str] = field(default_factory=list)
|
||||
available_turns: Optional[list[int]] = None # None => all turns
|
||||
can_renovate: bool = True # Metropolis cannot renovate
|
||||
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
||||
|
|
@ -252,9 +258,9 @@ class Agent:
|
|||
|
||||
@classmethod
|
||||
def industrialist(cls, name: str = "Industrialist", **kw) -> "Agent":
|
||||
"""Industrialist: when appointed Governor, their City gains the
|
||||
Infrastructure Upgrade for free (the adjacent-City grant is not
|
||||
modeled, as the map layout is outside this resource model)."""
|
||||
"""Industrialist: when appointed Governor, their City and every City
|
||||
adjacent to it (see ``City.adjacent``) gain the Infrastructure Upgrade
|
||||
for free."""
|
||||
return cls(name=name, grants_infrastructure=True, **kw)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -448,6 +454,7 @@ class _Builder:
|
|||
# -- main build -------------------------------------------------------- #
|
||||
|
||||
def build(self):
|
||||
self._build_adjacency()
|
||||
self._build_actions_and_governors()
|
||||
self._build_city_dynamics()
|
||||
self._build_trade_conversion()
|
||||
|
|
@ -543,6 +550,23 @@ class _Builder:
|
|||
# Only one overworking placement at a time is already implied by
|
||||
# the agent's "<=1 city" constraint.
|
||||
|
||||
def _build_adjacency(self):
|
||||
"""Build the symmetric City adjacency map (``self.adj[ci]`` -> set of
|
||||
adjacent City indices). Declaring A adjacent to B also makes B adjacent
|
||||
to A, so callers need only list each edge once."""
|
||||
name_to_idx = {c.name: i for i, c in enumerate(self.p.cities)}
|
||||
self.adj: dict[int, set[int]] = {i: set() for i in range(len(self.p.cities))}
|
||||
for i, c in enumerate(self.p.cities):
|
||||
for nm in c.adjacent or []:
|
||||
if nm not in name_to_idx:
|
||||
raise KeyError(
|
||||
f"City {c.name!r} is adjacent to unknown City {nm!r}")
|
||||
j = name_to_idx[nm]
|
||||
if j == i:
|
||||
continue
|
||||
self.adj[i].add(j)
|
||||
self.adj[j].add(i)
|
||||
|
||||
def _city_index(self, name: str) -> int:
|
||||
for i, c in enumerate(self.p.cities):
|
||||
if c.name == name:
|
||||
|
|
@ -694,9 +718,9 @@ class _Builder:
|
|||
ni = ai + bf
|
||||
if u == "infrastructure":
|
||||
# Industrialist Governor: installs Infrastructure for free
|
||||
# this turn, regardless of the City's Action.
|
||||
inf_src = self._gov_attr_bool(
|
||||
ci, t, lambda a: a.grants_infrastructure, "indinfra")
|
||||
# this turn, regardless of the City's Action. The grant
|
||||
# spreads from the governed City to every adjacent City.
|
||||
inf_src = self._industrialist_infra_bool(ci, t)
|
||||
if inf_src is not None:
|
||||
bf = m.NewBoolVar(f"indinfrafree_{ci}_t{t}")
|
||||
# bf = inf_src AND not already installed
|
||||
|
|
@ -811,6 +835,26 @@ class _Builder:
|
|||
def _free_upgrade_bool(self, ci: int, t: int):
|
||||
return self._gov_attr_bool(ci, t, lambda a: a.free_upgrade, "freeup")
|
||||
|
||||
def _industrialist_infra_bool(self, ci: int, t: int):
|
||||
"""Bool that is 1 iff an Industrialist Governs City ``ci`` *or* any City
|
||||
adjacent to it on Turn ``t`` (each grants ``ci`` a free Infrastructure
|
||||
Upgrade). Returns None when no Industrialist could reach this City.
|
||||
|
||||
Multiple adjacent Cities may each be Governed by an Industrialist, so
|
||||
the contributions are OR'd (any one is enough) rather than summed."""
|
||||
sources = [ci] + sorted(self.adj[ci])
|
||||
contrib = [
|
||||
self.gov[(ai, cj, t)]
|
||||
for cj in sources
|
||||
for ai, a in enumerate(self.p.agents)
|
||||
if a.grants_infrastructure and (ai, cj, t) in self.gov
|
||||
]
|
||||
if not contrib:
|
||||
return None
|
||||
b = self.m.NewBoolVar(f"indinfra_c{ci}_t{t}")
|
||||
self.m.AddMaxEquality(b, contrib)
|
||||
return b
|
||||
|
||||
def _bonus_collect_expr(self, ci: int, t: int, attr: str):
|
||||
"""Sum of ``agent.<attr>`` over Agents governing City ``ci`` on Turn
|
||||
``t`` (each such Agent's per-Collection bonus). None if no contributor."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue