201 lines
7.1 KiB
JavaScript
201 lines
7.1 KiB
JavaScript
(function () {
|
||
"use strict";
|
||
|
||
// ===== State =====
|
||
let boardWidth = 5;
|
||
let boardHeight = 5;
|
||
// colConstraints[c] = number of cells that should be filled in column c
|
||
let colConstraints = [];
|
||
// rowConstraints[r] = number of cells that should be filled in row r
|
||
let rowConstraints = [];
|
||
// blocked[r][c] = true if that cell is blocked
|
||
let blocked = [];
|
||
// pieces: array of 3x3 boolean grids e.g. [[false,true,false],[true,true,true],[false,false,false]]
|
||
let pieces = [];
|
||
// piece designer state (3x3)
|
||
let pieceDesign = [
|
||
[false, false, false],
|
||
[false, false, false],
|
||
[false, false, false]
|
||
];
|
||
|
||
// ===== DOM refs =====
|
||
const boardWidthInput = document.getElementById("boardWidth");
|
||
const boardHeightInput = document.getElementById("boardHeight");
|
||
const generateBtn = document.getElementById("generateBtn");
|
||
const boardArea = document.getElementById("boardArea");
|
||
const pieceDesignerGrid = document.getElementById("pieceDesignerGrid");
|
||
const addPieceBtn = document.getElementById("addPieceBtn");
|
||
const pieceInventory = document.getElementById("pieceInventory");
|
||
const exportBtn = document.getElementById("exportBtn");
|
||
const exportOutput = document.getElementById("exportOutput");
|
||
|
||
// ===== Board generation =====
|
||
function initBoard() {
|
||
boardWidth = parseInt(boardWidthInput.value) || 5;
|
||
boardHeight = parseInt(boardHeightInput.value) || 5;
|
||
|
||
colConstraints = new Array(boardWidth).fill(0);
|
||
rowConstraints = new Array(boardHeight).fill(0);
|
||
blocked = [];
|
||
for (let r = 0; r < boardHeight; r++) {
|
||
blocked.push(new Array(boardWidth).fill(false));
|
||
}
|
||
renderBoard();
|
||
}
|
||
|
||
function renderBoard() {
|
||
// Build table: top-left empty | col spinboxes
|
||
// row spinbox | cells
|
||
let html = '<table class="board-table">';
|
||
|
||
// Header row: empty corner + column spinboxes
|
||
html += "<tr>";
|
||
html += '<td></td>'; // top-left corner
|
||
for (let c = 0; c < boardWidth; c++) {
|
||
html += '<td class="constraint-label">';
|
||
html += `<input type="number" min="0" class="col-constraint" data-col="${c}" value="${colConstraints[c]}">`;
|
||
html += "</td>";
|
||
}
|
||
html += "</tr>";
|
||
|
||
// Board rows
|
||
for (let r = 0; r < boardHeight; r++) {
|
||
html += "<tr>";
|
||
// Row spinbox
|
||
html += '<td class="constraint-label">';
|
||
html += `<input type="number" min="0" class="row-constraint" data-row="${r}" value="${rowConstraints[r]}">`;
|
||
html += "</td>";
|
||
for (let c = 0; c < boardWidth; c++) {
|
||
const blockedClass = blocked[r][c] ? " blocked" : "";
|
||
html += `<td class="board-cell${blockedClass}" data-row="${r}" data-col="${c}"></td>`;
|
||
}
|
||
html += "</tr>";
|
||
}
|
||
|
||
html += "</table>";
|
||
boardArea.innerHTML = html;
|
||
|
||
// Attach events
|
||
boardArea.querySelectorAll(".board-cell").forEach(cell => {
|
||
cell.addEventListener("click", function () {
|
||
const r = parseInt(this.dataset.row);
|
||
const c = parseInt(this.dataset.col);
|
||
blocked[r][c] = !blocked[r][c];
|
||
this.classList.toggle("blocked");
|
||
});
|
||
});
|
||
|
||
boardArea.querySelectorAll(".col-constraint").forEach(input => {
|
||
input.addEventListener("change", function () {
|
||
colConstraints[parseInt(this.dataset.col)] = parseInt(this.value) || 0;
|
||
});
|
||
});
|
||
|
||
boardArea.querySelectorAll(".row-constraint").forEach(input => {
|
||
input.addEventListener("change", function () {
|
||
rowConstraints[parseInt(this.dataset.row)] = parseInt(this.value) || 0;
|
||
});
|
||
});
|
||
}
|
||
|
||
// ===== Piece designer =====
|
||
function renderPieceDesigner() {
|
||
pieceDesignerGrid.innerHTML = "";
|
||
for (let r = 0; r < 3; r++) {
|
||
for (let c = 0; c < 3; c++) {
|
||
const cell = document.createElement("div");
|
||
cell.className = "piece-designer-cell" + (pieceDesign[r][c] ? " active" : "");
|
||
cell.dataset.row = r;
|
||
cell.dataset.col = c;
|
||
cell.addEventListener("click", function () {
|
||
const pr = parseInt(this.dataset.row);
|
||
const pc = parseInt(this.dataset.col);
|
||
pieceDesign[pr][pc] = !pieceDesign[pr][pc];
|
||
this.classList.toggle("active");
|
||
});
|
||
pieceDesignerGrid.appendChild(cell);
|
||
}
|
||
}
|
||
}
|
||
|
||
function addPiece() {
|
||
// Check that at least one cell is active
|
||
const hasCell = pieceDesign.some(row => row.some(v => v));
|
||
if (!hasCell) return;
|
||
|
||
// Deep-copy the design
|
||
const copy = pieceDesign.map(row => row.slice());
|
||
pieces.push(copy);
|
||
|
||
// Reset designer
|
||
pieceDesign = [
|
||
[false, false, false],
|
||
[false, false, false],
|
||
[false, false, false]
|
||
];
|
||
renderPieceDesigner();
|
||
renderInventory();
|
||
}
|
||
|
||
function renderInventory() {
|
||
pieceInventory.innerHTML = "";
|
||
pieces.forEach((piece, index) => {
|
||
const wrapper = document.createElement("div");
|
||
wrapper.className = "inventory-piece";
|
||
|
||
for (let r = 0; r < 3; r++) {
|
||
for (let c = 0; c < 3; c++) {
|
||
const cell = document.createElement("div");
|
||
cell.className = "inventory-piece-cell" + (piece[r][c] ? " active" : "");
|
||
wrapper.appendChild(cell);
|
||
}
|
||
}
|
||
|
||
// Delete button
|
||
const delBtn = document.createElement("button");
|
||
delBtn.className = "delete-piece-btn";
|
||
delBtn.textContent = "×";
|
||
delBtn.title = "Delete piece";
|
||
delBtn.addEventListener("click", function (e) {
|
||
e.stopPropagation();
|
||
pieces.splice(index, 1);
|
||
renderInventory();
|
||
});
|
||
wrapper.appendChild(delBtn);
|
||
|
||
pieceInventory.appendChild(wrapper);
|
||
});
|
||
}
|
||
|
||
// ===== Export =====
|
||
function exportPuzzle() {
|
||
const data = {
|
||
width: boardWidth,
|
||
height: boardHeight,
|
||
colConstraints: colConstraints,
|
||
rowConstraints: rowConstraints,
|
||
blocked: blocked,
|
||
pieces: pieces
|
||
};
|
||
const json = JSON.stringify(data);
|
||
const encoded = btoa(json);
|
||
exportOutput.style.display = "block";
|
||
exportOutput.value = encoded;
|
||
exportOutput.select();
|
||
}
|
||
|
||
// ===== Event bindings =====
|
||
generateBtn.addEventListener("click", initBoard);
|
||
addPieceBtn.addEventListener("click", addPiece);
|
||
exportBtn.addEventListener("click", exportPuzzle);
|
||
|
||
// Also auto-update board on input change
|
||
boardWidthInput.addEventListener("change", initBoard);
|
||
boardHeightInput.addEventListener("change", initBoard);
|
||
|
||
// ===== Init =====
|
||
initBoard();
|
||
renderPieceDesigner();
|
||
})();
|