feat: Resources & Industry — per-region resource profiles + industry LLM lever (v0.36.0, #59)
CI / build + unit + e2e (pull_request) Successful in 2m11s
CI / build + unit + e2e (pull_request) Successful in 2m11s
- scripts/build-region-resources.mjs: deterministic, idempotent builder →
public/region-resources.json (1,800 entries; FNV-1a hash × area factor, 0–1, 2dp)
- server/regions.ts: hasResources()/resourceOf() optional-manifest loader
- shared/types.ts: RegionResources, IndustryType/INDUSTRY_TYPES, IndustryProject,
GameState.industry
- shared/protocol.ts: IndustryOps, TurnResult.industry, deriveNationResources
(area×population-weighted average, 2dp)
- server/engine.ts: isIndustryType, validateIndustryOps, applyIndustryOps
(per-region SET, first-declared-type wins), applyIndustryEffects
(INDUSTRY_ECONOMY_BONUS=1, +1 when region industry≥0.5; sole-owner only;
0/2+ owner regions kept but no boost; re-derive after), system+user prompt
contract lines, runTurn resourceOf opt, nationResources in user payload
- server/db.ts: legacy-row industry default migration
- server/stub.ts: industry:{projects:[]} in stub result; resources/industry
in StubPayload (never read)
- server/index.ts: resourceOf wired to jump route
- All test fixtures updated; new describe blocks in engine.test.ts,
protocol.test.ts, regions.test.ts, db.test.ts
- CHANGELOG.md v0.36.0, AGENTS.md docs, package.json 0.36.0
Fixes #59
This commit is contained in:
@@ -26,6 +26,23 @@ export interface RegionStats {
|
||||
population: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* (v0.36, #59) A region's synthesized four-value resource profile — each value
|
||||
* a finite fraction in [0, 1]. Built once per region key by
|
||||
* `scripts/build-region-resources.mjs` into the committed
|
||||
* `public/region-resources.json` (the engine reads it via `regions.resourceOf`).
|
||||
* The engine scales a region's per-turn industry boost by its `industry` value,
|
||||
* and a nation's aggregate profile (the area×population-weighted mean of its
|
||||
* owned regions' profiles, see `protocol.deriveNationResources`) is what the
|
||||
* LLM prompt carries so the model can reason about where to build.
|
||||
*/
|
||||
export interface RegionResources {
|
||||
oil: number;
|
||||
iron: number;
|
||||
food: number;
|
||||
industry: number;
|
||||
}
|
||||
|
||||
export const STAT_KEYS = ['military', 'economy', 'stability', 'population'] as const;
|
||||
export type StatKey = (typeof STAT_KEYS)[number];
|
||||
|
||||
@@ -174,6 +191,36 @@ export interface Embargo {
|
||||
target: string;
|
||||
}
|
||||
|
||||
// --- Resources & industry (v0.36, #59) ---------------------------------------
|
||||
//
|
||||
// The LLM can build per-region INDUSTRY PROJECTS (an oil facility, an iron
|
||||
// mine, a food operation, or a factory) on regions it controls; each active
|
||||
// project gives that region's sole owner a small, deterministic economy boost
|
||||
// every committed turn, scaled by the region's `industry` resource value from
|
||||
// the committed resource manifest. The active projects live in
|
||||
// `GameState.industry` — the LLM's "industry" lever.
|
||||
|
||||
/** The four industry project types a region can host (v0.36, #59). */
|
||||
export type IndustryType = 'oil' | 'iron' | 'food' | 'industry';
|
||||
|
||||
/** All industry project types, in canonical order (the LLM contract + validator). */
|
||||
export const INDUSTRY_TYPES: IndustryType[] = ['oil', 'iron', 'food', 'industry'];
|
||||
|
||||
/**
|
||||
* (v0.36, #59) One active industry project: a building of a given
|
||||
* {@link IndustryType} in a specific region. The engine keys the per-turn
|
||||
* economy boost by the region's SOLE owner (a region with no owner or 2+
|
||||
* owners yields no boost) and scales it by the region's `industry` resource
|
||||
* value. A region hosts at most ONE project — re-declaring on a region that
|
||||
* already has one is a no-op (the first declared type wins).
|
||||
*/
|
||||
export interface IndustryProject {
|
||||
/** The region key the project sits in (see `public/region-keys.json`). */
|
||||
regionKey: string;
|
||||
/** The kind of project built there (see {@link IndustryType}). */
|
||||
type: IndustryType;
|
||||
}
|
||||
|
||||
/**
|
||||
* (v0.33.0, #56) A first-class **war** — a named, ongoing armed conflict
|
||||
* between two or more nations. Unlike the boolean `war` relation (which a
|
||||
@@ -356,6 +403,18 @@ export interface GameState {
|
||||
* no data wipe.
|
||||
*/
|
||||
embargoes: Embargo[];
|
||||
/**
|
||||
* (v0.36, #59) Active industry projects (see {@link IndustryProject}): built
|
||||
* by a turn's `industry.projects` ops (the LLM names a region key + a
|
||||
* project type). Each region hosts at most one project — re-declaring on a
|
||||
* region that already has one is a no-op. Every committed turn the engine
|
||||
* gives each project's region's SOLE owner a small, deterministic economy
|
||||
* boost scaled by that region's `industry` resource value. A project on a
|
||||
* region that ends a turn with no owner (or 2+ owners) yields no boost but
|
||||
* is NOT removed. Always `[]` on a fresh game; rows saved before v0.36 load
|
||||
* with this defaulted to `[]` (see server/db.ts) — additive, no data wipe.
|
||||
*/
|
||||
industry: IndustryProject[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user