feat: Resources & Industry — per-region resource profiles + industry LLM lever (v0.36.0, #59)
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:
2026-10-15 18:23:05 +00:00
parent dd9fd6a09d
commit b237fe8dd4
19 changed files with 11990 additions and 46 deletions
+30
View File
@@ -12,11 +12,13 @@ import {
hasCentroids,
hasGeo,
hasRegionData,
hasResources,
loadRegions,
neighboursOf,
referenceForKeys,
regionName,
resetRegions,
resourceOf,
validKeys,
} from './regions';
import type { GameState } from '../shared/types';
@@ -114,6 +116,8 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => {
// (v0.35.0, #58) unused here (allTerritoryKeys is ownership-only).
trades: [],
embargoes: [],
// (v0.36.0, #59) unused here (allTerritoryKeys is ownership-only).
industry: [],
};
// 16 GBR sub-regions + 42 FRA sub-regions, no overlap between the two.
expect(allTerritoryKeys(state).size).toBe(58);
@@ -172,6 +176,16 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => {
expect(geoOf('not-a-real-key-xyz')).toBeUndefined();
});
it('(v0.36 #59) hasResources() is true and resourceOf resolves a real key to a 4-value profile', () => {
expect(hasResources()).toBe(true);
// exact spot-check values pinned from the committed manifest (Ain, FRA)
expect(resourceOf('ain-lqtt')).toEqual({ oil: 0.94, iron: 0.92, food: 0.51, industry: 0.4 });
});
it('(v0.36 #59) resourceOf returns undefined for an unknown key', () => {
expect(resourceOf('not-a-real-key-xyz')).toBeUndefined();
});
describe('graceful degradation with no region data', () => {
beforeEach(() => resetRegions());
@@ -191,6 +205,9 @@ describe('regions (region-key loader, v0.19 #16 P1)', () => {
// and no region-geo: the per-region stats weighting degrades to uniform
expect(hasGeo()).toBe(false);
expect(geoOf('ain-lqtt')).toBeUndefined();
// and no region-resources: industry boosts use the unscaled base value
expect(hasResources()).toBe(false);
expect(resourceOf('ain-lqtt')).toBeUndefined();
});
});
@@ -248,6 +265,19 @@ describe('loadRegions resolves the manifest per environment (dev public/ vs prod
// and no region-geo (not copied into this dist/) -> stats weighting is uniform
expect(hasGeo()).toBe(false);
expect(geoOf('ain-lqtt')).toBeUndefined();
// and no region-resources (not copied into this dist/) -> boosts unscaled
expect(hasResources()).toBe(false);
expect(resourceOf('ain-lqtt')).toBeUndefined();
});
it('(v0.36 #59) a dist/ WITH the region-resources manifest loads it (prod layout)', () => {
copyFileSync(
path.join(realRoot, 'public', 'region-resources.json'),
path.join(prodRoot, 'dist', 'region-resources.json'),
);
expect(loadRegions(prodRoot)).toBe(true);
expect(hasResources()).toBe(true);
expect(resourceOf('ain-lqtt')).toEqual({ oil: 0.94, iron: 0.92, food: 0.51, industry: 0.4 });
});
it('(v0.27 #44) a dist/ WITH the centroids manifest loads them (prod layout)', () => {