Files
endfieldpuzzle/play.js
2026-03-13 17:58:49 +01:00

430 lines
14 KiB
JavaScript

(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 = '<table class="board-table">';
// Header row: empty corner + column constraints
html += "<tr>";
html += '<td></td>';
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 += `<td class="constraint-label${overClass}"${matchClass}>${filled}/${target}</td>`;
}
html += "</tr>";
// Board rows
for (let r = 0; r < boardHeight; r++) {
html += "<tr>";
// 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 += `<td class="constraint-label${overClass}"${matchClass}>${filled}/${target}</td>`;
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 += `<td class="${cls}" data-row="${r}" data-col="${c}"></td>`;
}
html += "</tr>";
}
html += "</table>";
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();
});
})();