From 53818a69c2b6afaa299b22626c70ca46146a48f1 Mon Sep 17 00:00:00 2001 From: tukaunu Date: Fri, 13 Mar 2026 17:58:49 +0100 Subject: [PATCH] initial commit --- README.md | 3 + designer.html | 55 +++++++ designer.js | 200 +++++++++++++++++++++++ index.html | 36 +++++ play.js | 429 ++++++++++++++++++++++++++++++++++++++++++++++++++ prompt | 21 +++ styles.css | 265 +++++++++++++++++++++++++++++++ 7 files changed, 1009 insertions(+) create mode 100644 README.md create mode 100644 designer.html create mode 100644 designer.js create mode 100644 index.html create mode 100644 play.js create mode 100644 prompt create mode 100644 styles.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..15df1a5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +just an experiment to see how good claude opus 4.6 is. + +the prompt was entirely handwritten and took like 40 minutes holy f \ No newline at end of file diff --git a/designer.html b/designer.html new file mode 100644 index 0000000..c424db2 --- /dev/null +++ b/designer.html @@ -0,0 +1,55 @@ + + + + + + Puzzle Designer + + + + → Go to Play Page +

Puzzle Designer

+ +
+ +
+

Board Setup

+
+ + + +
+ +
+ +
+
+ + +
+

Piece Designer

+
+
+ +
+
+ +
+ +

Piece Inventory

+
+ +
+ +
+ +
+
+ +
+
+
+ + + + diff --git a/designer.js b/designer.js new file mode 100644 index 0000000..494bda5 --- /dev/null +++ b/designer.js @@ -0,0 +1,200 @@ +(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 = ''; + + // Header row: empty corner + column spinboxes + html += ""; + html += ''; // top-left corner + for (let c = 0; c < boardWidth; c++) { + html += '"; + } + html += ""; + + // Board rows + for (let r = 0; r < boardHeight; r++) { + html += ""; + // Row spinbox + html += '"; + for (let c = 0; c < boardWidth; c++) { + const blockedClass = blocked[r][c] ? " blocked" : ""; + html += ``; + } + html += ""; + } + + html += "
'; + html += ``; + html += "
'; + html += ``; + html += "
"; + 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(); +})(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..6b4749e --- /dev/null +++ b/index.html @@ -0,0 +1,36 @@ + + + + + + Puzzle Player + + + + → Go to Designer Page +

Puzzle Player

+ +
+ + + +
+ + + + + + diff --git a/play.js b/play.js new file mode 100644 index 0000000..d31a9a2 --- /dev/null +++ b/play.js @@ -0,0 +1,429 @@ +(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(); + }); + +})(); diff --git a/prompt b/prompt new file mode 100644 index 0000000..a02d98b --- /dev/null +++ b/prompt @@ -0,0 +1,21 @@ +I want to create a puzzle game using html, css and javascript. The idea is a combination of a nonogram and tetris, where you have a 2D grid board of variable size and some pieces you place on the board. For each row and column, there's a number describing how many cells should be filled in that row/column. To win, all the given pieces should be used and all the row/column constraints should be satisfied. Cells can also be blocked off, stopping pieces from being placed there. Of course, pieces can't overlap. + +My concrete idea is two have two separate parts to this: the puzzle designer page and the play page. For the designer page: + +In the puzzle designer page the user should be able to specify the width and height of the board. After pressing a button (or updating automatically based on text field events if they exist), a board with the corresponding dimensions should be shown. Directly above the board should be one spinbox for each column, where the designer can specify how many cells in each column should be filled. On the left there should be one spinbox for each row for the same functionality. The user should also be able to click on each cell to toggle whether it is blocked or not. You don't have to bother doing any error checking here, so e.g. a column that wants more than its height in cells is fine. + +Also in the puzzle designer page, the user should be able to create and add pieces to the "inventory" that the player then has access to. The user should be presented with a 3x3 grid that they can click to toggle cells, and some kind of "submit" button that adds the newly designed shape to the player's inventory. The user should obviously be able to delete shapes too. + +Once the user is done designing the puzzle, they should be able to press a button that says "export" that generates some kind of string that the player can then import. + +For the play page: + +The player should be presented with a text box that they can paste any exported game string/code into, and then be presented with the designed board and the pieces. For each column and row, there should be both the number of cells that need to be filled as well as the number of cells that are currently filled. For example if a row is empty and there should be 4 filled cells, it should say "0/4" at that row to the left of the board. If there are two cells filled, it should say "2/4". If the number of cells filled are more than the number that should be filled, the number should also turn red. + +The pieces should be coloured light green (I think there's a css colour called this?) and the inventory should be shown to the right of the board. The board should be dark grey and the cell borders should be light grey. The player should be able to "pick up" and drag the pieces to the board. While dragging, the piece should follow under the player's cursor as if they were actually dragging the piece, and (also while dragging) they should be able to press R on the keyboard to rotate the piece. Since each piece is defined by a 3x3 grid, the piece should rotate around the center of that 3x3 grid. The player should also be able to remove the piece from the board by dragging it away and placing it back into the inventory. This is where you may need to use external libraries/html canvas/whatever, as I don't think 100% vanilla html+javascript supports this? + +If the player tries to place a piece illegally (i.e. overlapping another piece, a blocked cell or the outside of the board) the piece should be immediately placed back in the inventory. When all the constraints are met (all of the row and column filled cell numbers match) a simple javascript popup should appear that says "Puzzle complete" with an OK button. + +Regarding coding style and libraries: + +Split up the html, css and javascript into their own files. Try to use libraries that you can include simply by linking to them in the html (though I don't do webdev for a living so I'm not sure what's available). I can try to download more libraries in the form of javascript files for you if you need. Don't use any languages other than html, css and javascript though, I want to be able to test this project by simply opening the html file in a browser. \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..185d94c --- /dev/null +++ b/styles.css @@ -0,0 +1,265 @@ +/* ===== Shared styles ===== */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: #1a1a2e; + color: #e0e0e0; + min-height: 100vh; + padding: 20px; +} + +h1 { + text-align: center; + margin-bottom: 20px; + color: #e0e0e0; + font-size: 1.8em; +} + +h2 { + margin-bottom: 10px; + color: #c0c0c0; + font-size: 1.2em; +} + +button { + background: #3a3a5c; + color: #e0e0e0; + border: 1px solid #555; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background 0.2s; +} + +button:hover { + background: #4a4a7c; +} + +input[type="number"] { + width: 48px; + height: 32px; + text-align: center; + background: #2a2a4a; + color: #e0e0e0; + border: 1px solid #555; + border-radius: 3px; + font-size: 14px; +} + +input[type="text"], textarea { + background: #2a2a4a; + color: #e0e0e0; + border: 1px solid #555; + border-radius: 3px; + padding: 8px; + font-size: 14px; +} + +/* ===== Board grid ===== */ +.board-container { + display: inline-block; +} + +.board-table { + border-collapse: collapse; +} + +.board-table td { + width: 40px; + height: 40px; + text-align: center; + vertical-align: middle; +} + +.board-cell { + background: #3a3a3a; + border: 1px solid #888; + cursor: pointer; + transition: background 0.15s; +} + +.board-cell.blocked { + background: #1a1a1a; + cursor: pointer; +} + +.board-cell.filled { + background: lightgreen; + border: 1px solid #888; +} + +/* ===== Constraint labels ===== */ +.constraint-label { + font-size: 14px; + font-weight: bold; + color: #ccc; + padding: 2px 6px; + min-width: 40px; + text-align: center; +} + +.constraint-label.over { + color: #ff4444; +} + +/* ===== Piece designer grid (3x3) ===== */ +.piece-designer-grid { + display: inline-grid; + grid-template-columns: repeat(3, 36px); + grid-template-rows: repeat(3, 36px); + gap: 2px; + margin: 10px 0; +} + +.piece-designer-cell { + width: 36px; + height: 36px; + background: #3a3a3a; + border: 1px solid #888; + cursor: pointer; + transition: background 0.15s; +} + +.piece-designer-cell.active { + background: lightgreen; +} + +/* ===== Piece inventory ===== */ +.inventory { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 10px; + min-height: 60px; + background: #222244; + border: 1px solid #444; + border-radius: 6px; +} + +.inventory-piece { + display: inline-grid; + grid-template-columns: repeat(3, 24px); + grid-template-rows: repeat(3, 24px); + gap: 1px; + padding: 4px; + background: #2a2a4a; + border: 1px solid #555; + border-radius: 4px; + cursor: grab; + position: relative; +} + +.inventory-piece:hover { + border-color: #88f; +} + +.inventory-piece-cell { + width: 24px; + height: 24px; + background: #3a3a3a; + border: 1px solid #555; +} + +.inventory-piece-cell.active { + background: lightgreen; +} + +.delete-piece-btn { + position: absolute; + top: -8px; + right: -8px; + width: 20px; + height: 20px; + background: #cc3333; + color: white; + border: none; + border-radius: 50%; + font-size: 12px; + line-height: 20px; + text-align: center; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.delete-piece-btn:hover { + background: #ff4444; +} + +/* ===== Drag ghost ===== */ +.drag-ghost { + position: fixed; + pointer-events: none; + z-index: 9999; + display: inline-grid; + grid-template-columns: repeat(3, 36px); + grid-template-rows: repeat(3, 36px); + gap: 1px; + opacity: 0.85; +} + +.drag-ghost-cell { + width: 36px; + height: 36px; +} + +.drag-ghost-cell.active { + background: lightgreen; + border: 1px solid #6a6; + border-radius: 2px; +} + +/* ===== Layout helpers ===== */ +.page-layout { + display: flex; + gap: 40px; + justify-content: center; + align-items: flex-start; + flex-wrap: wrap; +} + +.panel { + background: #16213e; + border: 1px solid #333; + border-radius: 8px; + padding: 20px; +} + +.section { + margin-bottom: 20px; +} + +.nav-link { + display: inline-block; + margin-bottom: 16px; + color: #88aaff; + text-decoration: none; +} + +.nav-link:hover { + text-decoration: underline; +} + +/* ===== Play page specific ===== */ +.play-inventory { + max-width: 300px; + min-width: 200px; +} + +.highlight-valid { + outline: 2px solid #4f4; + outline-offset: -1px; +} + +.highlight-invalid { + outline: 2px solid #f44; + outline-offset: -1px; +}