Skip to content

Instantly share code, notes, and snippets.

@HamsterofDeath
Created February 8, 2025 21:28
Show Gist options
  • Save HamsterofDeath/ef2745e1cb4bc9aaf67bed4ec29e27cb to your computer and use it in GitHub Desktop.
Save HamsterofDeath/ef2745e1cb4bc9aaf67bed4ec29e27cb to your computer and use it in GitHub Desktop.
minigames.set('LaserHack', {
autorun: true,
html: `
<div class="aurigan_ui_container laserhack_container">
<h2 class="aurigan_ui_heading">Laser Hack</h2>
<div class="level-indicator-container" id="laserhack-levels"></div>
<div class="game-container">
<canvas id="laserhack-canvas"></canvas>
</div>
</div>
<style>
.aurigan_ui_container.laserhack_container {
display: flex;
flex-direction: column;
height: calc(100% - 40px);
width: 100%;
position: relative;
padding: 20px;
background-color: #111;
}
.aurigan_ui_heading {
margin: 0 0 10px 0;
color: #fff;
font-family: Arial, sans-serif;
text-align: center;
font-size: 24px;
}
.level-indicator-container {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.level-block {
width: 10px;
height: 10px;
background-color: #444;
margin: 0 2px;
border-radius: 2px;
}
.level-block.active {
background-color: #0f0;
}
.game-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background-color: #222;
border-radius: 8px;
overflow: hidden;
}
#laserhack-canvas {
width: 100%;
height: 100%;
display: block;
}
.splitter-selecting {
border: 2px dashed yellow;
}
</style>
`,
init: (container, endCallback, config) => {
// ---------------- Constants & Utilities ----------------
const DEBUG_MODE = true;
const COLS = 16, ROWS = 16;
const maxLevels = 30;
const levels = Array.from({ length: maxLevels }, (_, i) => i + 1);
// Tile types
const TILE_EMPTY = 'EMPTY';
const TILE_LASER = 'LASER';
const TILE_MIRROR = 'MIRROR';
const TILE_SPLITTER = 'SPLITTER';
const TILE_RECEPTOR = 'RECEPTOR';
// Laser colors and direction helpers
const LASER_COLORS = ['red', 'green', 'blue'];
const directionNames = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const invSqrt2 = 1 / Math.sqrt(2);
const directionVectors = [
{ x: 0, y: -1 }, // N (0)
{ x: invSqrt2, y: -invSqrt2 }, // NE (1)
{ x: 1, y: 0 }, // E (2)
{ x: invSqrt2, y: invSqrt2 }, // SE (3)
{ x: 0, y: 1 }, // S (4)
{ x: -invSqrt2, y: invSqrt2 }, // SW (5)
{ x: -1, y: 0 }, // W (6)
{ x: -invSqrt2, y: -invSqrt2 } // NW (7)
];
const mirrorLineVectors = [
{ x: 1, y: 0 },
{ x: invSqrt2, y: invSqrt2 },
{ x: 0, y: 1 },
{ x: -invSqrt2, y: invSqrt2 },
{ x: -1, y: 0 },
{ x: -invSqrt2, y: -invSqrt2 },
{ x: 0, y: -1 },
{ x: invSqrt2, y: -invSqrt2 }
];
// Simple random utilities
function randInt(n) {
return Math.floor(Math.random() * n);
}
function shuffleArray(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
}
// ---------------- Element Queue Factory (Modified to Flat Queue) ----------------
let elementIdCounter = 0; // To generate unique IDs
function makeQueue(levelIndex) {
elementIdCounter = 0;
const laserId = `element-${++elementIdCounter}`;
let queue = [{ id: laserId, type: TILE_LASER, dependency: null }]; // Laser is now in queue
function makeSubQueue(n, dependencyId = laserId, previousElementId = laserId) { // Default dependency to laserId
let currentDependencyId = dependencyId;
let lastElementId = previousElementId;
if (n <= 0) {
return [{ id: `element-${++elementIdCounter}`, type: TILE_RECEPTOR, dependency: lastElementId || currentDependencyId }];
}
if (n <= 3) {
let arr = [];
for (let i = 0; i < n; i++) {
const mirrorId = `element-${++elementIdCounter}`;
arr.push({ id: mirrorId, type: TILE_MIRROR, dependency: currentDependencyId });
lastElementId = mirrorId;
currentDependencyId = mirrorId;
}
arr.push({ id: `element-${++elementIdCounter}`, type: TILE_RECEPTOR, dependency: lastElementId || dependencyId });
return arr;
} else {
const splitterId = `element-${++elementIdCounter}`;
const splitterElement = { id: splitterId, type: TILE_SPLITTER, dependency: lastElementId || dependencyId };
lastElementId = splitterId;
currentDependencyId = splitterId;
const subQueue1 = makeSubQueue(n - 3, currentDependencyId, lastElementId);
const subQueue2 = makeSubQueue(1, currentDependencyId, lastElementId);
return [
{ id: `element-${++elementIdCounter}`, type: TILE_MIRROR, dependency: dependencyId },
{ id: `element-${++elementIdCounter}`, type: TILE_MIRROR, dependency: dependencyId },
{ id: `element-${++elementIdCounter}`, type: TILE_MIRROR, dependency: dependencyId },
splitterElement,
...subQueue1,
...subQueue2
];
}
}
queue = [...queue, ...makeSubQueue(levelIndex + 1, laserId, laserId)]; // Chain from laser
return queue;
}
function elementQueueFactory(levelIndex) {
return makeQueue(levelIndex);
}
// ---------------- Puzzle Element & Logic ----------------
// Defines individual puzzle cell objects (immutable).
class PuzzleElement {
constructor(type, x, y, rotation = 0, color = null, lit = false, splitDirs = []) {
this.type = type;
this.x = x;
this.y = y;
this.rotation = rotation;
this.color = color;
this.lit = lit;
this.splitDirs = [...splitDirs]; // Copy to preserve immutability
}
withType(type) {
return new PuzzleElement(type, this.x, this.y, this.rotation, this.color, this.lit, this.splitDirs);
}
withRotation(rotation) {
return new PuzzleElement(this.type, this.x, this.y, rotation, this.color, this.lit, this.splitDirs);
}
withColor(color) {
return new PuzzleElement(this.type, this.x, this.y, this.rotation, color, this.lit, this.splitDirs);
}
withLit(lit) {
return new PuzzleElement(this.type, this.x, this.y, this.rotation, this.color, lit, this.splitDirs);
}
withSplitDirs(splitDirs) {
return new PuzzleElement(this.type, this.x, this.y, this.rotation, this.color, this.lit, [...splitDirs]);
}
}
// ---------------- PuzzleRenderer ----------------
// Draws the grid, beams, and tiles.
class PuzzleRenderer {
constructor(canvas, puzzle) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.puzzle = puzzle;
this.cellSize = 0;
}
// Adjust canvas size and compute cell dimensions.
resize() {
this.canvas.width = this.canvas.getBoundingClientRect().width;
this.canvas.height = this.canvas.getBoundingClientRect().height;
this.cellSize = Math.min(this.canvas.width / this.puzzle.cols, this.canvas.height / this.puzzle.rows);
}
// Draw the grid, cells, and beam paths.
draw(splitterState) {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const cs = this.cellSize;
for (let r = 0; r < this.puzzle.rows; r++) {
for (let c = 0; c < this.puzzle.cols; c++) {
const px = c * cs, py = r * cs;
ctx.fillStyle = '#333';
ctx.fillRect(px, py, cs, cs);
ctx.strokeStyle = '#555';
ctx.strokeRect(px, py, cs, cs);
// Debug: display cell coordinates.
if (DEBUG_MODE) {
ctx.fillStyle = '#888';
ctx.font = '8px Arial';
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillText(c + ',' + r, px + cs - 2, py + cs - 2);
}
const tile = this.puzzle.grid[r][c];
ctx.save();
ctx.translate(px + cs / 2, py + cs / 2);
ctx.rotate(tile.rotation * (Math.PI / 4));
if (tile.type === TILE_LASER) {
ctx.beginPath();
ctx.moveTo(0, -cs * 0.35);
ctx.lineTo(cs * 0.4, cs * 0.3);
ctx.lineTo(0, cs * 0.15);
ctx.lineTo(-cs * 0.4, cs * 0.3);
ctx.closePath();
ctx.fillStyle = tile.color || 'red';
ctx.fill();
if (DEBUG_MODE) {
ctx.fillStyle = 'white';
ctx.font = '8px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(`${directionNames[tile.rotation]} (${tile.rotation})`, 0, cs * 0.4);
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(directionVectors[tile.rotation].x * cs * 0.2, directionVectors[tile.rotation].y * cs * 0.2);
ctx.stroke();
}
} else if (tile.type === TILE_MIRROR) {
ctx.fillStyle = '#bbb';
ctx.fillRect(-cs / 2 + 2, -3, cs - 4, 6);
} else if (tile.type === TILE_RECEPTOR) {
ctx.beginPath();
ctx.arc(0, 0, cs / 3, 0, 2 * Math.PI);
ctx.strokeStyle = tile.lit ? tile.color : '#999';
ctx.lineWidth = tile.lit ? 3 : 1;
ctx.stroke();
} else if (tile.type === TILE_SPLITTER) {
ctx.fillStyle = '#0cf';
ctx.beginPath();
ctx.moveTo(-cs * 0.3, 0);
ctx.lineTo(0, -cs * 0.3);
ctx.lineTo(cs * 0.3,0);
ctx.lineTo(0, cs * 0.3);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
}
// Draw beams on top of the grid.
for (const b of this.puzzle.beams) {
ctx.beginPath();
ctx.moveTo(b.x1 * cs, b.y1 * cs);
ctx.lineTo(b.x2 * cs, b.y2 * cs);
ctx.strokeStyle = b.color;
ctx.lineWidth = 2;
ctx.stroke();
}
// If selecting splitter directions, draw helper lines.
if (splitterState.active) {
const px = splitterState.x * cs + cs / 2;
const py = splitterState.y * cs + cs / 2;
ctx.strokeStyle = 'yellow';
ctx.lineWidth = 1;
for (let i = 0; i < 8; i++) {
const dv = directionVectors[i];
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + dv.x * cs, py + dv.y * cs);
ctx.stroke();
}
}
}
}
// Manages the grid, beam simulation, and cell locking (immutable operations).
class PuzzleLogic {
constructor(cols, rows, grid = null, beams = null, lockedCells = null, laserColor = "red", paths = null) {
this.cols = cols;
this.rows = rows;
this.grid = grid || (() => {
const initialGrid = [];
for (let r = 0; r < rows; r++) {
initialGrid[r] = [];
for (let c = 0; c < cols; c++) {
initialGrid[r][c] = new PuzzleElement(TILE_EMPTY, c, r);
}
}
return initialGrid;
})();
this.beams = beams || [];
this.lockedCells = lockedCells ? new Set(lockedCells) : new Set();
this.laserColor = laserColor;
this.paths = paths || [];
}
clone() {
const clonedGrid = this.grid.map(row => row.map(cell =>
new PuzzleElement(cell.type, cell.x, cell.y, cell.rotation, cell.color, cell.lit, cell.splitDirs)
));
return new PuzzleLogic(this.cols, this.rows, clonedGrid, [...this.beams], new Set(this.lockedCells), this.laserColor, [...this.paths]);
}
// --- Immutable operations ---
withGrid(newGrid) {
return new PuzzleLogic(this.cols, this.rows, newGrid, this.beams, this.lockedCells, this.laserColor, this.paths);
}
withUpdatedTile(r, c, tileUpdateFn) {
if (r < 0 || r >= this.rows || c < 0 || c >= this.cols) {
console.error(`withUpdatedTile: Invalid grid coordinates: r=${r}, c=${c}`);
return this; // Or throw an error, depending on desired behavior
}
const newGrid = this.grid.map(row => [...row]);
newGrid[r] = [...newGrid[r]];
newGrid[r][c] = tileUpdateFn(this.grid[r][c]);
return this.withGrid(newGrid);
}
withLockedCells(newLockedCells) {
return new PuzzleLogic(this.cols, this.rows, this.grid, this.beams, new Set(newLockedCells), this.laserColor, this.paths);
}
withLaserColor(newLaserColor) {
return new PuzzleLogic(this.cols, this.rows, this.grid, this.beams, this.lockedCells, newLaserColor, this.paths);
}
withBeams(newBeams) {
return new PuzzleLogic(this.cols, this.rows, this.grid, newBeams, this.lockedCells, this.laserColor, this.paths);
}
withPaths(newPaths) {
return new PuzzleLogic(this.cols, this.rows, this.grid, this.beams, this.lockedCells, this.laserColor, newPaths);
}
// Given an incoming direction and a mirror rotation, compute the reflection direction.
mirrorReflect(dirIndex, mirrorRot) {
const d = directionVectors[dirIndex];
const m = mirrorLineVectors[mirrorRot];
const dotDM = d.x * m.x + d.y * m.y;
if (Math.abs(dotDM) > 0.9999) return -1;
const n = { x: -m.y, y: m.x };
const dotDN = d.x * n.x + d.y * n.y;
const rx = d.x - 2 * dotDN * n.x;
const ry = d.y - 2 * dotDN * n.y;
const rLen = Math.sqrt(rx * rx + ry * ry);
if (rLen < 0.0001) return -1;
let bestDir = -1, bestDot = -999;
const rnx = rx / rLen, rny = ry / rLen;
for (let i = 0; i < 8; i++) {
const dv = directionVectors[i];
const dot = dv.x * rnx + dv.y * rny;
if (dot > bestDot) {
bestDot = dot;
bestDir = i;
}
}
return bestDot > 0.99 ? bestDir : -1;
}
// Modified onBeamEnter: for splitters, check and mark them as used.
onBeamEnter(dirIndex, tile, color, usedSplitters) {
switch (tile.type) {
case TILE_EMPTY:
return dirIndex;
case TILE_LASER: // Modified: Laser blocks incoming lasers
return -1;
case TILE_MIRROR:
return this.mirrorReflect(dirIndex, tile.rotation);
case TILE_RECEPTOR:
if (tile.color === color) {
tile.lit = true;
}
return -1;
case TILE_SPLITTER: {
const key = `${tile.x},${tile.y}`;
if (usedSplitters.has(key)) return -1;
usedSplitters.add(key);
if (tile.splitDirs.length < 2) return -1;
if (tile.splitDirs.includes(dirIndex)) return -1;
return tile.splitDirs;
}
default:
return -1;
}
}
// Runs beam simulation from all laser sources.
simulateBeams() {
let nextPuzzle = this.withBeams([]);
let mutableGrid = nextPuzzle.grid.map(row => row.map(cell => ({ ...cell, lit: false })));
nextPuzzle = nextPuzzle.withGrid(mutableGrid.map(row => row.map(cell =>
new PuzzleElement(cell.type, cell.x, cell.y, cell.rotation, cell.color, cell.lit, cell.splitDirs)
)));
const beams = [];
const paths = [];
const visited = new Set();
const usedSplitters = new Set(); // Track which splitters have been used
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) {
const tile = this.grid[r][c];
if (tile.type === TILE_LASER) {
const startPath = [{
x: c,
y: r,
dirIndex: tile.rotation
}];
this.traceBeamAndPath(c, r, tile.rotation, tile.color, visited, startPath, beams, paths, nextPuzzle, usedSplitters);
}
}
}
return nextPuzzle.withBeams(beams).withPaths(paths);
}
// Recursive beam tracing.
traceBeamAndPath(x, y, dirIndex, color, visited, currentPath, beams, paths, puzzleState, usedSplitters) {
const key = `${x},${y},${dirIndex},${color}`;
if (visited.has(key)) return;
visited.add(key);
const dv = directionVectors[dirIndex];
const nx = x + (dv.x > 0 ? Math.ceil(dv.x) : Math.floor(dv.x));
const ny = y + (dv.y > 0 ? Math.ceil(dv.y) : Math.floor(dv.y));
// Beam goes off grid.
if (nx < 0 || nx >= this.cols || ny < 0 || ny >= this.rows) {
paths.push([...currentPath]);
return;
}
beams.push({
x1: x + 0.5,
y1: y + 0.5,
x2: nx + 0.5,
y2: ny + 0.5,
color
});
const tile = puzzleState.grid[ny][nx];
const nd = puzzleState.onBeamEnter(dirIndex, tile, color, usedSplitters);
currentPath.push({
x: nx,
y: ny,
dirIndex: Array.isArray(nd) ? nd[0] : nd
});
if (Array.isArray(nd)) {
for (const splitDir of nd) {
const splitPath = [...currentPath];
splitPath[splitPath.length - 1].dirIndex = splitDir;
this.traceBeamAndPath(nx, ny, splitDir, color, visited, splitPath, beams, paths, puzzleState, usedSplitters);
}
} else if (nd >= 0) {
this.traceBeamAndPath(nx, ny, nd, color, visited, currentPath, beams, paths, puzzleState, usedSplitters);
} else {
paths.push([...currentPath]);
}
}
// Returns all beam paths.
simulateAndGetAllPaths() {
const simulationResult = this.simulateBeams();
return simulationResult.paths;
}
// Lock cells along a beam path.
lockCellsUpToIndex(path, idx) {
let locked = new Set(this.lockedCells);
let lockedCoords = [];
for (let i = 0; i <= idx; i++) {
const { x, y } = path[i];
const key = `${x},${y}`;
locked.add(key);
lockedCoords.push(`(${x},${y})`);
}
return { updatedPuzzle: this.withLockedCells(locked), lockedCells: lockedCoords };
}
// Returns the sub-branch of a beam path starting after a given cell.
getCandidateBranch(fullPath, placedX, placedY) {
const idx = fullPath.findIndex(pt => pt.x === placedX && pt.y === placedY);
if (idx < 0) return [];
return fullPath.slice(idx + 1);
}
}
// ---------------- PuzzleGenerator with Backtracking (Modified for Flat Queue and Verbose Log) ----------------
class PuzzleGenerator {
constructor(puzzle, elementQueue, verbose = false) {
this.puzzle = puzzle;
this.initialElementQueue = elementQueue;
this.strategies = {
[TILE_MIRROR]: this.placeMirror.bind(this),
[TILE_SPLITTER]: this.placeSplitter.bind(this),
[TILE_RECEPTOR]: this.placeReceptor.bind(this)
};
this.placedElements = {};
this.verbose = verbose;
this.backtrackCounter = 0;
}
placeLaser(elementSpec) {
let nextPuzzle = new PuzzleLogic(this.puzzle.cols, this.puzzle.rows);
nextPuzzle = nextPuzzle.withLockedCells(new Set());
for (let r = 0; r < nextPuzzle.rows; r++) {
for (let c = 0; c < nextPuzzle.cols; c++) {
nextPuzzle = nextPuzzle.withUpdatedTile(r, c, (tile) =>
tile.withType(TILE_EMPTY).withRotation(0).withColor(null).withSplitDirs([])
);
}
}
let lx, ly, ldir;
while (true) {
lx = randInt(nextPuzzle.cols);
ly = randInt(nextPuzzle.rows);
ldir = randInt(8);
const dv = directionVectors[ldir];
if (lx === 0 && dv.x < 0) continue;
if (lx === nextPuzzle.cols - 1 && dv.x > 0) continue;
if (ly === 0 && dv.y < 0) continue;
if (ly === nextPuzzle.rows - 1 && dv.y > 0) continue;
break;
}
let laserColor = LASER_COLORS[randInt(LASER_COLORS.length)];
nextPuzzle = nextPuzzle.withLaserColor(laserColor);
nextPuzzle = nextPuzzle.withUpdatedTile(ly, lx, (tile) =>
tile.withType(TILE_LASER).withRotation(ldir).withColor(laserColor)
);
this.puzzle = nextPuzzle;
if (elementSpec && elementSpec.rotation !== undefined && elementSpec.color !== undefined) {
nextPuzzle = nextPuzzle.withUpdatedTile(ly, lx, (tile) =>
tile.withRotation(elementSpec.rotation).withColor(elementSpec.color)
);
if (this.verbose) console.log(`Laser placed at (${lx},${ly}) with rotation ${directionNames[elementSpec.rotation]} and color ${elementSpec.color} (from queue spec)`);
} else {
if (this.verbose) console.log(`Laser placed at (${lx},${ly}) with rotation ${directionNames[ldir]} and color ${laserColor} (random)`);
}
this.puzzle = nextPuzzle.simulateBeams();
return { x: lx, y: ly, rotation: ldir, color: laserColor };
}
generate() {
const elementQueue = this.initialElementQueue;
const laserSpec = elementQueue[0];
const laserPlacement = this.placeLaser(laserSpec);
if (!laserPlacement) return false;
let paths = this.puzzle.simulateAndGetAllPaths();
if (paths.length === 0) return false;
this.placedElements = {};
this.placedElements[laserSpec.id] = { x: laserPlacement.x, y: laserPlacement.y, rotation: laserPlacement.rotation };
this.backtrackCounter = 0;
if (!this.verbose) {
console.log("Generating puzzle...");
const elementTree = this.buildElementQueueTree(this.initialElementQueue);
this.logElementQueueTree(elementTree);
} else {
const elementTree = this.buildElementQueueTree(this.initialElementQueue);
this.logElementQueueTree(elementTree);
}
const success = this.placeElementFromQueue(elementQueue.slice(1));
this.renderGridAscii(this.puzzle.grid); // Always render grid at the end of generate
return success;
}
buildElementQueueTree(elementQueue) {
const elementMap = {};
const rootNodes = [];
elementQueue.forEach(element => {
elementMap[element.id] = { element: element, children: [] };
});
elementQueue.forEach(element => {
if (element.dependency) {
const parentNode = elementMap[element.dependency];
if (parentNode) {
parentNode.children.push(elementMap[element.id]);
}
} else {
rootNodes.push(elementMap[element.id]);
}
});
return rootNodes;
}
logElementQueueTree(treeNodes, indent = '') {
if (this.verbose) {
if (!Array.isArray(treeNodes)) {
treeNodes = [treeNodes];
}
treeNodes.forEach(treeNode => {
const element = treeNode.element;
const line = `${indent}- ${element.type} (${element.id}) ${element.dependency ? `[dep: ${element.dependency}]` : ''}`;
console.log(line);
if (treeNode.children.length > 0) {
this.logElementQueueTree(treeNode.children, indent + ' ');
}
});
} else {
console.log("Element Queue: (see verbose log for details)");
}
}
placeElementFromQueue(elementQueue) {
if (elementQueue.length === 0) return true;
const currentElementSpec = elementQueue[0];
const remainingQueue = elementQueue.slice(1);
const elementType = currentElementSpec.type;
const elementId = currentElementSpec.id;
let validBranch = null; // To store a valid branch with free cells
const dependencyId = currentElementSpec.dependency;
if (dependencyId) {
const dependencyPlacement = this.placedElements[dependencyId];
if (dependencyPlacement) {
const dependencyX = dependencyPlacement.x;
const dependencyY = dependencyPlacement.y;
const nextPaths = this.puzzle.simulateAndGetAllPaths();
const pathsThroughDependency = nextPaths.filter(path =>
path.some(p => p.x === dependencyX && p.y === dependencyY)
);
for (const path of pathsThroughDependency) {
const branch = this.puzzle.getCandidateBranch(path, dependencyX, dependencyY);
if (branch.length > 0) {
// Check if the branch has at least one valid cell
const hasValidCell = branch.some(pt => {
return !this.puzzle.lockedCells.has(`${pt.x},${pt.y}`) && this.puzzle.grid[pt.y][pt.x].type === TILE_EMPTY;
});
if (hasValidCell) {
validBranch = branch; // Found a valid branch
break; // Exit loop after finding the first valid branch
}
}
}
if (!validBranch && this.verbose) console.log(`No valid branch found from dependency ${dependencyId} @(${dependencyX},${dependencyY}) with free cells!`);
} else {
console.error("Dependency placement not found!", dependencyId);
return false;
}
} else {
// For laser, any path is potentially valid at the start
const nextPaths = this.puzzle.simulateAndGetAllPaths();
if (nextPaths.length > 0) {
validBranch = nextPaths[0]; // Just take the first path for laser's initial branch.
} else {
return false; // No paths at all, something is wrong.
}
}
if (this.verbose) {
console.log(`\n=== Placing Element: ${elementType} (ID: ${elementId}, Parent: ${dependencyId}) ===`);
console.log(`Remaining Queue IDs:`, remainingQueue.map(e => e.id));
console.log("Current Branch (calculated and filtered for free cells):", validBranch ? validBranch.map(p => `(${p.x},${p.y}:${directionNames[p.dirIndex]})`).join(' → ') : 'No Valid Branch');
this.renderGridAscii(this.puzzle.grid, validBranch || []);
}
if (dependencyId && !this.placedElements[dependencyId]) {
if (this.verbose) console.log(`Dependency ${dependencyId} not yet placed. Skipping ${elementId} for now.`);
return false;
}
if (!validBranch || validBranch.length === 0) {
if (this.verbose) console.log(`No valid branch with free cells to place ${elementType} (ID: ${elementId}) on, backtracking.`);
return false;
}
let validMoves = this.getValidMovesForElement({ type: elementType }, validBranch);
shuffleArray(validMoves);
for (const move of validMoves) {
if (this.verbose) console.log(`Trying move: ${elementType} (ID: ${elementId}, Parent: ${dependencyId}) @(${move.x},${move.y}) ${move.directionInfo || ''}`);
let puzzleStateBeforeMove = this.puzzle.clone();
let nextPuzzle = this.applyMoveToPuzzle(puzzleStateBeforeMove, move, validBranch);
if (!nextPuzzle) continue;
if (!nextPuzzle.grid[move.y] || !nextPuzzle.grid[move.y][move.x]) {
console.error("Invalid grid access after applyMoveToPuzzle!", move.y, move.x, "Grid bounds:", ROWS, COLS, "Move:", move);
continue;
}
this.puzzle = nextPuzzle;
this.placedElements[elementId] = move;
if (remainingQueue.length === 0) {
if (this.verbose) console.log(`Queue fully placed! Success for ${elementType} (ID: ${elementId}, Parent: ${dependencyId}) @(${move.x},${move.y})`);
return true;
}
if (this.placeElementFromQueue(remainingQueue)) {
return true;
} else {
if (this.verbose) console.log(`Placing remaining queue failed after ${elementType} (ID: ${elementId}, Parent: ${dependencyId}) @(${move.x},${move.y}), backtracking.`);
this.puzzle = puzzleStateBeforeMove;
delete this.placedElements[elementId];
continue;
}
}
this.backtrackCounter++;
if (!this.verbose && this.backtrackCounter % 1000 === 0) {
console.log("Working... Backtracks:", this.backtrackCounter);
generator.renderGridAscii(this.puzzle.grid); // Added grid debug log here
}
if (this.verbose) console.log(`No valid moves found for ${elementType} (ID: ${elementId}), backtracking.`);
return false;
}
getValidMovesForElement(elementSpec, branch, allowedSplitterBranches = null) {
const puzzle = this.puzzle;
const strat = this.strategies[elementSpec.type];
if (!strat) throw new Error("No strategy for " + elementSpec.type);
return strat(branch, elementSpec, allowedSplitterBranches);
}
applyMoveToPuzzle(puzzle, move, branch) {
let nextPuzzle = puzzle.clone();
if (move.y < 0 || move.y >= ROWS || move.x < 0 || move.x >= COLS) {
console.error(`applyMoveToPuzzle: Invalid move coordinates! r=${move.y}, c=${move.x}`);
return null;
}
nextPuzzle = nextPuzzle.withUpdatedTile(move.y, move.x, (tile) => {
let updatedTile = null;
if (move.newState.type === TILE_LASER) {
updatedTile = tile.withType(TILE_LASER).withRotation(move.newState.rotation).withColor(move.newState.color);
} else if (move.newState.type === TILE_MIRROR) {
updatedTile = tile.withType(TILE_MIRROR).withRotation(move.newState.rotation);
} else if (move.newState.type === TILE_SPLITTER) {
updatedTile = tile.withType(TILE_SPLITTER).withSplitDirs(move.newState.splitDirs || []);
} else if (move.newState.type === TILE_RECEPTOR) {
updatedTile = tile.withType(TILE_RECEPTOR).withColor(this.puzzle.laserColor);
} else {
return tile;
}
return updatedTile;
});
if (!nextPuzzle.grid[move.y][move.x].type)
return null;
let idx = branch.findIndex(pt => pt.x === move.x && pt.y === move.y);
if (idx >= 0) {
let lockResult = nextPuzzle.lockCellsUpToIndex(branch, idx);
nextPuzzle = lockResult.updatedPuzzle;
}
return nextPuzzle;
}
// ---------------- Placement Strategies ----------------
placeMirror(branch, elementSpec, allowedSplitterBranches = null) {
let moves = [];
let candidateCellCoords = new Set();
for (const pathPoint of branch) {
const { x, y, dirIndex } = pathPoint;
if (y < 0 || y >= ROWS || x < 0 || x >= COLS) continue;
if (this.puzzle.lockedCells.has(`${x},${y}`)) continue;
if (this.puzzle.grid[y][x].type !== TILE_EMPTY) continue;
candidateCellCoords.add(`${x},${y}`);
}
let filteredCellCoords = new Set();
for (const cellCoord of candidateCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
let isOldLaserCell = false;
const cellKey = `${x},${y}`;
let firstIndex = -1;
for (let i = 0; i < branch.length; i++) {
if (branch[i].x === x && branch[i].y === y) {
firstIndex = i;
break;
}
}
if (firstIndex !== -1) {
for (let j = 0; j < firstIndex; j++) {
if (branch[j].x === x && branch[j].y === y) {
isOldLaserCell = true;
break;
}
}
}
if (!isOldLaserCell) {
filteredCellCoords.add(cellCoord);
}
}
for (const cellCoord of filteredCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
let incomingDir = -1;
for (const pathPoint of branch) {
if (pathPoint.x === x && pathPoint.y === y) {
const ptIndex = branch.indexOf(pathPoint);
if (ptIndex > 0) {
incomingDir = branch[ptIndex - 1].dirIndex;
if (incomingDir !== -1) break;
}
}
}
for (let rot = 0; rot < 8; rot++) {
if (incomingDir !== -1 && this.puzzle.mirrorReflect(incomingDir, rot) < 0) continue;
moves.push({
x, y,
newState: { type: TILE_MIRROR, rotation: rot },
directionInfo: `rotation ${directionNames[rot]}`
});
}
}
return moves;
}
placeSplitter(branch, elementSpec, allowedSplitterBranches = null) {
let moves = [];
let candidateCellCoords = new Set();
for (const pathPoint of branch) {
const { x, y, dirIndex } = pathPoint;
if (y < 0 || y >= ROWS || x < 0 || x >= COLS) continue;
if (this.puzzle.lockedCells.has(`${x},${y}`)) continue;
if (this.puzzle.grid[y][x].type !== TILE_EMPTY) continue;
candidateCellCoords.add(`${x},${y}`);
}
let filteredCellCoords = new Set();
for (const cellCoord of candidateCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
let isOldLaserCell = false;
const cellKey = `${x},${y}`;
let firstIndex = -1;
for (let i = 0; i < branch.length; i++) {
if (branch[i].x === x && branch[i].y === y) {
firstIndex = i;
break;
}
}
if (firstIndex !== -1) {
for (let j = 0; j < firstIndex; j++) {
if (branch[j].x === x && branch[j].y === y) {
isOldLaserCell = true;
break;
}
}
}
if (!isOldLaserCell) {
filteredCellCoords.add(cellCoord);
}
}
for (const cellCoord of filteredCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
let dirs = [...Array(8).keys()];
let subMoves = [];
for (let a = 0; a < dirs.length; a++) {
for (let b = a + 1; b < dirs.length; b++) {
subMoves.push({
x, y,
newState: { type: TILE_SPLITTER, splitDirs: [dirs[a], dirs[b]] },
directionInfo: `splitDirs [${directionNames[dirs[a]]}, ${directionNames[dirs[b]]}]`
});
}
}
return subMoves;
}
return moves;
}
placeReceptor(branch, elementSpec, allowedSplitterBranches = null) {
let moves = [];
let candidateCellCoords = new Set();
for (const pathPoint of branch) {
const { x, y, dirIndex } = pathPoint;
if (y < 0 || y >= ROWS || x < 0 || x >= COLS) continue;
if (this.puzzle.lockedCells.has(`${x},${y}`)) continue;
if (this.puzzle.grid[y][x].type !== TILE_EMPTY) continue;
candidateCellCoords.add(`${x},${y}`);
}
let filteredCellCoords = new Set();
for (const cellCoord of candidateCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
let isOldLaserCell = false;
const cellKey = `${x},${y}`;
let firstIndex = -1;
for (let i = 0; i < branch.length; i++) {
if (branch[i].x === x && branch[i].y === y) {
firstIndex = i;
break;
}
}
if (firstIndex !== -1) {
for (let j = 0; j < firstIndex; j++) {
if (branch[j].x === x && branch[j].y === y) {
isOldLaserCell = true;
break;
}
}
}
if (!isOldLaserCell) {
filteredCellCoords.add(cellCoord);
}
}
for (const cellCoord of filteredCellCoords) {
const [x, y] = cellCoord.split(",").map(Number);
moves.push({
x, y,
newState: { type: TILE_RECEPTOR }
});
}
return moves;
}
// ---------------- Debugging ----------------
renderGridAscii(grid, currentBranch = []) {
if (!DEBUG_MODE && !this.verbose) return; // Keep debug mode and verbose control
let gridStr = "";
const pathsCoords = new Set();
const allPaths = this.puzzle.simulateAndGetAllPaths();
for (const path of allPaths) {
for (const pt of path) {
pathsCoords.add(`${pt.x},${pt.y}`);
}
}
const currentBranchCoords = new Set(currentBranch.map(pt => `${pt.x},${pt.y}`));
let receptorCount = 0;
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
const tile = grid[r][c];
let char = '.';
const isPathCell = pathsCoords.has(`${c},${r}`);
const isCurrentBranchCell = currentBranchCoords.has(`${c},${r}`);
if (tile.type === TILE_LASER) char = 'L';
else if (tile.type === TILE_MIRROR) char = 'M';
else if (tile.type === TILE_SPLITTER) char = 'S';
else if (tile.type === TILE_RECEPTOR) {
char = 'R';
receptorCount++;
}
else if (this.puzzle.lockedCells.has(`${c},${r}`)) char = 'x';
else if (isCurrentBranchCell) char = '#';
else if (isPathCell) char = '*';
gridStr += char;
}
gridStr += "\n";
}
console.log("\n--- Grid ASCII (Receptor Count: " + receptorCount + ") ---"); // Always log grid
console.log(gridStr);
console.log("--- End Grid ASCII ---");
}
}
const levelsContainer = container.querySelector('#laserhack-levels');
const canvasEl = container.querySelector('#laserhack-canvas');
let rendererPuzzle = new PuzzleLogic(COLS, ROWS);
let renderer = new PuzzleRenderer(canvasEl, rendererPuzzle);
renderer.puzzle = rendererPuzzle;
let currentLevelIndex = 3;
let splitterState = {
active: false,
x: -1,
y: -1,
step: 0,
element: null
};
let generator = null;
const verboseLogging = false;
function nextLevel() {
currentLevelIndex++;
if (currentLevelIndex >= levels.length)
endCallback('success');
else
initLevel();
}
function initLevel() {
console.log(`=== INIT LEVEL ${currentLevelIndex + 1} ===`);
let elementQueue = elementQueueFactory(currentLevelIndex);
generator = new PuzzleGenerator(renderer.puzzle, elementQueue, verboseLogging);
const generationSuccess = generator.generate(); // Capture return value
if (generationSuccess) {
console.log("Puzzle generated successfully with backtracking!");
// generator.renderGridAscii(generator.puzzle.grid); // Moved to generate method
renderer.puzzle = generator.puzzle;
} else {
console.error("Puzzle generation failed after exhaustive backtracking.");
// generator.renderGridAscii(generator.puzzle.grid); // Moved to generate method, will run even on fail
}
updateLevelIndicator();
}
function updateLevelIndicator() {
levelsContainer.innerHTML = '';
for (let i = 0; i < maxLevels; i++) {
const block = document.createElement('div');
block.classList.add('level-block');
if (i <= currentLevelIndex) block.classList.add('active');
levelsContainer.appendChild(block);
}
}
function onClick(e) {
const rect = canvasEl.getBoundingClientRect();
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
const cx = Math.floor(mx / renderer.cellSize), cy = Math.floor(my / renderer.cellSize);
if (cx < 0 || cx >= COLS || cy >= ROWS || cy < 0) {
if (splitterState.active) {
splitterState.active = false;
if (splitterState.element) {
splitterState.element.classList.remove('splitter-selecting');
splitterState.element = null;
}
}
return;
}
let tile = renderer.puzzle.grid[cy][cx];
if (splitterState.active) {
if (cx === splitterState.x && cy === splitterState.y) {
splitterState.active = false;
if (splitterState.element) {
splitterState.element.classList.remove('splitter-selecting');
splitterState.element = null;
}
console.log("Splitter direction selection cancelled.");
return;
}
const dx = cx - splitterState.x, dy = cy - splitterState.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len > 0.01) {
let bestDir = -1, bestDot = -999;
const ndx = dx / len, ndy = dy / len;
for (let i = 0; i < 8; i++) {
const dv = directionVectors[i];
ctx.beginPath();
ctx.moveTo(px, py);
ctx.lineTo(px + dv.x * cs, py + dv.y * cs);
ctx.stroke();
}
}
return;
}
if (tile.type === TILE_LASER || tile.type === TILE_MIRROR) {
const newRotation = (tile.rotation + 1) % 8;
renderer.puzzle = renderer.puzzle.withUpdatedTile(cy, cx, (oldTile) => oldTile.withRotation(newRotation));
console.log(`${tile.type} @(${cx},${cy}) => rotation ${directionNames[newRotation]}`);
} else if (tile.type === TILE_RECEPTOR) {
if (DEBUG_MODE) {
console.log("DEBUG => nextLevel()");
nextLevel();
} else {
if (tile.lit) nextLevel();
}
} else if (tile.type === TILE_SPLITTER) {
splitterState.active = true;
splitterState.x = cx;
splitterState.y = cy;
splitterState.step = 0;
splitterState.element = canvasEl;
splitterState.element.classList.add('splitter-selecting');
tile.splitDirs = [];
console.log(`Clicked SPLITTER @(${cx},${cy}), pick 2 directions...`);
}
}
function onResize() {
renderer.resize();
}
function animate() {
renderer.puzzle = renderer.puzzle.simulateBeams();
renderer.draw(splitterState);
requestAnimationFrame(animate);
}
renderer.resize();
window.addEventListener('resize', onResize);
canvasEl.addEventListener('click', onClick);
initLevel();
animate();
return {
destroy() {
window.removeEventListener('resize', onResize);
canvasEl.removeEventListener('click', onClick);
if (splitterState.element) {
splitterState.element.classList.remove('splitter-selecting');
}
}
};
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment