initial commit

This commit is contained in:
2026-03-13 17:58:49 +01:00
commit 53818a69c2
7 changed files with 1009 additions and 0 deletions

3
README.md Normal file
View File

@@ -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

55
designer.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puzzle Designer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a href="index.html" class="nav-link">&rarr; Go to Play Page</a>
<h1>Puzzle Designer</h1>
<div class="page-layout">
<!-- Left panel: Board setup -->
<div class="panel">
<h2>Board Setup</h2>
<div class="section">
<label>Width: <input type="number" id="boardWidth" min="1" max="20" value="5"></label>
<label style="margin-left:12px;">Height: <input type="number" id="boardHeight" min="1" max="20" value="5"></label>
<button id="generateBtn" style="margin-left:12px;">Generate Board</button>
</div>
<div id="boardArea" class="section">
<!-- Board table will be generated here -->
</div>
</div>
<!-- Right panel: Piece designer + inventory -->
<div class="panel">
<h2>Piece Designer</h2>
<div class="section">
<div class="piece-designer-grid" id="pieceDesignerGrid">
<!-- 9 cells generated by JS -->
</div>
<br>
<button id="addPieceBtn">Add Piece</button>
</div>
<h2>Piece Inventory</h2>
<div class="inventory" id="pieceInventory">
<!-- Pieces added here -->
</div>
<div class="section" style="margin-top: 20px;">
<button id="exportBtn">Export Puzzle</button>
</div>
<div class="section">
<textarea id="exportOutput" rows="4" cols="40" readonly style="width:100%; display:none;"></textarea>
</div>
</div>
</div>
<script src="designer.js"></script>
</body>
</html>

200
designer.js Normal file
View File

@@ -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 = '<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();
})();

36
index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puzzle Player</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a href="designer.html" class="nav-link">&rarr; Go to Designer Page</a>
<h1>Puzzle Player</h1>
<div id="importSection" class="section" style="text-align:center; margin-bottom: 20px;">
<label>Paste puzzle code: </label>
<input type="text" id="importInput" style="width: 400px;" placeholder="Paste exported puzzle string here...">
<button id="importBtn" style="margin-left:8px;">Import</button>
</div>
<div id="gameArea" class="page-layout" style="display:none;">
<!-- Board -->
<div class="panel">
<h2>Board</h2>
<div id="playBoardArea"></div>
</div>
<!-- Inventory -->
<div class="panel play-inventory">
<h2>Pieces</h2>
<p style="font-size:12px; color:#888; margin-bottom:10px;">Drag pieces to the board. Press <kbd>R</kbd> while dragging to rotate.</p>
<div class="inventory" id="playInventory"></div>
</div>
</div>
<script src="play.js"></script>
</body>
</html>

429
play.js Normal file
View File

@@ -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 = '<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();
});
})();

21
prompt Normal file
View File

@@ -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.

265
styles.css Normal file
View File

@@ -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;
}