No description
Find a file
2026-08-02 14:53:40 -04:00
.serena initial hopefully correct model 2026-06-16 13:59:07 -04:00
.gitignore Better collect descriptions and capitalist bug fix 2026-06-22 12:49:59 -04:00
.python-version initial hopefully correct model 2026-06-16 13:59:07 -04:00
agents.txt added other agents 2026-06-17 14:35:29 -04:00
docker-compose.yml share links added 2026-06-17 23:33:12 -04:00
Dockerfile docker 2026-06-17 16:30:31 -04:00
example.json initial hopefully correct model 2026-06-16 13:59:07 -04:00
index.html New planner meta incorporated into the model 2026-08-02 14:31:24 -04:00
LICENSE license added 2026-06-25 16:21:28 -04:00
main.py import functionality 2026-06-20 21:38:51 -04:00
pyproject.toml initial hopefully correct model 2026-06-16 13:59:07 -04:00
README.md New planner meta incorporated into the model 2026-08-02 14:31:24 -04:00
solve.py fixed a bug allowing storing of overwork charge 2026-08-02 14:53:40 -04:00
uv.lock initial hopefully correct model 2026-06-16 13:59:07 -04:00

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)

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_pending seeds 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 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

{
  "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.