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>
|
<legend>Cities</legend>
|
||||||
<p class="help">The <b>name</b> is the city's unique identifier — it labels the
|
<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
|
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>
|
<div id="cities" class="cards"></div>
|
||||||
<button class="mini" type="button" onclick="addCity()">+ city</button>
|
<button class="mini" type="button" onclick="addCity()">+ city</button>
|
||||||
</fieldset>
|
</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}),
|
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});
|
ve = num(c.vat_electrum||0, {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", {value:(c.adjacent||[]).join(", "),
|
||||||
|
placeholder:"Bearhearth, Kingsland"});
|
||||||
const forced = el("input", {value:"", placeholder:"0:upgrade"});
|
const forced = el("input", {value:"", placeholder:"0:upgrade"});
|
||||||
const avail = el("input", {value:"", placeholder:""});
|
const avail = el("input", {value:"", placeholder:""});
|
||||||
|
|
||||||
|
|
@ -315,6 +319,8 @@ function addCity(c={}) {
|
||||||
const u = allowedUpgrades().filter(name => upBoxes[name] && upBoxes[name].checked);
|
const u = allowedUpgrades().filter(name => upBoxes[name] && upBoxes[name].checked);
|
||||||
if (u.length) o.upgrades = u;
|
if (u.length) o.upgrades = u;
|
||||||
if (!reno.checked) o.can_renovate = false;
|
if (!reno.checked) o.can_renovate = false;
|
||||||
|
const adj = parseStrs(adjacent.value);
|
||||||
|
if (adj) o.adjacent = adj;
|
||||||
const fa = parsePairs(forced.value);
|
const fa = parsePairs(forced.value);
|
||||||
if (Object.keys(fa).length) o.forced_action = fa;
|
if (Object.keys(fa).length) o.forced_action = fa;
|
||||||
const at = parseInts(avail.value);
|
const at = parseInts(avail.value);
|
||||||
|
|
@ -330,6 +336,7 @@ function addCity(c={}) {
|
||||||
field("Vat electrum", ve),
|
field("Vat electrum", ve),
|
||||||
field("Upgrades (already installed)", upWrap),
|
field("Upgrades (already installed)", upWrap),
|
||||||
checkField("Can renovate", reno),
|
checkField("Can renovate", reno),
|
||||||
|
field("Adjacent cities (csv of names)", adjacent),
|
||||||
field("Forced actions (turn:action, csv)", forced),
|
field("Forced actions (turn:action, csv)", forced),
|
||||||
field("Avail turns (csv, blank=all)", avail),
|
field("Avail turns (csv, blank=all)", avail),
|
||||||
el("div", {class:"card-actions"}, removeBtn(card)));
|
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);
|
const out = s.split(",").map(x=>x.trim()).filter(Boolean).map(Number);
|
||||||
return out.length ? out : null;
|
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) {
|
function parsePairs(s, valueIsString=false) {
|
||||||
const o = {};
|
const o = {};
|
||||||
for (const part of s.split(",").map(x=>x.trim()).filter(Boolean)) {
|
for (const part of s.split(",").map(x=>x.trim()).filter(Boolean)) {
|
||||||
|
|
@ -563,9 +574,9 @@ function fmtNum(v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- seed with the example problem ---
|
// --- seed with the example problem ---
|
||||||
addCity({name:"Aridias", type:"hub", renown:2});
|
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});
|
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});
|
addCity({name:"Kingsland", type:"metropolis", renown:4, can_renovate:false, adjacent:["Roseward"]});
|
||||||
addCity({name:"Roseward", type:"monument", renown:2});
|
addCity({name:"Roseward", type:"monument", renown:2});
|
||||||
addAgent({kind:"Planner"});
|
addAgent({kind:"Planner"});
|
||||||
[["renown",5],["capital",1],["luxuries",1],["steel",1],["brass",1],
|
[["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),
|
Baron (+Trade Goods/Bastion), Builder (free type-specific Upgrade),
|
||||||
Capitalist (+2 Capital), Vinter (+2 Luxuries), Artificer (+1 Trade Good),
|
Capitalist (+2 Capital), Vinter (+2 Luxuries), Artificer (+1 Trade Good),
|
||||||
Metallurgist (Overflow Vats on a Foundry), Industrialist (free
|
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
|
(refund <=2 Steel on Upgrade/Launch), Provisioner (+1.5 Electrum per
|
||||||
governing Turn), Courier (one-time +3 Capital/Steel/Brass).
|
governing Turn), Courier (one-time +3 Capital/Steel/Brass).
|
||||||
|
|
||||||
|
|
@ -165,6 +166,11 @@ class City:
|
||||||
vat_brass: int = 0
|
vat_brass: int = 0
|
||||||
vat_electrum: int = 0
|
vat_electrum: int = 0
|
||||||
upgrades: list[str] = field(default_factory=list) # already-installed
|
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
|
available_turns: Optional[list[int]] = None # None => all turns
|
||||||
can_renovate: bool = True # Metropolis cannot renovate
|
can_renovate: bool = True # Metropolis cannot renovate
|
||||||
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
||||||
|
|
@ -252,9 +258,9 @@ class Agent:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def industrialist(cls, name: str = "Industrialist", **kw) -> "Agent":
|
def industrialist(cls, name: str = "Industrialist", **kw) -> "Agent":
|
||||||
"""Industrialist: when appointed Governor, their City gains the
|
"""Industrialist: when appointed Governor, their City and every City
|
||||||
Infrastructure Upgrade for free (the adjacent-City grant is not
|
adjacent to it (see ``City.adjacent``) gain the Infrastructure Upgrade
|
||||||
modeled, as the map layout is outside this resource model)."""
|
for free."""
|
||||||
return cls(name=name, grants_infrastructure=True, **kw)
|
return cls(name=name, grants_infrastructure=True, **kw)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -448,6 +454,7 @@ class _Builder:
|
||||||
# -- main build -------------------------------------------------------- #
|
# -- main build -------------------------------------------------------- #
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
|
self._build_adjacency()
|
||||||
self._build_actions_and_governors()
|
self._build_actions_and_governors()
|
||||||
self._build_city_dynamics()
|
self._build_city_dynamics()
|
||||||
self._build_trade_conversion()
|
self._build_trade_conversion()
|
||||||
|
|
@ -543,6 +550,23 @@ class _Builder:
|
||||||
# Only one overworking placement at a time is already implied by
|
# Only one overworking placement at a time is already implied by
|
||||||
# the agent's "<=1 city" constraint.
|
# 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:
|
def _city_index(self, name: str) -> int:
|
||||||
for i, c in enumerate(self.p.cities):
|
for i, c in enumerate(self.p.cities):
|
||||||
if c.name == name:
|
if c.name == name:
|
||||||
|
|
@ -694,9 +718,9 @@ class _Builder:
|
||||||
ni = ai + bf
|
ni = ai + bf
|
||||||
if u == "infrastructure":
|
if u == "infrastructure":
|
||||||
# Industrialist Governor: installs Infrastructure for free
|
# Industrialist Governor: installs Infrastructure for free
|
||||||
# this turn, regardless of the City's Action.
|
# this turn, regardless of the City's Action. The grant
|
||||||
inf_src = self._gov_attr_bool(
|
# spreads from the governed City to every adjacent City.
|
||||||
ci, t, lambda a: a.grants_infrastructure, "indinfra")
|
inf_src = self._industrialist_infra_bool(ci, t)
|
||||||
if inf_src is not None:
|
if inf_src is not None:
|
||||||
bf = m.NewBoolVar(f"indinfrafree_{ci}_t{t}")
|
bf = m.NewBoolVar(f"indinfrafree_{ci}_t{t}")
|
||||||
# bf = inf_src AND not already installed
|
# bf = inf_src AND not already installed
|
||||||
|
|
@ -811,6 +835,26 @@ class _Builder:
|
||||||
def _free_upgrade_bool(self, ci: int, t: int):
|
def _free_upgrade_bool(self, ci: int, t: int):
|
||||||
return self._gov_attr_bool(ci, t, lambda a: a.free_upgrade, "freeup")
|
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):
|
def _bonus_collect_expr(self, ci: int, t: int, attr: str):
|
||||||
"""Sum of ``agent.<attr>`` over Agents governing City ``ci`` on Turn
|
"""Sum of ``agent.<attr>`` over Agents governing City ``ci`` on Turn
|
||||||
``t`` (each such Agent's per-Collection bonus). None if no contributor."""
|
``t`` (each such Agent's per-Collection bonus). None if no contributor."""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue