dws-city-res-solve/README.md
2026-06-16 13:59:07 -04:00

154 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.