1
0
Fork 0
polaris-task-force/docs/economy/supply-demand-design.md
Z8MB1E 447df220f5 feat(economy): add economy tick with demand simulation and price modifiers
Add the src/lib/economy service (GM baselines, deterministic per-period demand drift, price modifier computation, event emission) and the economy-tick bin. Gated by GameRules economy.enabled; period-idempotent with a cold-start observation gate.

Ultraworked with [Sisyphus](https://github.com/code-yeongju/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-06 17:22:30 -04:00

98 lines
7 KiB
Markdown
Raw 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.

# Supply & Demand Economy — Phase 1 Design
Status: **Approved direction** (decisions confirmed with product owner, 2026-09-06). Not yet implemented.
## Confirmed decisions
| Decision | Choice |
|---|---|
| Supply model | **Abstract per-item ledger** (`market-state` collection), GM baseline + transaction-driven adjustments. Never derived by scanning physical stockpiles. |
| Item scope | **Resources + tradeable assets only.** Vehicles deferred to a later phase. |
| Pricing reach | **New NPC listings only** + displayed reference price. Existing listings, player listings, and open negotiations are never repriced. |
| Simulation cadence | **New `economy-tick` bin** (external cron, idempotent, SSE notify). `market-tick` consumes the resulting state. |
| GM surface | **Minimal per-item** (baseline supply, baseline demand, price band, enable toggle) on Resources/Assets; global knobs on GameRules. |
## Current-state facts this builds on
- Price anchors: `Assets.marketTrading.perceivedValue.baseBuyPrice/baseSellPrice` (`src/lib/market/index.ts` — `getBaseBuyPrice`/`getBaseSellPrice`), `Resources.baseValue`, `Structures.sellPrice`.
- NPC listing generation: `src/scripts/marketTick.ts` — `autoPrice` (±15%) × `vendor.priceModifier`, `minPrice = price × 0.6`.
- Real-demand signal sources that already exist: `bank-transactions` (type `payment`, memo carries asset + qty), `market-listings` (sold/buyer/soldAt), `market:sale` event logs with structured data.
- Unwired production engine: `Structures.autoGeneratedResources[]` (min/max amount, frequency, produce chance) — schema exists, no tick consumes it.
- Tick bin pattern: `bin[]` registration in `src/payload.config.ts`, `src/scripts/<key>.ts` exporting `script`, `createBinLogger` (`src/scripts/lib/binFileLogger.ts`), SSE notify via `/api/game-tick/notify` with `x-game-tick-secret`.
- Negotiation system invariants to protect: NPC stance anchored to asking price, `minPrice` floor, patience meter, strictly-increasing buyer offers (`src/lib/market/negotiations.ts`).
## Architecture
### 1. `market-state` collection (new) — the supply/demand ledger
One doc per item (`itemType: "resource" | "asset"`, `itemId`). Fields:
- `gmBaselineSupply`, `gmBaselineDemand` — numbers, GM-set (copied from per-item fields on first creation).
- `currentSupply` — adjusted by: unit purchases from NPC listings (decrement), unit sales to NPC buyers (increment, later phase), production events (increment).
- `artificialDemand` — slow-drifting NPC pressure, updated per tick (random walk bounded around `gmBaselineDemand`).
- `realDemand` — decaying rolling aggregate of completed unit purchases (quantity-weighted), decayed each tick by a GameRules decay factor.
- `priceModifier` — computed, bounded: derived from demand-vs-supply ratio, clamped to the item's band (`±maxModifier`).
- `lastComputedAt`.
### 2. Per-item GM fields (Resources + Assets)
- `economy.enabled` (default true for tradeable items)
- `economy.baselineSupply`, `economy.baselineDemand` (numbers; defaults derived from rarity)
- `economy.maxModifier` (price band, e.g. 0.5 → price can move ±50% of base)
### 3. GameRules global knobs (new group `economy`)
- `economyTickIntervalHint` (docs for cron), `realDemandWindowTicks`, `realDemandDecay`, `artificialDemandDrift`, `coldStartBehavior` (use baseline until N observations), global price band default, `economyEnabled` master gate (pattern: `staffHiringEnabled`).
### 4. `economy-tick` bin (new)
`src/scripts/economyTick.ts` + logic in `src/lib/economy/`:
1. For each enabled market-state doc (creating missing ones from GM baselines): decay `realDemand`, drift `artificialDemand`, recompute `priceModifier` from `demand / supply` ratio, clamp to band.
2. Cold start: items with no purchase observations keep `priceModifier = 1` (neutral).
3. Emits a batch summary `economy:tick` event + per-item `economy:price-change` events only when the modifier crosses a visible threshold (avoid event spam).
4. SSE notify (same path as other bins). Idempotent: a re-run recomputes the same values from stored state; drift steps are seeded deterministically per period.
### 5. Consumption by market-tick
When generating a new NPC auto listing, price becomes:
```
price = round(baseBuyPrice × priceModifier × vendor.priceModifier)
```
`autoPrice` ±15% randomness is dropped (the modifier replaces it) — `minPrice = price × 0.6`, `desiredPrice = price` unchanged, so negotiation invariants are untouched. A "reference price" (modifier-applied base) is displayed on item views for player pricing guidance.
Shortage behavior (phase 1): modifier raises prices and NPC restock quantity scales down when `supply < demand` (bounded, never zero without a GM flag).
### 6. Real-demand ingestion
Hook points (no new collection needed in phase 1 — read from existing records):
- `buyListing` (`src/lib/market/index.ts`) — on successful purchase of an NPC/vendor listing, increment `realDemand` on that item's market-state (quantity-weighted).
- Player-to-player sales do **not** move prices in phase 1 (no double counting).
## Out of scope (explicit deferrals)
NPC faction economies, autonomous NPC buyers with wallets, production/crafting chains, vehicle market, physical-stock reconciliation, multi-currency, repricing existing listings/negotiations, real-time price streaming (refresh-on-tick only — SSE bus is single-instance).
## Migration & rollout
- New collection + fields = **named migration** (`bun run payload migrate:create add_market_economy_state`), registered in `migrations/index.ts`; dev applies via `bun run payload migrate` (`push: false`). Never `db push`.
- Existing listings stay at their prices; modifier applies only to newly generated listings. `economyEnabled` master flag allows instant disable.
- Regenerate `src/payload-types.ts` after schema changes.
## Top risks (from design review)
1. **Non-atomic mutations** — `buyListing` already credits locker before clearing payment; new flows must not compound this. Define failure/retry behavior before adding ledger writes.
2. **Negotiation breakage** — never reprice under open negotiations (phase 1 avoids entirely by only touching new listings).
3. **Ledger/physical divergence** — abstract supply will not track destroyed/admin-edited stock; GM can reset per-item state from the admin panel (rebuild from baselines).
4. **Tick duplication** — external cron may retry; idempotency + deterministic period boundaries required.
5. **Scope explosion** — every extension (factions, vehicles, production) is a separate approved phase.
## Implementation phases
1. **P1a — schema**: `market-state` collection, per-item economy fields, GameRules knobs, named migration, generated types.
2. **P1b — engine**: `src/lib/economy/` (modifier math, decay, drift, cold start) + unit-testable pure functions.
3. **P1c — tick**: `economy-tick` bin + event types + SSE wiring.
4. **P1d — consumption**: market-tick uses modifier for new NPC listings; restock scaling; reference price display; `buyListing` real-demand ingestion; GM reset action.