(function () { "use strict"; // ===== Puzzle state ===== let boardWidth = 0; let boardHeight = 0; let colConstraints = []; let rowConstraints = []; let blocked = []; // blocked[r][c] = true let boardState = []; // boardState[r][c] = pieceIndex or -1 // Each piece in inventory: { shape: 3x3 bool, placed: bool, boardRow: int, boardCol: int, rotation: int } let pieces = []; // ===== Drag state ===== let dragging = false; let dragPieceIndex = -1; let dragShape = null; // current (possibly rotated) 3x3 bool grid let dragGhostEl = null; // the floating DOM element let dragOffsetX = 0; // offset from mouse to ghost top-left let dragOffsetY = 0; // ===== DOM refs ===== const importInput = document.getElementById("importInput"); const importBtn = document.getElementById("importBtn"); const gameArea = document.getElementById("gameArea"); const playBoardArea = document.getElementById("playBoardArea"); const playInventory = document.getElementById("playInventory"); // ===== Import ===== function importPuzzle() { const encoded = importInput.value.trim(); if (!encoded) return; let data; try { data = JSON.parse(atob(encoded)); } catch (e) { alert("Invalid puzzle code."); return; } boardWidth = data.width; boardHeight = data.height; colConstraints = data.colConstraints; rowConstraints = data.rowConstraints; blocked = data.blocked; // Init board state boardState = []; for (let r = 0; r < boardHeight; r++) { boardState.push(new Array(boardWidth).fill(-1)); } // Init pieces pieces = data.pieces.map(shape => ({ shape: shape, // original 3x3 currentShape: shape.map(row => row.slice()), // working copy (rotated when placed) placed: false, boardRow: -1, boardCol: -1 })); gameArea.style.display = "flex"; renderBoard(); renderInventory(); } // ===== Rotation ===== // Rotate a 3x3 grid 90 degrees clockwise function rotateShape(shape) { const result = [ [false, false, false], [false, false, false], [false, false, false] ]; for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { result[c][2 - r] = shape[r][c]; } } return result; } // ===== Board rendering ===== function renderBoard() { let html = ''; // Header row: empty corner + column constraints html += ""; html += ''; for (let c = 0; c < boardWidth; c++) { const filled = countColFilled(c); const target = colConstraints[c]; const overClass = filled > target ? " over" : ""; const matchClass = filled === target && target > 0 ? ' style="color:#4f4;"' : ""; html += ``; } html += ""; // Board rows for (let r = 0; r < boardHeight; r++) { html += ""; // Row constraint const filled = countRowFilled(r); const target = rowConstraints[r]; const overClass = filled > target ? " over" : ""; const matchClass = filled === target && target > 0 ? ' style="color:#4f4;"' : ""; html += ``; for (let c = 0; c < boardWidth; c++) { let cls = "board-cell"; if (blocked[r][c]) { cls += " blocked"; } else if (boardState[r][c] >= 0) { cls += " filled"; } html += ``; } html += ""; } html += "
${filled}/${target}
${filled}/${target}
"; playBoardArea.innerHTML = html; } function countRowFilled(r) { let count = 0; for (let c = 0; c < boardWidth; c++) { if (boardState[r][c] >= 0) count++; } return count; } function countColFilled(c) { let count = 0; for (let r = 0; r < boardHeight; r++) { if (boardState[r][c] >= 0) count++; } return count; } // ===== Inventory rendering ===== function renderInventory() { playInventory.innerHTML = ""; pieces.forEach((piece, index) => { if (piece.placed) return; // don't show placed pieces in inventory const wrapper = document.createElement("div"); wrapper.className = "inventory-piece"; wrapper.dataset.pieceIndex = index; const shape = piece.shape; // always show original orientation in inventory for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { const cell = document.createElement("div"); cell.className = "inventory-piece-cell" + (shape[r][c] ? " active" : ""); wrapper.appendChild(cell); } } // Start drag on mousedown wrapper.addEventListener("mousedown", function (e) { e.preventDefault(); startDrag(index, piece.shape.map(row => row.slice()), e.clientX, e.clientY, wrapper); }); playInventory.appendChild(wrapper); }); } // ===== Drag & Drop ===== function startDrag(pieceIndex, shape, mouseX, mouseY, sourceEl) { dragging = true; dragPieceIndex = pieceIndex; dragShape = shape.map(row => row.slice()); // If the piece was on the board, remove it if (pieces[pieceIndex].placed) { removePieceFromBoard(pieceIndex); pieces[pieceIndex].placed = false; renderBoard(); renderInventory(); } // Create ghost element createDragGhost(mouseX, mouseY); } function createDragGhost(mouseX, mouseY) { // Remove existing ghost if (dragGhostEl) { dragGhostEl.remove(); dragGhostEl = null; } const ghost = document.createElement("div"); ghost.className = "drag-ghost"; for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { const cell = document.createElement("div"); cell.className = "drag-ghost-cell" + (dragShape[r][c] ? " active" : ""); ghost.appendChild(cell); } } document.body.appendChild(ghost); dragGhostEl = ghost; // Compute offset: center the ghost grid on the cursor at the center cell (1,1) // Each cell is 36px + 1px gap; total grid ~ 3*36 + 2*1 = 110px const ghostSize = 3 * 36 + 2 * 1; dragOffsetX = ghostSize / 2; dragOffsetY = ghostSize / 2; positionGhost(mouseX, mouseY); } function positionGhost(mouseX, mouseY) { if (!dragGhostEl) return; dragGhostEl.style.left = (mouseX - dragOffsetX) + "px"; dragGhostEl.style.top = (mouseY - dragOffsetY) + "px"; } function endDrag(mouseX, mouseY) { if (!dragging) return; dragging = false; // Try to find which board cell is under the center of the ghost const boardCell = getCellUnderPoint(mouseX, mouseY); let placed = false; if (boardCell) { // The ghost center corresponds to the center cell (1,1) of the 3x3 shape. // So the top-left of the shape maps to (boardCell.row - 1, boardCell.col - 1). const topRow = boardCell.row - 1; const topCol = boardCell.col - 1; if (canPlace(dragShape, topRow, topCol, dragPieceIndex)) { placePiece(dragPieceIndex, dragShape, topRow, topCol); placed = true; } } if (!placed) { // Return to inventory pieces[dragPieceIndex].placed = false; } // Clean up ghost if (dragGhostEl) { dragGhostEl.remove(); dragGhostEl = null; } renderBoard(); renderInventory(); // Check win if (placed) { checkWin(); } } function getCellUnderPoint(x, y) { // Use document.elementFromPoint - temporarily hide ghost if (dragGhostEl) dragGhostEl.style.display = "none"; const el = document.elementFromPoint(x, y); if (dragGhostEl) dragGhostEl.style.display = ""; if (el && el.classList.contains("board-cell")) { return { row: parseInt(el.dataset.row), col: parseInt(el.dataset.col) }; } return null; } function canPlace(shape, topRow, topCol, pieceIndex) { for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { if (!shape[r][c]) continue; const br = topRow + r; const bc = topCol + c; // Out of bounds if (br < 0 || br >= boardHeight || bc < 0 || bc >= boardWidth) return false; // Blocked if (blocked[br][bc]) return false; // Occupied by another piece if (boardState[br][bc] >= 0 && boardState[br][bc] !== pieceIndex) return false; } } return true; } function placePiece(pieceIndex, shape, topRow, topCol) { pieces[pieceIndex].placed = true; pieces[pieceIndex].boardRow = topRow; pieces[pieceIndex].boardCol = topCol; pieces[pieceIndex].currentShape = shape.map(row => row.slice()); for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { if (!shape[r][c]) continue; boardState[topRow + r][topCol + c] = pieceIndex; } } } function removePieceFromBoard(pieceIndex) { for (let r = 0; r < boardHeight; r++) { for (let c = 0; c < boardWidth; c++) { if (boardState[r][c] === pieceIndex) { boardState[r][c] = -1; } } } } // Allow picking up pieces from the board function handleBoardMouseDown(e) { if (dragging) return; const target = e.target; if (!target.classList.contains("board-cell")) return; if (!target.classList.contains("filled")) return; const r = parseInt(target.dataset.row); const c = parseInt(target.dataset.col); const pieceIndex = boardState[r][c]; if (pieceIndex < 0) return; e.preventDefault(); const piece = pieces[pieceIndex]; const shape = piece.currentShape.map(row => row.slice()); removePieceFromBoard(pieceIndex); piece.placed = false; startDrag(pieceIndex, shape, e.clientX, e.clientY, target); renderBoard(); } // ===== Highlight preview while dragging ===== function updateHighlight(mouseX, mouseY) { // Remove old highlights playBoardArea.querySelectorAll(".highlight-valid, .highlight-invalid").forEach(el => { el.classList.remove("highlight-valid", "highlight-invalid"); }); if (!dragging) return; const boardCell = getCellUnderPoint(mouseX, mouseY); if (!boardCell) return; const topRow = boardCell.row - 1; const topCol = boardCell.col - 1; const valid = canPlace(dragShape, topRow, topCol, dragPieceIndex); for (let r = 0; r < 3; r++) { for (let c = 0; c < 3; c++) { if (!dragShape[r][c]) continue; const br = topRow + r; const bc = topCol + c; if (br < 0 || br >= boardHeight || bc < 0 || bc >= boardWidth) continue; const cellEl = playBoardArea.querySelector(`[data-row="${br}"][data-col="${bc}"]`); if (cellEl) { cellEl.classList.add(valid ? "highlight-valid" : "highlight-invalid"); } } } } // ===== Win check ===== function checkWin() { // All pieces must be placed const allPlaced = pieces.every(p => p.placed); if (!allPlaced) return; // All row constraints must match for (let r = 0; r < boardHeight; r++) { if (countRowFilled(r) !== rowConstraints[r]) return; } // All column constraints must match for (let c = 0; c < boardWidth; c++) { if (countColFilled(c) !== colConstraints[c]) return; } // WIN! setTimeout(function () { alert("Puzzle complete!"); }, 100); } // ===== Global event listeners ===== document.addEventListener("mousemove", function (e) { if (!dragging) return; positionGhost(e.clientX, e.clientY); updateHighlight(e.clientX, e.clientY); }); document.addEventListener("mouseup", function (e) { if (!dragging) return; endDrag(e.clientX, e.clientY); }); document.addEventListener("keydown", function (e) { if (!dragging) return; if (e.key === "r" || e.key === "R") { dragShape = rotateShape(dragShape); // Rebuild ghost if (dragGhostEl) { const rect = dragGhostEl.getBoundingClientRect(); const cx = rect.left + rect.width / 2; const cy = rect.top + rect.height / 2; createDragGhost(cx, cy); } } }); // Board click to pick up pieces playBoardArea.addEventListener("mousedown", handleBoardMouseDown); // Import importBtn.addEventListener("click", importPuzzle); importInput.addEventListener("keydown", function (e) { if (e.key === "Enter") importPuzzle(); }); })();