154 lines
5.9 KiB
Markdown
154 lines
5.9 KiB
Markdown
# 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.
|