| .serena | ||
| .gitignore | ||
| .python-version | ||
| agents.txt | ||
| docker-compose.yml | ||
| Dockerfile | ||
| example.json | ||
| index.html | ||
| LICENSE | ||
| main.py | ||
| pyproject.toml | ||
| README.md | ||
| solve.py | ||
| uv.lock | ||
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
"overwork_pending": false, // starts holding an Overwork charge
"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): 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_pendingseeds 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 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,
"overwork_armed": false} // armed = the Planner charged this city here
]
}
See example.json for a complete runnable input.