We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Submission 2026-03-26 21:19
Approvedby ElFantasma on Mar 26, 2026 21:19 · v1
Reviewed by ElFantasma on Mar 26, 2026 21:19
Changes
diff --git a/games/quarto/about.md b/games/quarto/about.md new file mode 100644 index 0000000..95d01b5 --- /dev/null +++ b/games/quarto/about.md @@ -0,0 +1,28 @@ +# Quarto + +A deceptively simple strategy game with a unique twist: your opponent chooses which piece you must play. + +## Rules + +### Pieces +There are 16 unique pieces, each with 4 binary attributes: +- **Tall** or **Short** +- **Light** or **Dark** +- **Round** or **Square** +- **Hollow** or **Solid** + +Every combination exists exactly once. + +### Turns +Each turn has two phases: + +1. **Place** the piece your opponent selected for you on any empty cell +2. **Select** a piece from the remaining pool for your opponent to place next + +On the very first turn, the first player only selects a piece (nothing to place yet). + +### Winning +Get **four pieces in a row** (horizontal, vertical, or diagonal) that share **at least one common attribute**. For example: four tall pieces, or four round pieces, regardless of their other traits. + +### Draw +If all 16 pieces are placed with no four-in-a-row sharing an attribute, the game is a draw. diff --git a/games/quarto/game.json b/games/quarto/game.json new file mode 100644 index 0000000..3cd65c6 --- /dev/null +++ b/games/quarto/game.json @@ -0,0 +1,6 @@ +{ + "title": "Quarto", + "description": "16 unique pieces, 4 binary traits. Your opponent picks which piece you must place. Get four in a row sharing any trait to win.", + "min_players": 2, + "max_players": 2 +} diff --git a/games/quarto/logic.js b/games/quarto/logic.js new file mode 100644 index 0000000..77b8518 --- /dev/null +++ b/games/quarto/logic.js @@ -0,0 +1,131 @@ +function getPlayers() { + return ["Gold", "Silver"]; +} +function newGame(_params) { + return ser({ + b: "..../..../..../....", + t: "Gold", + ph: "s", + sp: null, + av: "0123456789ABCDEF", + w: null + }); +} +function getActivePlayers(json) { + const s = parse(json); + return s.w ? [] : [s.t]; +} +function applyMove(json, moveJson, player) { + const s = parse(json); + const m = JSON.parse(moveJson); + if (s.w) + throw new Error("Game over"); + if (s.t !== player) + throw new Error("Not your turn"); + if (s.ph === "p") { + return doPlace(s, m.row, m.col, player); + } else { + return doSelect(s, m.piece, player); + } +} +function getPlayerState(json, _player) { + return json; +} +function getWinner(json) { + return parse(json).w; +} +function getValidMoves(json, player) { + const s = parse(json); + if (s.w || s.t !== player) + return []; + if (s.ph === "p") { + const cells = boardCells(s.b); + const moves = []; + for (let r = 0; r < 4; r++) + for (let c = 0; c < 4; c++) + if (cells[r][c] === ".") + moves.push(JSON.stringify({ row: r, col: c })); + return moves; + } else { + return [...s.av].map((p) => JSON.stringify({ piece: p })); + } +} +function parse(json) { + return JSON.parse(json); +} +function ser(s) { + return JSON.stringify(s); +} +function opp(p) { + return p === "Gold" ? "Silver" : "Gold"; +} +function boardCells(b) { + return b.split("/").map((row) => [...row]); +} +function cellsToBoard(cells) { + return cells.map((row) => row.join("")).join("/"); +} +function doPlace(s, row, col, player) { + if (row < 0 || row > 3 || col < 0 || col > 3) + throw new Error("Out of bounds"); + if (!s.sp) + throw new Error("No piece to place"); + const cells = boardCells(s.b); + if (cells[row][col] !== ".") + throw new Error("Cell occupied"); + cells[row][col] = s.sp; + s.b = cellsToBoard(cells); + s.sp = null; + if (checkWin(cells)) { + s.w = player; + return ser(s); + } + if (s.av.length === 0) { + s.w = "draw"; + return ser(s); + } + s.ph = "s"; + return ser(s); +} +function doSelect(s, piece, player) { + if (!piece || piece.length !== 1) + throw new Error("Invalid piece"); + const p = piece.toUpperCase(); + if (!s.av.includes(p)) + throw new Error("Piece not available"); + s.av = s.av.replace(p, ""); + s.sp = p; + s.t = opp(player); + s.ph = "p"; + return ser(s); +} +function checkWin(cells) { + const lines = []; + for (let r = 0; r < 4; r++) + lines.push([[r, 0], [r, 1], [r, 2], [r, 3]]); + for (let c = 0; c < 4; c++) + lines.push([[0, c], [1, c], [2, c], [3, c]]); + lines.push([[0, 0], [1, 1], [2, 2], [3, 3]]); + lines.push([[0, 3], [1, 2], [2, 1], [3, 0]]); + for (const line of lines) { + const pieces = line.map(([r, c]) => cells[r][c]); + if (pieces.some((p) => p === ".")) + continue; + const vals = pieces.map((p) => parseInt(p, 16)); + for (let bit = 0; bit < 4; bit++) { + const bits = vals.map((v) => v >> bit & 1); + if (bits.every((b) => b === 1) || bits.every((b) => b === 0)) + return true; + } + } + return false; +} +export { + applyMove, + getActivePlayers, + getPlayerState, + getPlayers, + getValidMoves, + getWinner, + newGame +}; diff --git a/games/quarto/logic.ts b/games/quarto/logic.ts new file mode 100644 index 0000000..18df4ae --- /dev/null +++ b/games/quarto/logic.ts @@ -0,0 +1,153 @@ +// Quarto — Game Logic +// +// 4×4 board, 16 unique pieces (hex 0-F), each with 4 binary traits: +// bit 3: Tall(1)/Short(0) +// bit 2: Light(1)/Dark(0) +// bit 1: Round(1)/Square(0) +// bit 0: Hollow(1)/Solid(0) +// +// Board: "..../..../..../....", rows separated by / +// State: { b, t, ph, sp, av, w } + +interface State { + b: string; // board: 4 rows of 4 chars, "/" separated + t: string; // turn: "Gold" | "Silver" + ph: string; // phase: "s" (select) | "p" (place) + sp: string | null; // selected piece to be placed + av: string; // available pieces (hex chars) + w: string | null; // winner +} + +// --- SDK exports --- + +export function getPlayers(): string[] { return ["Gold", "Silver"]; } + +export function newGame(_params: string): string { + return ser({ + b: "..../..../..../....", + t: "Gold", + ph: "s", + sp: null, + av: "0123456789ABCDEF", + w: null, + }); +} + +export function getActivePlayers(json: string): string[] { + const s = parse(json); + return s.w ? [] : [s.t]; +} + +export function applyMove(json: string, moveJson: string, player: string): string { + const s = parse(json); + const m = JSON.parse(moveJson); + if (s.w) throw new Error("Game over"); + if (s.t !== player) throw new Error("Not your turn"); + + if (s.ph === "p") { + return doPlace(s, m.row, m.col, player); + } else { + return doSelect(s, m.piece, player); + } +} + +export function getPlayerState(json: string, _player: string | null): string { return json; } + +export function getWinner(json: string): string | null { return parse(json).w; } + +export function getValidMoves(json: string, player: string): string[] { + const s = parse(json); + if (s.w || s.t !== player) return []; + + if (s.ph === "p") { + const cells = boardCells(s.b); + const moves: string[] = []; + for (let r = 0; r < 4; r++) + for (let c = 0; c < 4; c++) + if (cells[r][c] === ".") moves.push(JSON.stringify({ row: r, col: c })); + return moves; + } else { + return [...s.av].map(p => JSON.stringify({ piece: p })); + } +} + +// --- Internal --- + +function parse(json: string): State { return JSON.parse(json); } +function ser(s: State): string { return JSON.stringify(s); } +function opp(p: string): string { return p === "Gold" ? "Silver" : "Gold"; } + +function boardCells(b: string): string[][] { + return b.split("/").map(row => [...row]); +} + +function cellsToBoard(cells: string[][]): string { + return cells.map(row => row.join("")).join("/"); +} + +function doPlace(s: State, row: number, col: number, player: string): string { + if (row < 0 || row > 3 || col < 0 || col > 3) throw new Error("Out of bounds"); + if (!s.sp) throw new Error("No piece to place"); + + const cells = boardCells(s.b); + if (cells[row][col] !== ".") throw new Error("Cell occupied"); + + cells[row][col] = s.sp; + s.b = cellsToBoard(cells); + s.sp = null; + + // Check win + if (checkWin(cells)) { + s.w = player; + return ser(s); + } + + // Check draw (no pieces left and no win) + if (s.av.length === 0) { + s.w = "draw"; + return ser(s); + } + + // Same player now selects a piece for opponent + s.ph = "s"; + return ser(s); +} + +function doSelect(s: State, piece: string, player: string): string { + if (!piece || piece.length !== 1) throw new Error("Invalid piece"); + const p = piece.toUpperCase(); + if (!s.av.includes(p)) throw new Error("Piece not available"); + + s.av = s.av.replace(p, ""); + s.sp = p; + s.t = opp(player); + s.ph = "p"; + return ser(s); +} + +function checkWin(cells: string[][]): boolean { + const lines: [number, number][][] = []; + + // Rows + for (let r = 0; r < 4; r++) lines.push([[r,0],[r,1],[r,2],[r,3]]); + // Cols + for (let c = 0; c < 4; c++) lines.push([[0,c],[1,c],[2,c],[3,c]]); + // Diagonals + lines.push([[0,0],[1,1],[2,2],[3,3]]); + lines.push([[0,3],[1,2],[2,1],[3,0]]); + + for (const line of lines) { + const pieces = line.map(([r, c]) => cells[r][c]); + if (pieces.some(p => p === ".")) continue; + + const vals = pieces.map(p => parseInt(p, 16)); + + // Check each bit: if all share the same value for any bit, it's a win + for (let bit = 0; bit < 4; bit++) { + const bits = vals.map(v => (v >> bit) & 1); + if (bits.every(b => b === 1) || bits.every(b => b === 0)) return true; + } + } + + return false; +} diff --git a/games/quarto/params.json b/games/quarto/params.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/games/quarto/params.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/games/quarto/ui/default/main.js b/games/quarto/ui/default/main.js new file mode 100644 index 0000000..0d3ffcf --- /dev/null +++ b/games/quarto/ui/default/main.js @@ -0,0 +1,169 @@ +let inst = null; +function mount(el, init) { + if (inst) + destroy(); + inst = { el, cb: null, state: null, seat: null }; + el.innerHTML = `<div class="qt-root"> + <div class="qt-info"></div> + <div class="qt-board"></div> + <div class="qt-selected"></div> + <div class="qt-pool"></div> + </div>`; + if (init) + update(init); +} +function update(json) { + if (!inst) + return; + try { + inst.state = JSON.parse(json); + } catch { + return; + } + render(); +} +function setSeat(n) { + if (!inst) + return; + inst.seat = n; + if (inst.state) + render(); +} +function onMove(cb) { + if (inst) + inst.cb = cb; +} +function destroy() { + inst = null; +} +function setStatus(st) { + if (!inst) + return; + const info = inst.el.querySelector(".qt-info"); + if (info && st === "waiting") + info.textContent = "Waiting for players\u2026"; +} +function setResult(r) { + if (!inst) + return; + const info = inst.el.querySelector(".qt-info"); + if (!info) + return; + if (r === "draw") { + info.textContent = "Draw!"; + } else { + info.textContent = `${LBL[r] || r} wins!`; + } +} +const LBL = { Gold: "Gold", Silver: "Silver" }; +function me() { + return inst?.seat ? inst.seat === 1 ? "Gold" : "Silver" : null; +} +function render() { + if (!inst?.state) + return; + const s = inst.state; + const my = me(); + const myTurn = my === s.t && !s.w; + const board = inst.el.querySelector(".qt-board"); + const cells = s.b.split("/"); + let bh = '<div class="qt-grid">'; + for (let r = 0; r < 4; r++) { + for (let c = 0; c < 4; c++) { + const p = cells[r][c]; + const clickable = myTurn && s.ph === "p" && p === "."; + bh += `<div class="qt-cell${clickable ? " qt-clickable" : ""}" data-r="${r}" data-c="${c}">`; + if (p !== ".") + bh += pieceSvg(p, "qt-piece-lg"); + bh += `</div>`; + } + } + bh += "</div>"; + board.innerHTML = bh; + board.querySelectorAll(".qt-clickable").forEach((el) => { + el.addEventListener("click", () => { + const r = parseInt(el.getAttribute("data-r")); + const c = parseInt(el.getAttribute("data-c")); + inst?.cb?.(JSON.stringify({ row: r, col: c })); + }); + }); + const selDiv = inst.el.querySelector(".qt-selected"); + if (s.sp && s.ph === "p") { + const who = myTurn ? "You must place:" : `${LBL[s.t]} places:`; + selDiv.innerHTML = `<span class="qt-sel-label">${who}</span>${pieceSvg(s.sp, "qt-piece-sel")}`; + } else { + selDiv.innerHTML = ""; + } + const pool = inst.el.querySelector(".qt-pool"); + const pickable = myTurn && s.ph === "s"; + let ph = ""; + if (s.av.length > 0) { + ph += `<div class="qt-pool-label">${pickable ? "Pick a piece for your opponent:" : "Available pieces:"}</div>`; + ph += '<div class="qt-pool-grid">'; + for (const p of s.av) { + ph += `<div class="qt-pool-item${pickable ? " qt-pickable" : ""}" data-piece="${p}">${pieceSvg(p, "qt-piece-sm")}</div>`; + } + ph += "</div>"; + } + pool.innerHTML = ph; + pool.querySelectorAll(".qt-pickable").forEach((el) => { + el.addEventListener("click", () => { + const p = el.getAttribute("data-piece"); + inst?.cb?.(JSON.stringify({ piece: p })); + }); + }); + const info = inst.el.querySelector(".qt-info"); + if (s.w) { + info.textContent = s.w === "draw" ? "Draw!" : `${LBL[s.w]} wins!`; + info.className = "qt-info qt-result"; + } else if (my === null) { + const act = s.ph === "s" ? "selects" : "places"; + info.textContent = `${LBL[s.t]} ${act}`; + info.className = "qt-info"; + } else if (myTurn) { + info.textContent = s.ph === "p" ? "Place the piece on the board" : "Pick a piece for your opponent"; + info.className = "qt-info qt-myturn"; + } else { + info.textContent = `${LBL[s.t]}'s turn`; + info.className = "qt-info"; + } +} +function pieceSvg(hex, cls) { + const v = parseInt(hex, 16); + const tall = !!(v & 8); + const light = !!(v & 4); + const round = !!(v & 2); + const hollow = !!(v & 1); + const fill = light ? "#f5c542" : "#5b3e87"; + const stroke = light ? "#d4a017" : "#3d2560"; + const h = tall ? 40 : 26; + const y = tall ? 2 : 16; + const w = 30; + let shape; + if (round) { + const rx = w / 2, ry = h / 2; + const cx = 21, cy = y + h / 2; + if (hollow) { + shape = `<ellipse cx="${cx}" cy="${cy}" rx="${rx - 2}" ry="${ry - 2}" fill="none" stroke="${fill}" stroke-width="4"/>`; + } else { + shape = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>`; + } + } else { + const x = 21 - w / 2; + if (hollow) { + shape = `<rect x="${x + 2}" y="${y + 2}" width="${w - 4}" height="${h - 4}" rx="2" fill="none" stroke="${fill}" stroke-width="4"/>`; + } else { + shape = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>`; + } + } + return `<svg class="${cls}" viewBox="0 0 42 44">${shape}</svg>`; +} +export { + destroy, + mount, + onMove, + setResult, + setSeat, + setStatus, + update +}; diff --git a/games/quarto/ui/default/main.ts b/games/quarto/ui/default/main.ts new file mode 100644 index 0000000..8c5047b --- /dev/null +++ b/games/quarto/ui/default/main.ts @@ -0,0 +1,182 @@ +// Quarto — UI Module +// +// Piece encoding (hex 0-F): +// bit 3: Tall(1)/Short(0) +// bit 2: Light(1)/Dark(0) +// bit 1: Round(1)/Square(0) +// bit 0: Hollow(1)/Solid(0) + +interface State { + b: string; t: string; ph: string; + sp: string | null; av: string; w: string | null; +} + +let inst: { + el: HTMLElement; cb: ((m: string) => void) | null; + state: State | null; seat: number | null; +} | null = null; + +// --- SDK exports --- + +export function mount(el: HTMLElement, init?: string) { + if (inst) destroy(); + inst = { el, cb: null, state: null, seat: null }; + el.innerHTML = `<div class="qt-root"> + <div class="qt-info"></div> + <div class="qt-board"></div> + <div class="qt-selected"></div> + <div class="qt-pool"></div> + </div>`; + if (init) update(init); +} + +export function update(json: string) { + if (!inst) return; + try { inst.state = JSON.parse(json); } catch { return; } + render(); +} + +export function setSeat(n: number) { + if (!inst) return; + inst.seat = n; + if (inst.state) render(); +} + +export function onMove(cb: (m: string) => void) { if (inst) inst.cb = cb; } +export function destroy() { inst = null; } + +export function setStatus(st: string) { + if (!inst) return; + const info = inst.el.querySelector(".qt-info"); + if (info && st === "waiting") info.textContent = "Waiting for players\u2026"; +} + +export function setResult(r: string) { + if (!inst) return; + const info = inst.el.querySelector(".qt-info"); + if (!info) return; + if (r === "draw") { info.textContent = "Draw!"; } + else { info.textContent = `${LBL[r] || r} wins!`; } +} + +// --- Internal --- + +const LBL: Record<string, string> = { Gold: "Gold", Silver: "Silver" }; + +function me(): string | null { + return inst?.seat ? (inst.seat === 1 ? "Gold" : "Silver") : null; +} + +function render() { + if (!inst?.state) return; + const s = inst.state; + const my = me(); + const myTurn = my === s.t && !s.w; + + // Board + const board = inst.el.querySelector(".qt-board")!; + const cells = s.b.split("/"); + let bh = '<div class="qt-grid">'; + for (let r = 0; r < 4; r++) { + for (let c = 0; c < 4; c++) { + const p = cells[r][c]; + const clickable = myTurn && s.ph === "p" && p === "."; + bh += `<div class="qt-cell${clickable ? " qt-clickable" : ""}" data-r="${r}" data-c="${c}">`; + if (p !== ".") bh += pieceSvg(p, "qt-piece-lg"); + bh += `</div>`; + } + } + bh += "</div>"; + board.innerHTML = bh; + + board.querySelectorAll(".qt-clickable").forEach(el => { + el.addEventListener("click", () => { + const r = parseInt(el.getAttribute("data-r")!); + const c = parseInt(el.getAttribute("data-c")!); + inst?.cb?.(JSON.stringify({ row: r, col: c })); + }); + }); + + // Selected piece (waiting to be placed) + const selDiv = inst.el.querySelector(".qt-selected") as HTMLElement; + if (s.sp && s.ph === "p") { + const who = myTurn ? "You must place:" : `${LBL[s.t]} places:`; + selDiv.innerHTML = `<span class="qt-sel-label">${who}</span>${pieceSvg(s.sp, "qt-piece-sel")}`; + } else { + selDiv.innerHTML = ""; + } + + // Pool + const pool = inst.el.querySelector(".qt-pool") as HTMLElement; + const pickable = myTurn && s.ph === "s"; + let ph = ""; + if (s.av.length > 0) { + ph += `<div class="qt-pool-label">${pickable ? "Pick a piece for your opponent:" : "Available pieces:"}</div>`; + ph += '<div class="qt-pool-grid">'; + for (const p of s.av) { + ph += `<div class="qt-pool-item${pickable ? " qt-pickable" : ""}" data-piece="${p}">${pieceSvg(p, "qt-piece-sm")}</div>`; + } + ph += "</div>"; + } + pool.innerHTML = ph; + + pool.querySelectorAll(".qt-pickable").forEach(el => { + el.addEventListener("click", () => { + const p = el.getAttribute("data-piece")!; + inst?.cb?.(JSON.stringify({ piece: p })); + }); + }); + + // Info + const info = inst.el.querySelector(".qt-info") as HTMLElement; + if (s.w) { + info.textContent = s.w === "draw" ? "Draw!" : `${LBL[s.w]} wins!`; + info.className = "qt-info qt-result"; + } else if (my === null) { + const act = s.ph === "s" ? "selects" : "places"; + info.textContent = `${LBL[s.t]} ${act}`; + info.className = "qt-info"; + } else if (myTurn) { + info.textContent = s.ph === "p" ? "Place the piece on the board" : "Pick a piece for your opponent"; + info.className = "qt-info qt-myturn"; + } else { + info.textContent = `${LBL[s.t]}'s turn`; + info.className = "qt-info"; + } +} + +// Piece SVG rendering +// Traits: Tall/Short × Light/Dark × Round/Square × Hollow/Solid +function pieceSvg(hex: string, cls: string): string { + const v = parseInt(hex, 16); + const tall = !!(v & 8); + const light = !!(v & 4); + const round = !!(v & 2); + const hollow = !!(v & 1); + + const fill = light ? "#f5c542" : "#5b3e87"; + const stroke = light ? "#d4a017" : "#3d2560"; + const h = tall ? 40 : 26; + const y = tall ? 2 : 16; + const w = 30; + + let shape: string; + if (round) { + const rx = w / 2, ry = h / 2; + const cx = 21, cy = y + h / 2; + if (hollow) { + shape = `<ellipse cx="${cx}" cy="${cy}" rx="${rx - 2}" ry="${ry - 2}" fill="none" stroke="${fill}" stroke-width="4"/>`; + } else { + shape = `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${ry}" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>`; + } + } else { + const x = 21 - w / 2; + if (hollow) { + shape = `<rect x="${x + 2}" y="${y + 2}" width="${w - 4}" height="${h - 4}" rx="2" fill="none" stroke="${fill}" stroke-width="4"/>`; + } else { + shape = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="3" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>`; + } + } + + return `<svg class="${cls}" viewBox="0 0 42 44">${shape}</svg>`; +} diff --git a/games/quarto/ui/default/ui.css b/games/quarto/ui/default/ui.css new file mode 100644 index 0000000..2801106 --- /dev/null +++ b/games/quarto/ui/default/ui.css @@ -0,0 +1,133 @@ +/* Quarto */ + +.qt-root { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + max-width: 480px; + margin: 0 auto; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: rgb(var(--c-primary, 24 24 27)); + user-select: none; + -webkit-user-select: none; +} + +/* Board */ +.qt-board { + width: 100%; + max-width: 360px; +} + +.qt-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; + width: 100%; + aspect-ratio: 1; + background: rgb(var(--c-edge-light, 228 228 231)); + border-radius: 8px; + padding: 4px; +} + +.qt-cell { + background: rgb(var(--c-surface, 255 255 255)); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + transition: background 0.15s; +} + +.qt-cell.qt-clickable { + cursor: pointer; +} + +.qt-cell.qt-clickable:hover { + background: rgb(var(--c-surface-alt, 244 244 245)); + box-shadow: inset 0 0 0 2px #22c55e66; +} + +/* Piece sizes */ +.qt-piece-lg { + width: 70%; + height: 70%; +} + +.qt-piece-sm { + width: 100%; + height: 100%; +} + +.qt-piece-sel { + width: 40px; + height: 40px; +} + +/* Selected piece banner */ +.qt-selected { + display: flex; + align-items: center; + gap: 0.5rem; + min-height: 2.5rem; +} + +.qt-sel-label { + font-size: 0.85rem; + font-weight: 600; + color: rgb(var(--c-secondary, 63 63 70)); +} + +/* Piece pool */ +.qt-pool { + width: 100%; + max-width: 360px; +} + +.qt-pool-label { + font-size: 0.8rem; + font-weight: 600; + color: rgb(var(--c-muted, 113 113 122)); + margin-bottom: 0.25rem; + text-align: center; +} + +.qt-pool-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 4px; +} + +.qt-pool-item { + aspect-ratio: 1; + display: flex; + align-items: center; + justify-content: center; + background: rgb(var(--c-surface-muted, 250 250 250)); + border-radius: 4px; + padding: 2px; + transition: background 0.15s, box-shadow 0.15s; +} + +.qt-pool-item.qt-pickable { + cursor: pointer; +} + +.qt-pool-item.qt-pickable:hover { + background: rgb(var(--c-surface-alt, 244 244 245)); + box-shadow: 0 0 0 2px #facc15; +} + +/* Info */ +.qt-info { + text-align: center; + font-size: 0.85rem; + font-weight: 600; + padding: 0.2rem 0.5rem; + color: rgb(var(--c-muted, 113 113 122)); +} + +.qt-myturn { color: #facc15; } +.qt-result { color: #22c55e; font-size: 1rem; }