New planner meta incorporated into the model
This commit is contained in:
parent
4f01b3f1af
commit
6ebfeddf81
3 changed files with 139 additions and 39 deletions
15
README.md
15
README.md
|
|
@ -78,6 +78,7 @@ for step in solution.plan:
|
|||
"upgrades": [], // already-installed upgrade keys
|
||||
"available_turns": null, // null = all turns, or e.g. [0,2,4]
|
||||
"can_renovate": true, // metropolis must be false
|
||||
"overwork_pending": false, // starts holding an Overwork charge
|
||||
"forced_action": {"1": "renovate"} // turn -> action (hard constraint)
|
||||
}
|
||||
],
|
||||
|
|
@ -111,8 +112,15 @@ Upgrade keys: `infrastructure`, `harvester`, `fine_dining`, `overflow_vats`,
|
|||
Monument/Metropolis renown + trade goods), with the **1 Capital** cost.
|
||||
- **Foundry vats**: collecting a vat yields its level, empties it, and adds +1
|
||||
to the other two (full stateful per-turn model; *Overflow Vats* adds +1 more).
|
||||
- **Overwork** (Planner Governor): doubles a city's collection that turn, waives
|
||||
the Capital cost, and **locks** that city out of collecting the next turn.
|
||||
- **Overwork** (Planner Governor): an Overworked collection is doubled, has its
|
||||
Capital cost waived, and **locks** that city out of collecting the next turn.
|
||||
Appointing the Planner *arms* the city rather than necessarily firing that
|
||||
turn: the charge may be spent by that same turn's collection (the classic
|
||||
play) or held while the city collects normally, acts, or idles — in which case
|
||||
the city's **next** collection is the Overworked one, whether or not the
|
||||
Planner is still there. A held charge is not optional (the next collection
|
||||
spends it) and charges do not stack. `City.overwork_pending` seeds a city that
|
||||
entered the horizon already holding one.
|
||||
- **Upgrades** with Steel costs (Infrastructure 0 / Harvester 2 / type-specific
|
||||
2 / Fortification 4), Infrastructure's −1 future-cost discount, and the +1/+2
|
||||
Renown each grants. *Harvester*, *Fine Dining*, *Transit Authority* yield
|
||||
|
|
@ -146,7 +154,8 @@ Upgrade keys: `infrastructure`, `harvester`, `fine_dining`, `overflow_vats`,
|
|||
"final_renown_total": 35,
|
||||
"plan": [
|
||||
{"turn": 0, "city": "Aridias", "action": "collect",
|
||||
"detail": "hub: +2 luxuries", "governor": "", "overwork": false}
|
||||
"detail": "hub: +2 luxuries", "governor": "", "overwork": false,
|
||||
"overwork_armed": false} // armed = the Planner charged this city here
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
|
|||
14
index.html
14
index.html
|
|
@ -645,6 +645,10 @@
|
|||
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;
|
||||
// Mid-game restore: the Planner already governed this city and it has
|
||||
// not collected since, so its first collection here is overworked.
|
||||
const owPend = el("input", {type: "checkbox"});
|
||||
owPend.checked = !!c.overwork_pending;
|
||||
const adjacent = el("input", {
|
||||
value: (c.adjacent || []).join(", "),
|
||||
placeholder: "Bearhearth, Kingsland"
|
||||
|
|
@ -712,6 +716,7 @@
|
|||
const u = allowedUpgrades().filter(name => upBoxes[name] && upBoxes[name].checked);
|
||||
if (u.length) o.upgrades = u;
|
||||
if (!reno.checked) o.can_renovate = false;
|
||||
if (owPend.checked) o.overwork_pending = true;
|
||||
const adj = parseStrs(adjacent.value);
|
||||
if (adj) o.adjacent = adj;
|
||||
const fa = parsePairs(forced.value);
|
||||
|
|
@ -733,6 +738,7 @@
|
|||
el("div", {class: "stack"}, [
|
||||
field("Type", type),
|
||||
checkField("Can renovate", reno),
|
||||
checkField("Overwork pending", owPend),
|
||||
]),
|
||||
field("Renown", renown),
|
||||
field("Upgrades (already installed)", upWrap),
|
||||
|
|
@ -1422,10 +1428,16 @@
|
|||
class: "mini", type: "button",
|
||||
onclick: () => downloadPlanCsv(s, n),
|
||||
}, "Download CSV")));
|
||||
// Overwork column: the Planner *arms* a city, and the charge is
|
||||
// spent by that city's next Collection - which may be a later
|
||||
// turn, with the Planner long gone.
|
||||
const owCell = p => p.overwork
|
||||
? (p.overwork_armed ? "yes (armed here)" : "yes (armed earlier)")
|
||||
: (p.overwork_armed ? "armed - fires on next collect" : "");
|
||||
out.append(gridTable(
|
||||
["Turn", "City", "Action", "Detail", "Governor", "Overwork"],
|
||||
s.plan.map(p => [p.turn, p.city, p.action, p.detail, p.governor,
|
||||
p.overwork ? "yes" : ""])));
|
||||
owCell(p)])));
|
||||
} else {
|
||||
out.append(el("p", {}, "(no actions / no feasible plan)"));
|
||||
}
|
||||
|
|
|
|||
149
solve.py
149
solve.py
|
|
@ -37,11 +37,30 @@ Upgrades (Steel cost in brackets, each grants +1 City Renown unless noted):
|
|||
|
||||
Governors / Overwork:
|
||||
|
||||
The Planner Leader has *Overwork*: when appointed Governor of a City it
|
||||
doubles that City's Collection that Turn and waives the Capital cost, but
|
||||
the City cannot Collect the following Turn. The Planner can govern at most
|
||||
one City per Turn. Generic governor Agents may also grant a free Upgrade
|
||||
or bonus Trade Goods on Collect.
|
||||
The Planner Leader has *Overwork*: an Overworked Collection is doubled and
|
||||
its Capital cost waived, but the City cannot Collect the following Turn.
|
||||
The Planner can govern at most one City per Turn.
|
||||
|
||||
Appointing the Planner Governor of a City *arms* that City rather than
|
||||
necessarily firing that Turn, which lets the model cover every legal
|
||||
timing:
|
||||
|
||||
* arm and spend the same Turn - the classic case: the Planner governs a
|
||||
Collecting City and that Collection is the doubled one;
|
||||
* arm and hold - the Planner governs a City that Collects normally (or
|
||||
takes some other Action, or is idle) and the charge carries forward;
|
||||
the City's *next* Collection is the Overworked one, whether or not the
|
||||
Planner is still there.
|
||||
|
||||
A held charge is not optional: once armed, the very next Collection that
|
||||
City makes is Overworked (and is followed by the no-Collect lock). Charges
|
||||
never stack, so the model does not offer arming a City that is already
|
||||
holding one (re-arming would buy nothing anyway).
|
||||
``City.overwork_pending`` seeds a City that entered the horizon already
|
||||
holding a charge.
|
||||
|
||||
Generic governor Agents may also grant a free Upgrade or bonus Trade Goods
|
||||
on Collect.
|
||||
|
||||
Other modeled industrial Governor Agents (see Agent named constructors):
|
||||
Baron (+Trade Goods/Bastion), Builder (free type-specific Upgrade),
|
||||
|
|
@ -181,6 +200,10 @@ class City:
|
|||
adjacent: list[str] = field(default_factory=list)
|
||||
available_turns: Optional[list[int]] = None # None => all turns
|
||||
can_renovate: bool = True # Metropolis cannot renovate
|
||||
# Start state: the City is already holding an unspent Overwork charge (the
|
||||
# Planner governed it before the horizon began and it has not Collected
|
||||
# since). Its first Collection in the horizon is then Overworked.
|
||||
overwork_pending: bool = False
|
||||
# Hard constraint: force a specific action on a given turn (turn -> Action).
|
||||
forced_action: dict[int, str] = field(default_factory=dict)
|
||||
# Soft hint (NOT a constraint): a suggested action on a given turn
|
||||
|
|
@ -204,7 +227,7 @@ class Agent:
|
|||
agents can be expressed with the generic effect flags.
|
||||
"""
|
||||
name: str
|
||||
overwork: bool = False # Planner: double + waive cost
|
||||
overwork: bool = False # Planner: arms Overwork on the governed City
|
||||
free_upgrade: bool = False # e.g. Brotherhood Builder
|
||||
bonus_trade_goods: int = 0 # e.g. Baron: +N Trade Goods on collect
|
||||
# --- additional industrial Governor effects (see named constructors) ---
|
||||
|
|
@ -234,8 +257,9 @@ class Agent:
|
|||
|
||||
@classmethod
|
||||
def planner(cls, name: str = "Planner", **kw) -> "Agent":
|
||||
"""Faction Planner Leader: Overwork (double collection, waive Capital
|
||||
cost, lock next-Turn collect on the governed City)."""
|
||||
"""Faction Planner Leader: arms Overwork on the governed City. The
|
||||
armed Collection (this Turn's, or the City's next one) is doubled with
|
||||
its Capital cost waived, and locks the following Turn's Collect."""
|
||||
return cls(name=name, overwork=True, **kw)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -430,7 +454,13 @@ class CityTurnPlan:
|
|||
action: str
|
||||
detail: str = "" # e.g. collect choice, upgrade name, renovate target
|
||||
governor: str = "" # agent appointed governor (if any)
|
||||
# This Turn's Collection is the Overworked one (doubled, Capital waived).
|
||||
overwork: bool = False
|
||||
# The Planner armed an Overwork charge here this Turn. Together with
|
||||
# ``overwork``: armed+overwork = armed and spent now; armed alone = the
|
||||
# charge is held for this City's next Collection; overwork alone = it is
|
||||
# spending a charge armed on an earlier Turn.
|
||||
overwork_armed: bool = False
|
||||
# Net resource change produced by this Action (resource -> amount). Only the
|
||||
# stockpiled resources this City's Action affects appear here.
|
||||
deltas: dict[str, float] = field(default_factory=dict)
|
||||
|
|
@ -500,8 +530,16 @@ class _Builder:
|
|||
self.act: dict[tuple[int, int, Action], cp_model.IntVar] = {}
|
||||
# governor assignment: gov[(agent_idx, city_idx, turn)] bool
|
||||
self.gov: dict[tuple[int, int, int], cp_model.IntVar] = {}
|
||||
# overwork[(city_idx, turn)] bool (planner governs & overworks this city)
|
||||
# overwork[(city_idx, turn)] bool: this City's Collection on this Turn
|
||||
# is the Overworked one (doubled yield, Capital cost waived).
|
||||
self.overwork: dict[tuple[int, int], cp_model.IntVar] = {}
|
||||
# ow_arm[(city_idx, turn)] bool: the Planner governs this City this Turn
|
||||
# and so arms an Overwork charge on it.
|
||||
self.ow_arm: dict[tuple[int, int], cp_model.IntVar] = {}
|
||||
# ow_pending[(city_idx, turn)] bool: an armed-but-unspent charge is
|
||||
# carried *into* this Turn. Defined for turns 0..T (T = after the last
|
||||
# Turn); ow_pending[(ci, 0)] comes from City.overwork_pending.
|
||||
self.ow_pending: dict[tuple[int, int], cp_model.IntVar] = {}
|
||||
# final renown per city
|
||||
self.city_final_renown: list[cp_model.IntVar] = []
|
||||
# airship launch booleans (each adds AIRSHIP_RENOWN to asset renown)
|
||||
|
|
@ -619,24 +657,68 @@ class _Builder:
|
|||
if govs:
|
||||
m.Add(sum(govs) <= 1)
|
||||
|
||||
# Overwork bool: planner (overwork agent) governs this city.
|
||||
self._build_overwork(cities, agents)
|
||||
|
||||
def _build_overwork(self, cities: list[City], agents: list[Agent]):
|
||||
"""Overwork as an armed-charge state machine, one per City.
|
||||
|
||||
Appointing the Planner (an ``overwork`` Agent) Governor *arms* the City
|
||||
(``ow_arm``). A charge sits on the City (``ow_pending``) until a
|
||||
Collection spends it, and the Collection that spends it is the
|
||||
Overworked one (``overwork``): doubled, Capital cost waived, and the
|
||||
following Turn's Collect is locked out.
|
||||
|
||||
The charge may be spent by the Collection on the very Turn it is armed
|
||||
(the classic Planner play) or held for a later one - the Planner does
|
||||
not have to still be there, and the City is free to Collect normally,
|
||||
act, or idle on the arming Turn. What it cannot do is *skip* a charge:
|
||||
a carried charge is spent by the next Collection, whichever Turn that
|
||||
falls on.
|
||||
|
||||
The flow equation ``pending' = pending + arm - overwork`` over 0/1
|
||||
variables is what ties this together; because ``pending`` is a Bool it
|
||||
also rules out arming a City that is already holding a charge (unless
|
||||
that Turn's Collection spends it), which would be wasted anyway.
|
||||
"""
|
||||
m = self.m
|
||||
overwork_agents = [ai for ai, a in enumerate(agents) if a.overwork]
|
||||
for ci in range(len(cities)):
|
||||
for ci, city in enumerate(cities):
|
||||
for t in range(self.T):
|
||||
contrib = [
|
||||
self.gov[(ai, ci, t)]
|
||||
for ai in overwork_agents
|
||||
if (ai, ci, t) in self.gov
|
||||
]
|
||||
ow = m.NewBoolVar(f"overwork_c{ci}_t{t}")
|
||||
if contrib:
|
||||
m.Add(ow == sum(contrib)) # at most one planner total
|
||||
else:
|
||||
m.Add(ow == 0)
|
||||
self.overwork[(ci, t)] = ow
|
||||
arm = m.NewBoolVar(f"owarm_c{ci}_t{t}")
|
||||
# At most one Planner can be here: each governs <=1 City and
|
||||
# each City has <=1 Governor, so the sum is already 0/1.
|
||||
m.Add(arm == sum(contrib) if contrib else arm == 0)
|
||||
self.ow_arm[(ci, t)] = arm
|
||||
self.overwork[(ci, t)] = m.NewBoolVar(f"overwork_c{ci}_t{t}")
|
||||
for t in range(self.T + 1):
|
||||
self.ow_pending[(ci, t)] = m.NewBoolVar(f"owpend_c{ci}_t{t}")
|
||||
# Charge carried in from before the horizon (session restore).
|
||||
m.Add(self.ow_pending[(ci, 0)] == int(city.overwork_pending))
|
||||
|
||||
# Only one overworking placement at a time is already implied by
|
||||
# the agent's "<=1 city" constraint.
|
||||
for t in range(self.T):
|
||||
arm = self.ow_arm[(ci, t)]
|
||||
ow = self.overwork[(ci, t)]
|
||||
pend = self.ow_pending[(ci, t)]
|
||||
collect = self.act[(ci, t, Action.COLLECT)]
|
||||
# Only a Collection can be Overworked, and only with a charge
|
||||
# (armed this Turn or carried in).
|
||||
m.Add(ow <= collect)
|
||||
m.Add(ow <= pend + arm)
|
||||
# A carried charge is not optional: Collecting spends it.
|
||||
m.Add(ow >= pend + collect - 1)
|
||||
# Charge flow. ``ow_pending`` being a Bool caps this at one
|
||||
# charge per City at a time.
|
||||
m.Add(self.ow_pending[(ci, t + 1)] == pend + arm - ow)
|
||||
# An Overworked Collection locks the next Turn's Collect.
|
||||
if t + 1 < self.T:
|
||||
m.Add(
|
||||
self.act[(ci, t + 1, Action.COLLECT)] == 0
|
||||
).OnlyEnforceIf(ow)
|
||||
|
||||
def _build_hints(self):
|
||||
"""Apply soft solution hints via ``model.AddHint``. Unlike the forced_*
|
||||
|
|
@ -927,17 +1009,8 @@ class _Builder:
|
|||
refund = self._mul_bool(capped, prodigy, cap)
|
||||
self._add_delta("steel", t, refund)
|
||||
|
||||
# --- Overwork "no collect next turn" lock --------------------------
|
||||
for t in range(T - 1):
|
||||
# if overworked at t, cannot collect at t+1
|
||||
m.Add(self.act[(ci, t + 1, Action.COLLECT)] == 0).OnlyEnforceIf(
|
||||
self.overwork[(ci, t)]
|
||||
)
|
||||
# Overwork requires a collection this turn (otherwise pointless, and a
|
||||
# governor that overworks implies the city collects).
|
||||
for t in range(T):
|
||||
m.Add(self.act[(ci, t, Action.COLLECT)] >= self.overwork[(ci, t)])
|
||||
|
||||
# --- Overwork: arming, spending and the "no collect next turn" lock
|
||||
# all live in _build_overwork, which runs before this.
|
||||
# --- Launches need available steel handled by balance; renown -------
|
||||
# --- Final renown of the city --------------------------------------
|
||||
self._build_city_renown(ci, city, installed, type_active)
|
||||
|
|
@ -1061,8 +1134,11 @@ class _Builder:
|
|||
|
||||
# ---- per-Collection bonuses from governor agents ---------------
|
||||
# Each is applied whenever the City Collects (any type) while
|
||||
# governed. A bonus Governor occupies the City's only Governor slot,
|
||||
# so these never co-occur with the Planner's Overwork doubling.
|
||||
# governed. These are *not* doubled by Overwork: the Governor's
|
||||
# bonus is the Agent's own grant, not part of the City's Collection
|
||||
# yield. (Overwork can now co-occur with one of these, since a
|
||||
# charge armed on an earlier Turn is spent by a Collection that a
|
||||
# different Agent may be governing.)
|
||||
# Baron -> +N Trade Goods (N = bonus per Bastion)
|
||||
# Artificer -> +1 Trade Good
|
||||
# Capitalist -> +2 Capital (only on a Hub collecting Capital)
|
||||
|
|
@ -1610,12 +1686,15 @@ def _extract(problem: Problem, b: _Builder, solver, status_name: str) -> Solutio
|
|||
(ci, t) in b._foreman_renov
|
||||
and bool(solver.Value(b._foreman_renov[(ci, t)]))
|
||||
)
|
||||
overwork = bool(solver.Value(b.overwork[(ci, t)]))
|
||||
# The Planner can arm a City that is idling this Turn, so an
|
||||
# otherwise-empty row still has to be reported.
|
||||
armed = bool(solver.Value(b.ow_arm[(ci, t)]))
|
||||
if (chosen is None or chosen == Action.IDLE) and not foreman_renov \
|
||||
and (ci, t) not in extra:
|
||||
and not armed and (ci, t) not in extra:
|
||||
continue
|
||||
detail = ""
|
||||
governor = ""
|
||||
overwork = bool(solver.Value(b.overwork[(ci, t)]))
|
||||
# governor name
|
||||
for ai, agent in enumerate(problem.agents):
|
||||
if (ai, ci, t) in b.gov and solver.Value(b.gov[(ai, ci, t)]) == 1:
|
||||
|
|
@ -1676,7 +1755,7 @@ def _extract(problem: Problem, b: _Builder, solver, status_name: str) -> Solutio
|
|||
plan.append(CityTurnPlan(
|
||||
turn=t, city=city.name, action=action_label,
|
||||
detail=detail, governor=governor, overwork=overwork,
|
||||
deltas=deltas,
|
||||
overwork_armed=armed, deltas=deltas,
|
||||
))
|
||||
|
||||
def _unscale(r, raw):
|
||||
|
|
|
|||
Loading…
Reference in a new issue