Files
History/scripts/build-region-resources.test.ts
T
kamilkrzywda b237fe8dd4
CI / build + unit + e2e (pull_request) Successful in 2m11s
feat: Resources & Industry — per-region resource profiles + industry LLM lever (v0.36.0, #59)
- 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
2026-10-15 18:23:05 +00:00

107 lines
4.6 KiB
TypeScript
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.
/**
* scripts/build-region-resources.test.ts — validation for the committed
* `public/region-resources.json` (built by `scripts/build-region-resources.mjs`).
*
* The manifest maps each region key to a synthesized four-value resource
* profile `{ oil, iron, food, industry }` — each a finite number in [0, 1]
* (2 decimal places). It is the v0.36 (#59) "Resources & Industry" data
* source: the engine scales each region's per-turn industry boost by its
* `industry` value, and the LLM prompt carries each nation's aggregate
* profile (the area×population-weighted mean of the owned regions' profiles,
* `deriveNationResources` in shared/protocol.ts).
*
* Like the region-geo manifest, this is a load-bearing data artifact — the
* test pins its contract. It is dependency-free: it reads the JSON with plain
* `fs`/`JSON.parse` (no `@turf`/geojson types, which would pull `@types/geojson`
* into the tsc program and break `WorldMap.tsx` — see the v0.17 note). The
* determinism test re-runs the (turf-free) `.mjs` builder and compares bytes,
* matching that the builder is idempotent.
*/
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const here = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(here, '..');
function readJson(rel: string): Record<string, unknown> {
return JSON.parse(readFileSync(path.join(root, rel), 'utf8'));
}
interface ResourceEntry {
oil: number;
iron: number;
food: number;
industry: number;
}
const RESOURCES = ['oil', 'iron', 'food', 'industry'] as const;
// feature id -> region key
const manifest = readJson('public/region-keys.json') as Record<string, string>;
const resources = readJson('public/region-resources.json') as Record<string, ResourceEntry>;
const keys = Object.values(manifest);
describe('region-resources.json', () => {
it('has exactly one entry per region key (same key set as region-keys.json)', () => {
expect(new Set(Object.keys(resources))).toEqual(new Set(keys));
expect(Object.keys(resources)).toHaveLength(1800);
});
it('is keyed in sorted (deterministic) order', () => {
const ks = Object.keys(resources);
expect(ks).toEqual([...ks].sort());
});
it('every entry is a finite { oil, iron, food, industry } in [0,1] with ≤ 2 dp', () => {
for (const [k, v] of Object.entries(resources)) {
for (const r of RESOURCES) {
const val = v[r];
expect(typeof val, `non-number ${r} for ${k}`).toBe('number');
expect(Number.isFinite(val), `non-finite ${r} for ${k}`).toBe(true);
expect(val >= 0, `negative ${r} for ${k}: ${val}`).toBe(true);
expect(val <= 1, `${r} above 1 for ${k}: ${val}`).toBe(true);
expect(
Math.round(val * 100) / 100 === val,
`${r} over 2dp for ${k}: ${val}`,
).toBe(true);
}
}
});
it('spot-checks well-known regions for exact profiles', () => {
const is = (k: string, oil: number, iron: number, food: number, industry: number) => {
const v = resources[k];
expect(v, `missing resources for ${k}`).toBeDefined();
expect(v!.oil, `oil for ${k}`).toBe(oil);
expect(v!.iron, `iron for ${k}`).toBe(iron);
expect(v!.food, `food for ${k}`).toBe(food);
expect(v!.industry, `industry for ${k}`).toBe(industry);
};
is('california-kikh', 0.31, 0.1, 0.82, 0.97); // US-CA
is('arkhangel-sk-lhbk', 0.49, 0.52, 0.98, 0.82); // RU-ARK mainland core
is('pechora-qwhs', 0.11, 0.16, 1, 0.04); // RU-PEO (Arctic-coast band, #41)
is('yamal-gosw', 0.64, 1, 1, 0.49); // RU-YAM (Arctic-coast band, #41)
is('new-earth-eikt', 0.6, 0.56, 0.91, 0.66); // RU-ARK-NE (island group, #31)
is('severnaya-zemlya-awqp', 0.1, 0.48, 1, 0.3); // RU-ARK-SZ (island group, #31)
is('ain-lqtt', 0.94, 0.92, 0.51, 0.4); // FR-01 (Ain)
});
it('is deterministic: re-running the builder rewrites a byte-identical file', () => {
const file = path.join(root, 'public', 'region-resources.json');
const before = readFileSync(file, 'utf8');
// The builder is cwd-independent (it resolves the repo root from its own
// location) and idempotent: same input files (region-keys.json + the
// committed region-geo.json when present) + pure string hashing + fixed
// 2-dp rounding + stable (sorted) key order → identical bytes.
execFileSync(process.execPath, [path.join(root, 'scripts', 'build-region-resources.mjs')], {
cwd: root,
stdio: 'pipe',
});
const after = readFileSync(file, 'utf8');
expect(after).toBe(before);
});
});