Add vision-LLM import pipeline for hex terrain classification
Adds a 3-phase pipeline to populate a HexMap from a source image using Claude's vision capabilities instead of naive pixel-color matching: Phase 1 (extract-submaps): crops annotated submap PNGs per hex, including center + 6 neighbours with SVG overlay. Phase 2: Claude classifies submaps in a Code session, writing classifications.json — resumable across sessions. Phase 3 (import-from-json): reads classifications.json into the DB. Also adds assemble-map.ts to reconstruct a full image from a Leaflet tile pyramid (used to recover the Aventurien source map from kiepenkerl). Adds CLAUDE.md documenting the approach, scale constants (PIXELS_PER_MEILE=8, hexSize=40 = 10 Meilen/Hex), and the Nord/Mitte/Süd region split for the Aventurien map. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
94
pipeline/import-from-json.ts
Normal file
94
pipeline/import-from-json.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Phase 3: Import hex classifications from JSON into the DB.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx pipeline/import-from-json.ts <classifications.json> <map-id>
|
||||
*
|
||||
* Input JSON format (array):
|
||||
* [
|
||||
* { "q": 0, "r": 0, "base": "forest", "features": [] },
|
||||
* { "q": 1, "r": 0, "base": "plains", "features": [{ "terrainId": "river", "edgeMask": 18 }] },
|
||||
* ...
|
||||
* ]
|
||||
*
|
||||
* Edge mask bits: NE=1, E=2, SE=4, SW=8, W=16, NW=32
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { initDb, getDb, saveDb } from '../server/db.js';
|
||||
|
||||
interface HexClassification {
|
||||
q: number;
|
||||
r: number;
|
||||
base: string;
|
||||
features: Array<{ terrainId: string; edgeMask: number }>;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [jsonPath, mapIdStr] = process.argv.slice(2);
|
||||
|
||||
if (!jsonPath || !mapIdStr) {
|
||||
console.error('Usage: npx tsx pipeline/import-from-json.ts <classifications.json> <map-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mapId = parseInt(mapIdStr, 10);
|
||||
const hexes: HexClassification[] = JSON.parse(readFileSync(resolve(jsonPath), 'utf-8'));
|
||||
console.log(`Importing ${hexes.length} hexes into map ${mapId}...`);
|
||||
|
||||
await initDb();
|
||||
const db = getDb();
|
||||
|
||||
// Verify map exists
|
||||
const mapRows = db.exec('SELECT id FROM hex_maps WHERE id = ?', [mapId]);
|
||||
if (mapRows.length === 0 || mapRows[0].values.length === 0) {
|
||||
console.error(`Map ${mapId} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
db.run('BEGIN TRANSACTION');
|
||||
let written = 0;
|
||||
let errors = 0;
|
||||
|
||||
try {
|
||||
for (const hex of hexes) {
|
||||
// Upsert hex
|
||||
db.run(
|
||||
`INSERT INTO hexes (map_id, q, r, base_terrain, updated_at)
|
||||
VALUES (?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(map_id, q, r)
|
||||
DO UPDATE SET base_terrain = excluded.base_terrain, updated_at = datetime('now')`,
|
||||
[mapId, hex.q, hex.r, hex.base],
|
||||
);
|
||||
|
||||
const idRows = db.exec(
|
||||
'SELECT id FROM hexes WHERE map_id = ? AND q = ? AND r = ?',
|
||||
[mapId, hex.q, hex.r],
|
||||
);
|
||||
const hexId = idRows[0].values[0][0] as number;
|
||||
|
||||
db.run('DELETE FROM hex_features WHERE hex_id = ?', [hexId]);
|
||||
for (const f of (hex.features ?? [])) {
|
||||
if (!f.edgeMask) continue;
|
||||
db.run(
|
||||
'INSERT INTO hex_features (hex_id, terrain_id, edge_mask) VALUES (?, ?, ?)',
|
||||
[hexId, f.terrainId, f.edgeMask],
|
||||
);
|
||||
}
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
db.run("UPDATE hex_maps SET updated_at = datetime('now') WHERE id = ?", [mapId]);
|
||||
db.run('COMMIT');
|
||||
saveDb();
|
||||
console.log(`Done: ${written} hexes written, ${errors} errors.`);
|
||||
} catch (err) {
|
||||
db.run('ROLLBACK');
|
||||
console.error('Transaction failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(err => { console.error('Fatal:', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user