// Offline generator for the Bloxorz level bank. // // Levels are hand-authored (switch/bridge/teleporter/split puzzles need // designed relational intent that random generation doesn't produce — see // Rush Hour/Dot Link for where randomize-then-filter DOES work, and note this // is deliberately NOT that). Each level is built from small composable tile // helpers below, then solved with BloxorzLogic's BFS solver to compute `par` // and hard-reject the whole build if any level turns out to be unsolvable. // // Usage: node tools/genBloxorz.js [outFile] import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadLevel, solve } from '../src/games/bloxorz/BloxorzLogic.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const OUT_FILE = process.argv[2] ? path.resolve(process.argv[2]) : path.join(__dirname, '../data/bloxorz.json'); // ── Tile helpers ───────────────────────────────────────────────────────────── const floor = (x, y, extra = {}) => ({ x, y, type: 'floor', ...extra }); const splitFloor = (x, y) => floor(x, y, { split: true }); const wallTile = (x, y) => ({ x, y, type: 'wall' }); const goalTile = (x, y) => ({ x, y, type: 'goal' }); const fragile = (x, y) => ({ x, y, type: 'fragile' }); const bridge = (x, y, bridgeId, initiallyOpen = false) => ({ x, y, type: 'bridge', bridgeId, initiallyOpen }); const hardSwitch = (x, y, switchId, bridgeIds) => ({ x, y, type: 'switch', switchId, linkedBridgeIds: bridgeIds, requireOrientation: 'any', mode: 'toggle', }); const softSwitch = (x, y, switchId, bridgeIds) => ({ x, y, type: 'switch', switchId, linkedBridgeIds: bridgeIds, requireOrientation: 'lying', mode: 'momentary', }); const teleport = (x, y, teleportId) => ({ x, y, type: 'teleport', teleportId }); function rect(x0, y0, w, h, extra = {}) { const out = []; for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) out.push(floor(x, y, extra)); return out; } // Later entries win on coordinate collision — lets a base rect/fill be // selectively overridden with bridges/switches/fragile/goal/etc. function compile(tileList) { const map = new Map(); for (const t of tileList) map.set(`${t.x},${t.y}`, t); return [...map.values()]; } function level(n, name, cols, rows, start, goal, tileList) { return { level: n, name, cols, rows, start, goal, tiles: compile(tileList) }; } // A single straight horizontal corridor at row `y`, columns 0..length. Rolling // right from a standing start at column 0 lands STANDING on columns 0,3,6,9… // and LYING on pairs (1,2),(4,5),(7,8)… — every level below places switches, // bridges, fragile tiles, teleporters and split-flags using that fixed // arithmetic, and always ends `length` on a multiple of 3 so the final // approach lands standing exactly on the goal. `extraRows` widens the board // so branch tiles (e.g. a switch reached by branching off the row) still fit. function corridorLevel(n, name, { length, y = 2, rows = 5, overrides = [] }) { const cols = length + 1; const tiles = []; for (let x = 0; x <= length; x++) tiles.push(floor(x, y)); tiles.push(...overrides); const hasGoal = overrides.some((o) => o.type === 'goal'); const goalPos = hasGoal ? overrides.find((o) => o.type === 'goal') : { x: length, y }; if (!hasGoal) tiles.push(goalTile(length, y)); return level(n, name, cols, rows, { x: 0, y, orient: 'up' }, { x: goalPos.x, y: goalPos.y }, tiles); } // ── Levels 1-6: plain rolling ──────────────────────────────────────────────── const L1 = level(1, 'Warm Up', 6, 5, { x: 1, y: 1, orient: 'up' }, { x: 4, y: 3 }, [ ...rect(0, 0, 6, 5), goalTile(4, 3), ]); const L2 = level(2, 'Stretch Out', 7, 5, { x: 1, y: 1, orient: 'up' }, { x: 5, y: 3 }, [ ...rect(0, 0, 7, 5), goalTile(5, 3), ]); const L3 = level(3, 'Open Floor', 8, 6, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 4 }, [ ...rect(0, 0, 8, 6), goalTile(6, 4), ]); const L4 = level(4, 'The Corner', 9, 7, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 5 }, [ ...rect(0, 0, 5, 7), ...rect(0, 4, 9, 3), goalTile(7, 5), ]); const L5 = level(5, 'Crossroads', 9, 9, { x: 4, y: 1, orient: 'up' }, { x: 1, y: 4 }, [ ...rect(3, 0, 3, 9), ...rect(0, 3, 9, 3), goalTile(1, 4), ]); const L6 = level(6, 'Staircase', 10, 9, { x: 1, y: 1, orient: 'up' }, { x: 8, y: 7 }, [ ...rect(0, 0, 4, 3), ...rect(2, 1, 4, 4), ...rect(4, 3, 4, 4), ...rect(6, 5, 4, 4), goalTile(8, 7), ]); // ── Levels 7-12: holes, ledges, static causeways ───────────────────────────── const L7 = level(7, 'Mind the Gap', 8, 6, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 4 }, compile(rect(0, 0, 8, 6)).filter((t) => !(t.x === 4 && t.y === 2)).concat(goalTile(6, 4))); const L8 = level(8, 'Pinwheel', 9, 7, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 5 }, compile(rect(0, 0, 9, 7)) .filter((t) => !([[4, 2], [4, 3], [4, 4], [3, 3], [5, 3]].some(([hx, hy]) => t.x === hx && t.y === hy))) .concat(goalTile(7, 5))); const L9 = level(9, 'Causeway', 9, 3, { x: 1, y: 1, orient: 'up' }, { x: 7, y: 1 }, [ ...rect(0, 0, 3, 3), ...rect(3, 1, 3, 1), ...rect(6, 0, 3, 3), goalTile(7, 1), ]); const L10 = level(10, 'Bent Causeway', 8, 8, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 6 }, [ ...rect(0, 0, 3, 3), ...rect(3, 1, 3, 2), ...rect(4, 1, 2, 5), ...rect(3, 5, 5, 3), goalTile(6, 6), ]); const L11 = level(11, 'Scattered', 10, 8, { x: 1, y: 1, orient: 'up' }, { x: 8, y: 6 }, compile(rect(0, 0, 10, 8)) .filter((t) => !([[3, 3], [6, 2], [4, 6], [7, 5], [2, 6]].some(([hx, hy]) => t.x === hx && t.y === hy))) .concat(goalTile(8, 6))); const L12 = level(12, 'Tightrope', 8, 8, { x: 1, y: 1, orient: 'up' }, { x: 6, y: 6 }, [ ...rect(0, 0, 3, 3), ...rect(1, 1, 3, 3), ...rect(2, 2, 3, 3), ...rect(3, 3, 3, 3), ...rect(4, 4, 3, 3), ...rect(5, 5, 3, 3), goalTile(6, 6), ]); // ── Levels 13-18: switches + bridges ───────────────────────────────────────── const L13 = corridorLevel(13, 'Flip the Switch', { length: 9, overrides: [hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false)], }); const L14 = corridorLevel(14, 'Weight and See', { length: 6, overrides: [softSwitch(2, 2, 's1', ['b1']), bridge(3, 2, 'b1', false)], }); const L15 = corridorLevel(15, 'Double Gate', { length: 12, overrides: [hardSwitch(2, 2, 's1', ['b1', 'b2']), bridge(4, 2, 'b1', false), bridge(8, 2, 'b2', false)], }); const L16 = corridorLevel(16, 'Two Keys', { length: 15, overrides: [ hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), hardSwitch(7, 2, 's2', ['b2']), bridge(10, 2, 'b2', false), ], }); const L17 = corridorLevel(17, 'Soft Landing', { length: 15, overrides: [ hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), softSwitch(8, 2, 's2', ['b2']), bridge(9, 2, 'b2', false), ], }); const L18 = corridorLevel(18, 'Three Gates', { length: 18, overrides: [ hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), softSwitch(8, 2, 's2', ['b2']), bridge(9, 2, 'b2', false), hardSwitch(13, 2, 's3', ['b3']), bridge(16, 2, 'b3', false), ], }); // ── Levels 19-24: fragile tiles ─────────────────────────────────────────────── const L19 = corridorLevel(19, 'Cracked Floor', { length: 9, overrides: [fragile(3, 2)] }); const L20 = corridorLevel(20, 'Eggshells', { length: 12, overrides: [fragile(3, 2), fragile(7, 2)] }); const L21 = corridorLevel(21, 'No Turning Back', { length: 15, overrides: [fragile(3, 2), fragile(6, 2), fragile(10, 2)], }); const L22 = corridorLevel(22, 'Fragile Gate', { length: 12, overrides: [hardSwitch(2, 2, 's1', ['b1']), bridge(5, 2, 'b1', false), fragile(8, 2)], }); const L23 = corridorLevel(23, 'Soft and Thin', { length: 15, overrides: [fragile(3, 2), softSwitch(7, 2, 's1', ['b1']), bridge(9, 2, 'b1', false), fragile(11, 2)], }); const L24 = corridorLevel(24, 'Every Step Counts', { length: 18, overrides: [ fragile(3, 2), hardSwitch(6, 2, 's1', ['b1']), bridge(8, 2, 'b1', false), fragile(11, 2), softSwitch(14, 2, 's2', ['b2']), bridge(15, 2, 'b2', false), ], }); // ── Levels 25-30: teleporters ───────────────────────────────────────────────── const L25 = corridorLevel(25, 'Shortcut', { length: 13, overrides: [teleport(3, 2, 't1'), teleport(10, 2, 't1')] }); const L26 = corridorLevel(26, 'Two Hops', { length: 21, overrides: [teleport(3, 2, 't1'), teleport(9, 2, 't1'), teleport(12, 2, 't2'), teleport(18, 2, 't2')], }); const L27 = corridorLevel(27, 'Triple Hop', { length: 27, overrides: [ teleport(3, 2, 't1'), teleport(9, 2, 't1'), teleport(12, 2, 't2'), teleport(18, 2, 't2'), teleport(21, 2, 't3'), teleport(24, 2, 't3'), ], }); const L28 = corridorLevel(28, 'Gate and Go', { length: 15, overrides: [ hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), teleport(6, 2, 't1'), teleport(12, 2, 't1'), ], }); const L29 = corridorLevel(29, 'Brittle Hop', { length: 12, overrides: [fragile(3, 2), teleport(6, 2, 't1'), teleport(9, 2, 't1')], }); const L30 = corridorLevel(30, 'All at Once', { length: 18, overrides: [ softSwitch(2, 2, 's1', ['b1']), bridge(3, 2, 'b1', false), fragile(6, 2), teleport(9, 2, 't1'), teleport(15, 2, 't1'), ], }); // ── Levels 31-36: split tiles ───────────────────────────────────────────────── const L31 = corridorLevel(31, 'Split Decision', { length: 10, overrides: [splitFloor(4, 2), splitFloor(5, 2)], }); // NOTE: a split-trigger-then-immediate-remerge costs one extra column versus // a plain tip (the remerge lands one column further right than a same-shape // non-split roll would have) — lengths below are chosen with that in mind. const L32 = corridorLevel(32, 'Split and Gate', { length: 19, overrides: [ hardSwitch(2, 2, 's1', ['b1']), bridge(4, 2, 'b1', false), splitFloor(7, 2), splitFloor(8, 2), ], }); const L33 = corridorLevel(33, 'Split and Crack', { length: 15, overrides: [fragile(3, 2), splitFloor(6, 2), splitFloor(7, 2)], }); const L34 = corridorLevel(34, 'Split and Hop', { length: 16, overrides: [splitFloor(4, 2), splitFloor(5, 2), teleport(7, 2, 't1'), teleport(13, 2, 't1')], }); // A wide open room around the split point (not a tight corridor) so the two // independent units have plenty of room to sidestep the wall pillars and // reconverge — the BFS solver (not hand-traced choreography) is the actual // proof this works. const L35 = level(35, 'Around the Pillar', 16, 7, { x: 1, y: 3, orient: 'up' }, { x: 14, y: 3 }, [ ...rect(0, 2, 5, 3), // entry platform, rows 2-4 splitFloor(5, 3), splitFloor(6, 3), // split trigger, row 3 ...rect(5, 0, 7, 7), // big open room, rows 0-6, cols 5-11 wallTile(8, 2), wallTile(8, 3), wallTile(8, 4), // pillar splitting the room ...rect(12, 2, 4, 3), // exit platform, rows 2-4 goalTile(14, 3), ]); // Open rooms throughout (not a tight single-row corridor) so switch/fragile/ // split placement doesn't depend on exact tip arithmetic — only reachability // within each room, which the BFS solver confirms directly. const L36 = level(36, 'Grand Finale', 22, 5, { x: 1, y: 1, orient: 'up' }, { x: 19, y: 2 }, [ ...rect(0, 0, 5, 4), // room A: start + switch hardSwitch(2, 2, 's1', ['b1']), bridge(5, 1, 'b1', false), // gate into room B ...rect(6, 0, 5, 4), // room B: fragile + split trigger fragile(7, 2), splitFloor(9, 1), splitFloor(10, 1), ...rect(11, 0, 7, 5), // room C: pillar maze wallTile(14, 1), wallTile(14, 2), wallTile(14, 3), ...rect(18, 0, 4, 4), // room D: goal goalTile(19, 2), ]); const LEVELS = [ L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15, L16, L17, L18, L19, L20, L21, L22, L23, L24, L25, L26, L27, L28, L29, L30, L31, L32, L33, L34, L35, L36, ]; // ── Solve-gate + write ──────────────────────────────────────────────────────── console.log(`[bloxorz] solving ${LEVELS.length} hand-authored levels…`); let failed = 0; const levels = LEVELS.map((def) => { const compiled = loadLevel(def); const { moves } = solve(compiled, { maxStates: 300000 }); if (moves < 0) { console.error(` ✗ level ${def.level} "${def.name}" is UNSOLVABLE`); failed++; return { ...def, par: -1 }; } console.log(` ✓ level ${def.level} "${def.name}" — par ${moves}`); return { ...def, par: moves }; }); if (failed > 0) { console.error(`[bloxorz] ${failed} level(s) unsolvable — refusing to write ${OUT_FILE}`); process.exit(1); } const payload = { generatedAt: new Date().toISOString(), seed: null, count: levels.length, levels, }; fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true }); fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2)); console.log(`[bloxorz] wrote ${levels.length} levels -> ${OUT_FILE}`);