// 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 public/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 '../../public/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, '../../public/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);