112 lines
3.9 KiB
JavaScript
112 lines
3.9 KiB
JavaScript
// Offline generator for Dot Link puzzles.
|
|
//
|
|
// Builds a smooth easy->legendary curve of 100 Flow-Free boards by growing a
|
|
// random Hamiltonian path over each grid (backbite) and cutting it into colour
|
|
// segments, then preferring boards the solver proves uniquely solvable. Writes
|
|
// ordered levels to data/dotlink.json.
|
|
//
|
|
// Usage:
|
|
// node server/scripts/genDotLink.js [seed] [outFile]
|
|
//
|
|
// Deterministic: same seed -> same bank.
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { makeRng, generateBoard, isSolved } from '../src/games/dotlink/DotLinkLogic.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const OUT_FILE = process.argv[3]
|
|
? path.resolve(process.argv[3])
|
|
: path.join(__dirname, '../data/dotlink.json');
|
|
const SEED = process.argv[2] ? Number(process.argv[2]) >>> 0 : 0xd07117e5;
|
|
|
|
// Difficulty curve: grid size grows, colour count grows. counts sum to 100.
|
|
const TIERS = [
|
|
{ count: 12, rows: 5, cols: 5, colorsMin: 4, colorsMax: 5 },
|
|
{ count: 14, rows: 6, cols: 6, colorsMin: 5, colorsMax: 6 },
|
|
{ count: 14, rows: 7, cols: 7, colorsMin: 6, colorsMax: 7 },
|
|
{ count: 14, rows: 8, cols: 8, colorsMin: 7, colorsMax: 8 },
|
|
{ count: 14, rows: 9, cols: 9, colorsMin: 8, colorsMax: 9 },
|
|
{ count: 12, rows: 10, cols: 10, colorsMin: 9, colorsMax: 10 },
|
|
{ count: 12, rows: 11, cols: 11, colorsMin: 10, colorsMax: 11 },
|
|
{ count: 8, rows: 11, cols: 11, colorsMin: 12, colorsMax: 12 },
|
|
];
|
|
|
|
function canonKey(board) {
|
|
return board.endpoints
|
|
.map((e) => {
|
|
const a = e.a[0] * board.cols + e.a[1];
|
|
const b = e.b[0] * board.cols + e.b[1];
|
|
return a < b ? `${a}-${b}` : `${b}-${a}`;
|
|
})
|
|
.sort()
|
|
.join('|');
|
|
}
|
|
|
|
const rng = makeRng(SEED);
|
|
console.log(`[dotlink] generating with seed 0x${SEED.toString(16)}…`);
|
|
|
|
const levels = [];
|
|
const seen = new Set();
|
|
let uniqueCount = 0;
|
|
const startedAt = Date.now();
|
|
|
|
for (const tier of TIERS) {
|
|
for (let i = 0; i < tier.count; i++) {
|
|
const span = tier.colorsMax - tier.colorsMin;
|
|
const colors = tier.count > 1
|
|
? tier.colorsMin + Math.round((i / (tier.count - 1)) * span)
|
|
: tier.colorsMin;
|
|
|
|
// generateBoard always returns a valid (solvable) board with its reference
|
|
// solution; prefer one that is uniquely solvable (only checked on small
|
|
// grids) and not a duplicate of an earlier level.
|
|
let chosen = null;
|
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
const g = generateBoard(tier.rows, tier.cols, colors, rng, { tries: 24, maxNodes: 200000 });
|
|
if (!g) continue;
|
|
const dup = seen.has(canonKey(g.board));
|
|
if (!chosen || (!dup && (g.unique || !chosen.unique))) chosen = g;
|
|
if (g.unique && !dup) break;
|
|
}
|
|
if (!chosen) {
|
|
console.error(`[dotlink] FAILED to generate a ${tier.rows}x${tier.cols}/${colors} board`);
|
|
process.exit(1);
|
|
}
|
|
seen.add(canonKey(chosen.board));
|
|
if (chosen.unique) uniqueCount++;
|
|
|
|
levels.push({
|
|
level: levels.length + 1,
|
|
rows: chosen.board.rows,
|
|
cols: chosen.board.cols,
|
|
colors,
|
|
unique: !!chosen.unique,
|
|
endpoints: chosen.board.endpoints,
|
|
solution: chosen.solution,
|
|
});
|
|
process.stdout.write(`\r[dotlink] built ${levels.length}/100 (unique ${uniqueCount}) `);
|
|
}
|
|
}
|
|
process.stdout.write('\n');
|
|
|
|
// Sanity pass: every level's stored reference solution must validate.
|
|
let unsolvable = 0;
|
|
for (const lv of levels) {
|
|
if (!isSolved(lv, lv.solution)) unsolvable++;
|
|
}
|
|
|
|
const payload = {
|
|
generatedAt: new Date().toISOString(),
|
|
seed: SEED,
|
|
count: levels.length,
|
|
levels,
|
|
};
|
|
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2));
|
|
|
|
const secs = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
console.log(`[dotlink] wrote ${levels.length} levels (${uniqueCount} unique, ${unsolvable} unsolvable) in ${secs}s -> ${OUT_FILE}`);
|
|
if (unsolvable > 0) process.exit(1);
|