5.9 KiB
dws-solve
A planner optimizer for the Days Without Strife Faction Planner role, built on Google OR-Tools (CP-SAT). It plans Industry Actions across the game's Turns to maximize a weighted score over the end-game resources.
What it optimizes
Given:
- the Cities a Faction controls (type, renown, foundry vats, installed upgrades),
- the Agents available to appoint as Governors (notably the Planner with Overwork),
- per-turn availability for cities and agents,
- optional hard constraints (force a city's action on a turn; force an agent to govern a particular city on a turn),
- starting resources,
it finds the sequence of per-city Industry Actions (Collect / Renovate / Upgrade / Launch Airship / Idle) that maximizes the objective.
Objective is one of two forms over the resources Renown, Luxuries, Capital, Steel, Brass, Electrum, Trade Goods, Express tickets:
- linear:
sum_n scalar_n * amount_n - log:
sum_n scalar_n * log_mapping[n][amount_n]wherelog_mappingis a caller-supplied lookup table (list indexed by integer resource amount).
"Renown" is scored as the total final Renown of controlled assets (each
City capped to 1..9), plus extra_renown for assets not modeled as Cities.
Usage
As a CLI (JSON in / JSON out)
uv run python solve.py input.json --time 30 -o plan.json
# or read stdin / write stdout
uv run python solve.py < input.json
As a module
from solve import Problem, City, Agent, Objective, CityType, solve
problem = Problem(
turns=5,
start={"capital": 3, "luxuries": 3, "steel": 3, "brass": 3, "electrum": 3},
cities=[
City("Aridias", CityType.HUB, renown=2),
City("Bearhearth", CityType.FOUNDRY, renown=2,
vat_steel=3, vat_brass=2, vat_electrum=1),
City("Kingsland", CityType.METROPOLIS, renown=4, can_renovate=False),
],
agents=[Agent("Planner", overwork=True)],
objective=Objective(mode="linear",
scalars={"renown": 5, "electrum": 2, "express": 3}),
)
solution = solve(problem, max_time_seconds=30)
print(solution.objective_value, solution.final_renown_total)
for step in solution.plan:
print(step)
Input JSON schema
{
"turns": 5,
"start": {"capital": 3, "luxuries": 3, "steel": 3, "brass": 3, "electrum": 3},
"extra_renown": 0, // renown of non-City assets
"tradeable_into": ["capital","luxuries","steel","brass","electrum"],
"max_resource": 300, "max_vat": 12, // accumulator bounds (optional)
"cities": [
{
"name": "Aridias", "type": "hub", "renown": 2,
"vat_steel": 0, "vat_brass": 0, "vat_electrum": 0, // foundry only
"upgrades": [], // already-installed upgrade keys
"available_turns": null, // null = all turns, or e.g. [0,2,4]
"can_renovate": true, // metropolis must be false
"forced_action": {"1": "renovate"} // turn -> action (hard constraint)
}
],
"agents": [
{
"name": "Planner", "overwork": true,
"free_upgrade": false, // e.g. Brotherhood Builder
"bonus_trade_goods": 0, // e.g. Baron
"available_turns": null,
"forced_city": {"0": "Mon1"} // turn -> city (hard constraint)
}
],
"objective": {
"mode": "linear", // or "log"
"scalars": {"renown": 5, "electrum": 2},
"log_mapping": { // required for "log" mode
"renown": [0,0,1,2,3,4,5,6,7,8,9]
}
}
}
Action keys: collect, renovate, upgrade, launch, idle.
City types: hub, foundry, monument, metropolis.
Upgrade keys: infrastructure, harvester, fine_dining, overflow_vats,
transit_authority, propaganda, fortification.
Modeled mechanics
-
Collection per city type (Hub capital/luxuries choice, Foundry vat pick, 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.
-
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 effects are applied; Propaganda/Fortification military effects are not.
-
Renovation changes a city's type for subsequent turns.
-
Airship launch (7 Steel; adds +3 to the asset Renown total).
-
Resource balances are constrained non-negative every turn, so the plan is always affordable in sequence.
-
Trade Goods exchange: each Turn, Trade Goods may be converted 1-for-1 into any resource in
tradeable_into; the converted resource is available that same Turn (so it can fund Upgrades/Airships). Reported undertrade_conversions. -
Wildcard governor Agents via
Agent.planner()/Agent.baron()(assumes 3 Bastions → +3 Trade Goods on Collect) /Agent.builder()(free type Upgrade), or the genericoverwork/bonus_trade_goods/free_upgradeflags.
Simplifications
- Buying Trade Goods with Electrum (2.5 each) is not modeled.
- Combat, diplomacy, espionage, bidding, and travel are out of scope — this optimizes the Planner's resource/upgrade decisions only.
- Resource amounts are integers; fractional starting Electrum is rounded.
Output
{
"status": "OPTIMAL", // or FEASIBLE / INFEASIBLE
"objective_value": 231.0,
"final_resources": {"capital": 2.0, ...},
"final_renown_total": 35,
"plan": [
{"turn": 0, "city": "Aridias", "action": "collect",
"detail": "hub: +2 luxuries", "governor": "", "overwork": false}
]
}
See example.json for a complete runnable input.