// Offline generator for the Bloxorz level bank. // // Levels are hand-authored as ASCII maps (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). Every map is then run through // BloxorzLogic's BFS solver to compute `par`, and through a set of design gates // that hard-reject the build if a level is unsolvable, if a mechanic on the // board turns out to be decorative, or if the difficulty curve regresses. // // Legend // (space) void . floor S start (standing) G goal hole // # wall ~ fragile : split-flagged floor // 1 2 3 bridge tiles for groups 1-3 // A B C heavy switch — must come to rest STANDING on it (toggles 1-3) // a b c soft switch — any contact, incl. a split cube (toggles 1-3) // p q r hold pad — bridge open only while a LYING block covers it // t u v teleport pads — each letter must appear exactly twice // // Per-level options: { open: [1] } starts those bridge groups open, // { links: { A: [1, 2] } } wires a switch char to extra bridge groups. // // Usage: node tools/genBloxorz.js [outFile] import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseMap, auditLevel } from './bloxorzMap.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'); const LEVELS = []; const lvl = (n, name, art, opts) => { LEVELS.push(parseMap(n, name, art, opts)); }; // ── Act I (1-6): rolling. Shape, parity and the 1x2 footprint. ─────────────── lvl(1, 'First Roll', ` ....... .S..... ....... .....G. ....... `); lvl(2, 'Mind the Gap', ` ......... .S....... .... ... .... ... ......... .......G. `); lvl(3, 'Right Angle', ` ..... .S... ..... .. .. ..... ...G. ..... `); lvl(4, 'Narrow Pass', ` .... ..... .S.. ..... ............ .... ..... ..G.. `); lvl(5, 'The Ring', ` ......... .S....... ... ... ... ... ... ... .......G. ......... `); lvl(6, 'Zigzag', ` ..... .S... ..... ...... ...... ...... ...... ...... ....G. ...... `); // ── Act II (7-12): switches and bridges. ───────────────────────────────────── // The heavy X switch only counts if the block comes to rest upright on it. lvl(7, 'Heavy Switch', ` ..... ..... .S... ..... .....11.G... ..... ..... ..A.. ..... `); // Soft switches trip on any contact — including in passing. Trip both and you // have toggled the bridge shut again. lvl(8, 'Light Touch', ` ......... .S....... ..a...a.. ......... 1 1 ......... ....G.... ......... `); // A hold pad only counts while a LYING block is on it, so the bridge is open // for exactly the one move that rolls off the pad onto it. lvl(9, 'Hold the Pad', ` .... .S.. .... ...p1..... ..... ..G.. `); lvl(10, 'The Long Way', ` ...... ... .S.... ... ...... ... ... ... ... ... ......111... ...... ... ..A... .G. ...... ... `); lvl(11, 'Two Gates', ` .... ..... .... .S.. ..B.. .... ....11.....22.G.. ..A. ..... .... `); // Bridge 1 starts open and bridge 2 shut; the switch in the middle room swaps // them, so the way in is also the way you give up. lvl(12, 'Shuttle', ` .... .... .... .S.. .A.. .... ....11......22..G... .... .... .... `, { open: [1], links: { A: [1, 2] } }); // ── Act III (13-18): fragile ground. Standing upright on orange puts the block // straight through it, so these are orientation puzzles: you cross lying, which // means shuffling sideways, and you can only change axis on solid ground. ──── lvl(13, 'Two at a Time', ` .... .S.. .... ~~ ~~ .... ..G. .... `); lvl(14, 'Cracked Gate', ` ..... ..~~~.. .S... ..~~~.. .....11..~~~G. ..... ..~~~.. ..A.. ..~~~.. `); lvl(15, 'Corner Ice', ` ....~~~~~~ .S..~~~~~~ ....~~~~~~ ~~~~~~ ~~~~~~ ...... ...G.. `); lvl(16, 'Thin Ice', ` ....~~~~.... .S..~~~~..G. ....~~~~.... `); lvl(17, 'Collapse', ` ..... .~~~~~.. .S... .~~~~~.. .....11.~~~~~G. ..... .~~~~~.. ..A.. .~~~~~.. `); lvl(18, 'Stepping Stones', ` ....~~....~~.... .S..~~....~~..G. ....~~....~~.... `); // ── Act IV (19-24): teleport pads. Only a STANDING landing warps. ──────────── lvl(19, 'Warp', ` ....... .S..... ...t... ....... ....... ...t... ....... ..G.... `); lvl(20, 'Brittle Warp', ` ...... .S.... ...t.. ...... ~~~~~~~ ~~t~~~~ ~~~~~~~ ~~~~G.. `); lvl(21, 'Crossed Wires', ` ...... ...... .S..t. ..u... ...... ...... ...t.. ...... ..u... ...G.. ...... `); lvl(22, 'Round Trip', ` ...... ...... .S.... ..t... ...t.. ...... ....u. ...... ..u... ...... ...G.. ...... `); lvl(23, 'Warp Gate', ` ..... ...... .S...11...t.. ..... ...... ..A.. ...... ...... ..t... ....G. ...... `); lvl(24, 'Warp Maze', ` ..... ...... .S... ..t... .....11...... ..A.. ...... ..... ...... ...... ..... ..t... ..u.. ...... ..... ...u.. ..G.. ...... ..... `); // ── Act V (25-30): splitting. Two cubes, one input between them. The ground is // narrow on purpose — an open room lets a joined block fix its parity by // walking around, which would make the split optional. ────────────────────── // The pair rejoins on the very next move, one cell further along than a plain // tip would have landed: splitting is a parity shift, and this corridor's goal // sits on the one parity a rolling block can never reach. lvl(25, 'Break Apart', ` .... .S.. .... . . ....::....G `); lvl(26, 'Split Gate', ` .... .S.. .... . . ....::....A 1 1 ..... ..G.. ..... `); lvl(27, 'Split Warp', ` .... .S.. .... . . ....::..t.. ...... ..t... ....G. ...... `); // Here the wall catches one cube while the other rolls on, so the two travel // the board separately and have to be lined back up to fuse. lvl(28, 'Held Back', ` S::........ .#..##...#. .......#... ........#.. ........... ........#G. `); lvl(29, 'Split Ice', ` .... .S.. .... ~~ ~~ ~~ ......::....G `); lvl(30, 'Reassembly', ` S::........ ..#.....#.. .......#... ...#....#.. ........... ........#G. `); // ── Act VI (31-36): everything at once, on bigger boards. ──────────────────── lvl(31, 'Warp and Crack', ` ..... ..~~~.. .S...11..~~~.. ..... ..~~~.. ..A.. ..~~~.. ..... ..~t~.. ....... ...t... ....... ..G.... ....... `); lvl(32, 'Gatehouse', ` ..... ..~~~.. ..... .S... ..~~~.. ..... .....11..~~~..22..G.. ..... ..~~~.. ..... ..A.. ..B~~.. ..... `); lvl(33, 'Pillars', ` S::.......... ..#.......#.. .#........... ............. ............. ...#.#....... .........#.#. ........#..G. `); lvl(34, 'The Gauntlet', ` ..... ..~~~~.. .S...11..~~~~.. ..... ..~~~~.. ..A.. ..~~~~.. ..... ..~~~~.. . . ....::....G `); lvl(35, 'Two Wings', ` ..A.. ..... . ..... .......... ..... .S...11..........22..G.. ..... .......... ..... . ..... ..B.. `); lvl(36, 'Grand Finale', ` ..... ....... .S...11...t... ..... ....... ..A.. ....... ..~~~~.. ..~~~~.. t.~~~~.. ..~~~~.. ..~~~~.. . . ....::....G `); // ── Solve-gate + write ─────────────────────────────────────────────────────── console.log(`[bloxorz] auditing ${LEVELS.length} hand-authored levels…`); let failed = 0; const levels = LEVELS.map((def) => { const { par, problems, notes } = auditLevel(def); if (problems.length) { console.error(` ✗ level ${def.level} "${def.name}" — ${problems.join(', ')}`); failed++; } else { const tail = notes.length ? ` (note: ${notes.join('; ')})` : ''; console.log(` ✓ level ${def.level} "${def.name}" — ${def.cols}x${def.rows}, par ${par}${tail}`); } return { ...def, par }; }); // The whole point of the overhaul: difficulty has to actually climb. Compare // six-level acts rather than adjacent levels, which leaves room for a breather // after a hard one without letting a whole act flatten out. const acts = []; for (let i = 0; i < levels.length; i += 6) { const slice = levels.slice(i, i + 6); acts.push(slice.reduce((s, l) => s + l.par, 0) / slice.length); } acts.forEach((avg, i) => console.log(`[bloxorz] act ${i + 1} average par ${avg.toFixed(1)}`)); for (let i = 1; i < acts.length; i++) { if (acts[i] <= acts[i - 1]) { console.error(` ✗ act ${i + 1} (avg par ${acts[i].toFixed(1)}) is no harder than act ${i} (${acts[i - 1].toFixed(1)})`); failed++; } } if (failed > 0) { console.error(`[bloxorz] ${failed} problem(s) — 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}`);