History: AI-driven alternate-history simulator
Pax Historia-style sim where the LLM is the world engine.
- React 19 + Vite 6 + Tailwind v4 + shadcn/ui frontend
- Express single-port backend (/api + /sse + built client + SPA fallback)
- SQLite (better-sqlite3) persistence, one row per game
- MapLibre choropleth map (OpenFreeMap dark + ISO_A3 regions)
- Defensive turn loop: strip fences, balanced-{} extract, parse, clamp 0-100,
retry-once nudge, generic 'unclear' fallback; never crashes on bad output
- 5 seed presets (wwii-1939, coldwar-1955, modern-2025, no-wwii-1940, aegis-2400)
- LLM client over native fetch w/ injectable transport (mesh-bot pattern);
LLM_STUB=1 deterministic transport for tests/e2e
- Unit (vitest, 19) + deterministic Playwright e2e
- Multi-stage node:22-slim Docker + Traefik (history.dev.kkhost.pl:4321)
Co-Authored-By: OpenCode <opencode@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// One-shot helper (not a runtime dependency): normalize a Natural Earth admin-0
|
||||
// GeoJSON into public/countries.geo.json with `id` = ISO_A3 and properties.name.
|
||||
// Usage: node scripts/normalize-geojson.mjs <input.geo.json>
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const input = process.argv[2];
|
||||
if (!input) {
|
||||
console.error('usage: node scripts/normalize-geojson.mjs <input.geo.json>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const geo = JSON.parse(readFileSync(input, 'utf8'));
|
||||
|
||||
// ISO_A3 can be missing/empty for a few territories; fall back to a small table
|
||||
// keyed by the Natural Earth `NAME` value, then to the (ISO_A2) feature id.
|
||||
const NAME_TO_A3 = {
|
||||
'South Sudan': 'SSD',
|
||||
'Somaliland': 'SOL',
|
||||
'N. Cyprus': 'CNX',
|
||||
'Antarctica': 'ATA',
|
||||
'Western Sahara': 'ESH',
|
||||
'Falkland Is.': 'FLK',
|
||||
'Falkland Islands': 'FLK',
|
||||
'Heard & Island McDonald Is.': 'HMD',
|
||||
'French Southern & Antarctic Lands': 'ATF',
|
||||
'Saint Helena, Ascension & Tristan da Cunha': 'SHN',
|
||||
'Bouvet Is.': 'BVT',
|
||||
'Pitcairn Is.': 'PCN',
|
||||
'S. Geo. de Las Malvinas (Falkland Is.)': 'FLK',
|
||||
};
|
||||
|
||||
let withA3 = 0;
|
||||
let fallback = 0;
|
||||
const fallbackNames = [];
|
||||
const out = {
|
||||
type: 'FeatureCollection',
|
||||
features: geo.features.map((f) => {
|
||||
const p = f.properties ?? {};
|
||||
let iso3 =
|
||||
(p.ADM0_A3 ?? p.ISO_A3 ?? p.ISO_A3_EH ?? p.SOV_A3 ?? '').toString().trim().toUpperCase();
|
||||
const name = (p.ADMIN ?? p.NAME ?? f.id ?? 'Unknown').toString().trim();
|
||||
if (!iso3) {
|
||||
iso3 = NAME_TO_A3[name] ?? (typeof f.id === 'string' && f.id.length === 2 ? f.id.toUpperCase() : 'ZZZ');
|
||||
fallback++;
|
||||
fallbackNames.push(name);
|
||||
} else {
|
||||
withA3++;
|
||||
}
|
||||
// Trim properties to the two fields the client actually uses (keeps the file small).
|
||||
const feature = {
|
||||
type: 'Feature',
|
||||
id: iso3,
|
||||
properties: { name },
|
||||
geometry: f.geometry,
|
||||
};
|
||||
return feature;
|
||||
}),
|
||||
};
|
||||
|
||||
writeFileSync(new URL('../public/countries.geo.json', import.meta.url), JSON.stringify(out));
|
||||
console.log(`features=${out.features.length} withA3=${withA3} fallback=${fallback}`);
|
||||
if (fallbackNames.length) console.log('fallbacks:', [...new Set(fallbackNames)].join(', '));
|
||||
const size = JSON.stringify(out).length;
|
||||
console.log(`output bytes=${size}`);
|
||||
Reference in New Issue
Block a user