# 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]` where `log_mapping` is 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) ```bash 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 ```python 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 ```jsonc { "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 under `trade_conversions`. - **Wildcard governor Agents** via `Agent.planner()` / `Agent.baron()` (assumes 3 Bastions → +3 Trade Goods on Collect) / `Agent.builder()` (free type Upgrade), or the generic `overwork` / `bonus_trade_goods` / `free_upgrade` flags. ## 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 ```jsonc { "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.