June 22, 2026 — admin

Controllers

High-level design for the *controller layer*: the "brains" that decide which tasks get posted, how they coordinate, and how a dynasty's many decision-makers fit into one machine.

Design — Controllers (the dynasty’s decision-makers)

High-level design for the controller layer: the “brains” that decide which tasks get posted, how they coordinate, and how a dynasty’s many decision-makers fit into one machine. This is the deep model behind ../GAME_DESIGN.md §2 (the control model — the architectural spine) and §7 (controllers — the brains). It builds on dynasty.md (offices, vessels, overload, succession) and feeds economy.md (a shop manager’s objective is to read the market and stay profitable). Design doc — the model and its rules, not an implementation plan.

Production is the first tenant of this layer, so it is the worked example throughout. The same machinery is meant to carry the Marshal (force), the dynasty-advancement controller (council/guild ambitions), and rival AI — those are later passes. The concrete production data (recipes, prices) lives in ../../design/production_notes.md.

This is the answer to the question that started the whole top-down exercise: “a workshop can’t work in a vacuum — what is a controller, and how do the dynasty’s decision-makers fit together?” The short version: a controller is a dynasty position that owns one domain; positions coordinate by passing declinable orders and asking each other for judgments; an unfilled position switches off a part of the dynasty’s brain.

This doc covers the intent brains — the position controllers that decide what should happen and post it. The other brain, the personal controller seated in every living character (which decides what that body actually does, and claims the posted work), is its counterpart in agency.md. Together they are the two halves of the control model; the noticeboard is the seam between them.


1. What a controller is

A controller is a dynasty position bound to a domain. The position is the agent — not the character sitting in it.

A dynasty is one machine assembled from positions, each responsible for a slice of its affairs. A position can do exactly three things:

  1. Post tasks inside its own domain — never reach into another’s.
  2. Query other positions (ask them for a judgment).
  3. Be queried by other positions.

Cross-domain work happens by one position asking another — the Marshal does not craft swords, he orders them from a shop that does. This keeps every controller’s scope small and is what stops the design collapsing into spaghetti.

Positions come at two scales:

  • The fixed cabinet — a slate of dynasty offices, each owning a domain (production, money, force, family…). Illustrative names used in this doc: Trademaster (production/trade), Treasurer (money), Marshal (force). The authoritative roster is a deferred second-pass decision (§9) — reconciling it with dynasty.md’s office slate is its own task. Treat the names here as examples of the shape, not a finalised list.

  • Building managers — a dynamic set, one position per workshop or extraction site. They are special only in that their count grows and shrinks with the buildings you own.

The position is the agent, not the character

The character in a seat is the vessel; the position is the agent. This is the keystone:

  • A character may hold many seats at once (a manager of three shops and the Treasurer). The overload rule (dynasty.md §4) erodes their effectiveness per seat as load grows, and tips it negative past their limit.

  • A position’s decisions are made as the office, tinted by the occupant’s modifiers — hat to hat. When one person holds two seats, the Treasurer-seat can still say no to the Elder-seat’s ambition. People wear multiple hats; the hats disagree.

So who you seat reshapes the whole house: a cautious daughter in the Treasurer seat makes the entire dynasty fiscally conservative; an overloaded patriarch genuinely stalls his own plans. Character → dynasty behaviour is a concrete lever, not flavour.

The machine principle — an empty seat is a disabled subsystem

This is the most important property of the whole layer:

Each position grants a capability to the others. Leaving a seat empty doesn’t just remove one role — it switches off that part of the dynasty’s brain.

  • No Trademaster → nobody can discover where resources are; shops fall back to running autonomously (gather / market) or stall if locked.

  • No Treasurer → no spending budget → no shop can buy anything.

  • Some seats are so central their absence cripples the whole dynasty.

The flip side of the overload rule, then, is a real ongoing choice: leave a seat empty (a capability switched off) versus cram it onto an already-overloaded relative (degraded output). Filling every position — even doubling a single character across several — is how a dynasty keeps its full mind online. This is the same scarce-family pressure that drives the marriage/heir loop (../GAME_DESIGN.md §4).


2. The building-level controller — a mandatory manager + an Auto switch

The bottom of the machine is a single shop’s controller. Today this exists only as the hardwired ladder inside WorkshopSystem (§8); the model that replaces it:

Every operating shop needs a seated manager (a family member). No manager → the shop is inert and posts nothing. True for player and AI alike.

The manager is the brain. On top of it sits one switch, and it is the only real fork in building-level control:

Mode Who decides Notes
Auto on the manager decides and posts tasks the only mode the AI ever uses
Auto off (owner only) the Player drives, by hand the manager is benched from deciding

Two consequences:

  • The manager never leaves the seat. Toggling Auto off only cuts the manager’s ability to post tasks — it still costs the family member, and its modifiers still apply to execution (a master smith’s shop crafts faster whether or not the player is steering). The Player overrides decisions, not presence.

  • Player and AI are the same machine. Both are a family member in a seat, differing only in whether a person or a rule sets intent — exactly the symmetry ../GAME_DESIGN.md §2 demands.

When the Player has Auto off and an order arrives from another controller (§3), the Player fields the accept/decline (via a notification, later a proper order-builder UI). If the Player ignores it, the order expires (§4) and the AI copes without it. Per-shop automation levels (partial delegation) are a later refinement (§9).


3. Two tiers of task — orders and labour

Reconciliation (see agency.md §2). “Two tiers” is a conceptual framing, not two systems. There is one substrate — the noticeboard — carrying one kind of thing: tasks. An order is simply a task that links to and monitors another task; the lifecycle below lives as states and parent→child links on that supervisory task, riding the postedBy/postedById pattern. So read “tier” as role a task plays (supervise vs. execute), not a separate mechanism.

The existing task system is a unit of labour: a worker claims “go craft”, “go deliver”, and executes it physically. Coordinating controllers needs a second tier above that:

  • Order (controller → controller) — a request addressed to a specific position: “make 2 swords, deliver to the requesting building.” It is encapsulated and declinable.

  • Labour (worker execution) — the current task system, unchanged.

The two meet by encapsulation, not by reaching across domains:

The Marshal posts an order addressed to the shop-manager seat. The manager applies its judgment gateaccept or decline — and if it accepts, it spawns its own craft + deliver labour tasks to fulfil it. The Marshal only ever sees the order’s outcome; the shop never learns why the swords are wanted.

That decoupling — “the workshop doesn’t care what the swords are for” — is what keeps each controller’s scope small. And the ability to decline is what gives fallbacks their meaning: a slammed or unprofitable shop says no, and the requester goes elsewhere (buys from the market, asks another shop, does without). Under a flat “post the labour directly” model there would be no one to say no.

Orders are delivered to the requesting building — a shop’s order lands at the shop. (Non-shop positions such as the Marshal will each operate from some building of their own — an HQ / house equivalent; that detail is parked, §9.)


4. The order lifecycle

An order mirrors the worker-task lifecycle (Posted → Claimed → InProgress → Done/Failed) one tier up:

  Requested ──► Accepted ──► InProgress ──► Fulfilled
        │           │                    ├─► Failed
        └─► Declined ┘                    ├─► Cancelled
                                          └─► Expired
  • Requested — the requester posts the order, addressed to a target position.
  • Accepted / Declined — the target’s judgment gate (§5) fires. Declined bounces back immediately → the requester picks a fallback.

  • InProgress — the target has spawned its own labour tasks.

  • Fulfilled (goods delivered) / Failed (couldn’t source / execute) / Cancelled (requester withdrew) / Expired (deadline hit).

Cancelled ≠ Failed, and the distinction matters: Failed may trigger a market fallback; Cancelled needs no fallback at all — it is the requester’s own choice.

Hooks & expiry — the requester watches and owns the deadline

  • Hook — the requester watches the orders it posted by scanning for its own posted tasks and reading their terminal state. This already half-exists: CountLabour (../../source/Esc/Systems/ProductionUtils.h) already does “scan every task where postedBy == me and check its state.” That scan is the hook; orders add an expiry field and a parent back-reference.

  • Expiry is requester-owned and does two jobs:

    1. A safety net — a soft deadline so a stuck order can’t wedge the game.
    2. A validity check — the requester re-tests “do I still want this?” on its update. The Marshal’s guards both died → the order is pointless → he kills it himself, long before any deadline.

Cascading cancellation

An order owns the labour tasks spawned to fulfil it (each child carries a back-reference to its parent — the postedBy/postedById pattern already models this). Kill the parent and the children go with it: when the requester drops an order, the accepting controller sees it is dead and tears down the labour it spawned.

Fallback is a per-task-type slot

There is no global cancellation rule. Every order/task type ships its own “what do I do when my parent goes invalid?” handler — a first-class slot the architecture guarantees, much like today’s TaskFailureRecoverySystem. The specifics are balancing, not architecture. Production’s, for example, can be a progress threshold: past ~50% complete, finish the work into shop stock; below it, hard-cancel. Goods already in a worker’s hands simply complete into normal inventory — you don’t un-forge a sword because the order lapsed.


5. Queries — judgments, not raw numbers

Discovery and approval run through queries: a position asks another for a verdict within its domain. Crucially, a query returns a judgment, not a fact:

  • “Are we flush?” asked of the Treasurer is not the raw balance — it is the Treasurer’s opinion, factoring reserves and planned spending.

  • “Is now a good time?” asked of a senior office weighs more than one number.

The verdict is tinted by the occupant’s modifiers, and it is hat to hat even when one character holds both seats (§1). This is what makes a position mean something: the Treasurer isn’t a variable, it is the family’s financial attitude.

The dummy implementations are trivial and the ceiling is high — the same property that makes rival AI cheap. A dummy Treasurer answers “flush?” with *”yes if we hold

5000 gold”*; a later one weighs commitments, risk, and temperament. Same socket, swappable brain (§7).


6. Coordination — discovery, the machine, and the spending budget

Putting §1–§5 together, here is how a shop gets what it needs:

  1. Discovery runs through positions. The shop asks the Trademaster “where’s the iron?” (routing) and the Treasurer “can we afford it?” (a spend judgment) — two positions, two queries.

  2. Graceful degradation when a position is absent (§1). No Trademaster → the shop can’t discover sources, so it runs autonomously (gather / market-buy) or stalls if locked. No Treasurer → no budget → it can’t buy at all. A missing position is a switched-off capability, not a crash.

  3. Spend approval is a standing budget, not per-transaction. The Treasurer does not vet every nail. It sets a split — e.g. “60% of current cash is the buying budget” — and shops spend freely within it. Only big one-offs (a new building, a guild licence) need explicit sign-off. This keeps the Treasurer meaningful without making it a bottleneck on every input purchase, and “set the household budget” is a clean, dummy-friendly dial.

A nice property falls out: the accept/decline gate (§3) absorbs most resource contention. When two of your shops both want the last of the iron, the source simply declines the second order → that shop falls back, before anyone walks anywhere. No race, no wasted pickup trip. Heavy reservation bookkeeping may not be needed at all — the order protocol already resolves it.


7. The brains themselves

Every position is a self-contained best-effort optimiser behind a uniform interface: it pursues its own narrow objective, asks others for what it needs, takes no gracefully, and does the best it can with what it is given — including nothing (“I can’t do anything with the resources I have” is a valid outcome).

The shop manager is a strategist with one objective: stay profitable

A manager is not a mere dispatcher keeping workers busy on a fixed routine. It is a strategist whose domain objective is keep this shop profitable: choose what to produce, stock up on cheap inputs, time sales into scarcity. Its decision inputs are the economy’s price signals — the floor × scarcity model of economy.md §2. The two designs interlock: the market produces the signals; the manager reads them.

When it accepts a non-profit order (the Marshal’s swords), the profit objective expresses itself at the judgment gate (§3) — a cold manager declines a break-even order; a loyal one accepts; the occupant’s modifiers tint the call.

Today’s hardwired ladder is the dummy manager

The current WorkshopSystem loop (craft → sell-when-stacked → gather-at-random) is exactly a strategist with no strategy. We don’t throw it out — it becomes the floor of the spectrum, the brain a shop runs when nothing smarter is seated.

The Trademaster is the optional layer above

Each shop optimising its own profit is not automatically good for the dynasty — three shops independently chasing the same hot good will trample each other and fight over the same cheap iron. The Trademaster is the optional dynasty-wide planner that sits above the managers and overrides local profit-seeking for whole-chain coordination (rationing raw supply across your shops, routing mine → smelter → smith). It is dynasty-scoped, never town-wide — a person you choose to invest a relative in, not a god-system. It is the GDD’s long-deferred “dynasty production planner (Option 3)” — just the fanciest controller, not a foundation everything waits on.

Uniform sockets ⇒ difficulty tiers and rival AI almost for free

Because every position is a swappable brain behind the same query/order interface, rival-dynasty AI and difficulty tiers are just smarter position logic plugged into the same sockets. There is no separate “AI code path” — symmetry (../GAME_DESIGN.md §2) is preserved by construction. Now designed: rival-ai.md makes this concrete — rival AI is the Elder controller (the topmost socket), player vs. rival is one Auto toggle on it, and it reacts to the world through the memory.md ledger. Difficulty is deliberately not a mode — friction, if ever, is modifiers, never a cheating brain.


8. ⚠ Tensions with current code

  • There is exactly one brain, and it is welded on. WorkshopSystem (../../source/Esc/Systems/WorkshopSystems.cpp) is a controller — the auto-policy one — but it is hardwired and unswappable. It walks every owned workshop and greedily posts Hire/Craft/Sell/Gather, with no notion of a manager, a mode, an order, or a query. The whole §1–§7 model is greenfield on top of it.

  • “Mandatory manager” inverts today’s rule. Right now a shop produces if it is owned (PostWorkshopTasks gates on BuildingOwnerComponent, not on a seated person). Under §2 a shop with no seated manager produces nothing — a real behavioural change to the posting gate, and it depends on the position layer (manager seats) that does not exist yet (../GAME_DESIGN.md §2 tension).

  • Only the labour tier exists. TaskComponent models worker labour (Posted → Claimed → InProgress → Done/Failed). The order tier (§3–§4) — addressed, declinable orders with accept/decline, a parent→child link, cascading cancel, a distinct Cancelled/Expired outcome, and an expiry field — is new. The postedBy/postedById fields and the CountLabour scan are the live hooks to build it from, but the order/labour decomposition is unbuilt.

  • No positions, no queries, no judgments. The cabinet offices, per-building manager seats, the overload curve, and the whole query-returns-a-judgment mechanism (§5) do not exist. DynastyFamilySystem tracks members; seats and trait-tinted decisions are greenfield.

  • No Trademaster / planner, and extraction is still inert. Extraction buildings have output data (ExtractionProductionComponent) but no controller posts their tasks (CHALLENGES: “Extraction Buildings Have No Work System”). Per ../GAME_DESIGN.md §7, extraction is not missing a system — it is missing a controller assignment, which this layer provides.

  • The shop manager’s profit objective needs the dynamic market. §7’s “stay profitable” brain reads price signals that don’t move yet — it depends on the economy.md market being built (and on the BuyStep for input-buying). The dummy ladder works without it; the strategist does not.


9. Open questions & held notes (next pass)

  • The authoritative position roster (§1).Resolved — now in positions.md: the unified position definition, the three classes (cabinet · building managers · council seats), and the finalised cabinet slate (Elder/Treasurer/Trademaster/Marshal, Matron parked). The illustrative names used in this doc are confirmed by it; treat positions.md as the truth for the slate.

  • Non-shop positions’ buildings (§3). The Marshal (and other non-production positions) presumably each operate from a building — an HQ / house equivalent. Loose future detail, not designed.

  • Per-shop automation levels (§2). Beyond the binary Auto on/off, partial delegation (the player approves some order types, auto-runs the rest). Later refinement.

  • Per-task-type fallback logic (§4). Each order/task type needs its own cancellation/recovery handler; the production progress-threshold is one example. Specifics are balancing.

  • Reservation vs accept/decline (§6). The accept/decline gate looks sufficient to resolve resource contention; whether any explicit reservation bookkeeping is still wanted is left to the build.

  • Query cost / cadence (§5). Whether judgments are synchronous reads each interval or something cached — an implementation concern for when controllers run.

  • The dynasty-advancement controller.Placed — it is the Elder’s execution arm (positions.md §3.1), not its own office: advancing the family in the town (getting members onto the City council, buying guild licences, promoting relatives). The Elder→advancement seam is deliberately movable — it can be split into a dedicated seat later as a code-move. (The town council itself is a separate arena — politics.md — not part of this layer.)