From 3013631fd792e58c5d9e9017e8337c8407fdafc5 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 13 Apr 2025 17:31:30 +0200 Subject: [PATCH] Update 2025-04-13_17:31:30 --- .gitignore | 21 +++++ static/app.js | 111 ++++++++++++--------------- static/button.js | 76 +++++++++++++++++++ static/chart.js | 65 ++++++++++++++++ static/color.js | 21 +++++ static/config.js | 2 + static/emoji.js | 74 ++++++++++++++++++ static/help.js | 30 ++++++++ static/index.html | 72 +++++++++++++----- static/logic.js | 81 ++++++++++++++++++++ static/number.js | 17 +++++ static/overlay.js | 60 +++++++++++++++ static/render.js | 72 ++++++++++++++++++ static/round.js | 86 +++++++++++++++++++++ static/score.js | 43 +++++++++++ static/sound.js | 14 ++++ static/state.js | 69 +++++++++++++++++ static/style.css | 190 ++++++++++++++++++++++++++++++++++------------ static/tile.js | 69 +++++++++++++++++ 19 files changed, 1043 insertions(+), 130 deletions(-) create mode 100644 .gitignore create mode 100644 static/button.js create mode 100644 static/chart.js create mode 100644 static/color.js create mode 100644 static/config.js create mode 100644 static/emoji.js create mode 100644 static/help.js create mode 100644 static/logic.js create mode 100644 static/number.js create mode 100644 static/overlay.js create mode 100644 static/render.js create mode 100644 static/round.js create mode 100644 static/score.js create mode 100644 static/sound.js create mode 100644 static/state.js create mode 100644 static/tile.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b89c079 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Bytecode-Dateien +__pycache__/ +*.py[cod] + +# Virtuelle Umgebungen +.venv/ +venv/ + +# Betriebssystem-Dateien +.DS_Store +Thumbs.db + +# Logfiles und Dumps +*.log +*.bak +*.swp +*.tmp + +# IDEs und Editoren +.vscode/ +.idea/ diff --git a/static/app.js b/static/app.js index 568510b..07da677 100644 --- a/static/app.js +++ b/static/app.js @@ -1,75 +1,60 @@ -let currentWord = []; -let attemptsLeft = 0; +import { setupButtons } from "./button.js"; +import { drawLineChart } from "./chart.js"; +import { shapeToEmoji } from "./emoji.js"; +import { playSound } from "./sound.js"; +import { guess } from "./score.js"; +import { startRound } from "./round.js"; +import { previousTile, currentTile } from "./state.js"; +import { generateRandomTiles } from "./tile.js"; +import { renderTiles } from "./render.js"; -const wordDisplay = document.getElementById("word"); -const input = document.getElementById("input"); -const feedback = document.getElementById("feedback"); -const attemptsDisplay = document.getElementById("attempts"); -const startBtn = document.getElementById("start"); -const topicLabel = document.createElement("div"); -topicLabel.id = "topic-label"; -topicLabel.style.marginTop = "1rem"; -topicLabel.style.fontWeight = "bold"; -wordDisplay.parentElement.insertBefore(topicLabel, wordDisplay); +let selectedIndices = []; +let correctChangedTiles = []; +let showFeedback = false; -function renderWord() { - wordDisplay.textContent = currentWord.join(" "); - attemptsDisplay.textContent = `Versuche รผbrig: ${attemptsLeft}`; +let tiles = []; + +function startGame() { + tiles.forEach(tile => delete tile.feedback); + document.getElementById("game-area").classList.add("flash"); + setTimeout(() => { + document.getElementById("game-area").classList.remove("flash"); + }, 250); + const beforeTiles = generateRandomTiles(); + const { modifiedTiles, changedIndices } = createModifiedTiles(beforeTiles); + + tiles = modifiedTiles; + correctChangedTiles = changedIndices; + selectedIndices = []; + showFeedback = false; + + renderTiles(); + document.getElementById("submit-btn").disabled = false; } -async function startGame() { - const res = await fetch("/api/start", { - method: "POST" - }); +function handleSubmit() { + showFeedback = true; + renderTiles(); - const data = await res.json(); - if (!data.word) { - feedback.textContent = "โŒ Fehler: Keine gรผltige Antwort vom Server."; - return; - } + const result = calculateScore(selectedIndices, correctChangedTiles); + playSound(result.score > 0 ? "correct" : "wrong"); - currentWord = data.word; - attemptsLeft = data.attempts; - topicLabel.textContent = `๐Ÿง  Thema: ${data.topic || '(unbekannt)'}`; - feedback.textContent = `Spiel gestartet (${data.source})`; - renderWord(); - input.focus(); + document.getElementById("submit-btn").disabled = true; + + setTimeout(() => { + startGame(); + }, 2500); } -async function guessLetter() { - if (!currentWord.length) { - feedback.textContent = "โŒ Bitte zuerst ein Spiel starten!"; - return; - } - - const letter = input.value.trim().toLowerCase(); - if (!letter || letter.length !== 1) return; - - const res = await fetch(`/api/guess/${letter}`, { method: "POST" }); - const data = await res.json(); - - if (!data.word) { - feedback.textContent = "โŒ Kein aktives Spiel โ€“ bitte erst starten!"; - return; - } - - currentWord = data.word; - attemptsLeft = data.attempts; - - let msg = data.correct_this_turn > 0 ? "โœ…" : "โŒ"; - if (data.status === "won") { - msg += " ๐ŸŽ‰ Gewonnen!"; - } else if (data.status === "lost") { - msg += ` ๐Ÿ’ฅ Verloren! โ‡จ ${data.target} โ‡ฆ`; - } - feedback.textContent = msg; - renderWord(); - input.value = ""; - input.focus(); +function resetGame() { + selectedIndices = []; + correctChangedTiles = []; + showFeedback = false; + tiles = []; + renderTiles(); } -startBtn.addEventListener("click", startGame); -input.addEventListener("keypress", (e) => { - if (e.key === "Enter") guessLetter(); +window.addEventListener("load", () => { + startRound(); }); diff --git a/static/button.js b/static/button.js new file mode 100644 index 0000000..861b1e8 --- /dev/null +++ b/static/button.js @@ -0,0 +1,76 @@ +import { showHelp } from "./help.js"; +import { DEBUG } from "./config.js"; + +export function setupButtons({ onStart, onSubmit, onRestart, onColor, onPosition, onShape, onNumber }) { + const startBtn = document.getElementById("start-btn"); + const submitBtn = document.getElementById("submit-btn"); + const restartBtn = document.getElementById("restart-btn"); + const colorBtn = document.getElementById("color-btn"); + const positionBtn = document.getElementById("position-btn"); + const shapeBtn = document.getElementById("shape-btn"); + const numberBtn = document.getElementById("number-btn"); + const helpBtn = document.getElementById("help-btn"); + + if (startBtn) { + startBtn.addEventListener("click", () => { + if (DEBUG) console.log("โ–ถ๏ธ Start clicked"); + onStart?.(); + }); + } + + if (submitBtn) { + submitBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐Ÿ“จ Submit clicked"); + onSubmit?.(); + }); + } + + if (restartBtn) { + restartBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐Ÿ” Restart clicked"); + onRestart?.(); + }); + } + + if (colorBtn) { + colorBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐ŸŽจ Color clicked"); + onColor?.(); + }); + } + + if (positionBtn) { + positionBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐Ÿ“ Position clicked"); + onPosition?.(); + }); + } + + if (shapeBtn) { + shapeBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐Ÿ”ท Shape clicked"); + onShape?.(); + }); + } + + if (numberBtn) { + numberBtn.addEventListener("click", () => { + if (DEBUG) console.log("๐Ÿ”ข Number clicked"); + onNumber?.(); + }); + } + + if (helpBtn) { + helpBtn.addEventListener("click", () => { + if (DEBUG) console.log("โ“ Help clicked"); + showHelp(); + }); + } +} + +export function enableButtons() { + document.querySelectorAll("button").forEach(btn => { + btn.disabled = false; + }); +} + diff --git a/static/chart.js b/static/chart.js new file mode 100644 index 0000000..b3440db --- /dev/null +++ b/static/chart.js @@ -0,0 +1,65 @@ +export async function drawLineChart() { + const res = await fetch("/api/stats"); + const data = await res.json(); + + const canvas = document.getElementById("scoreChart"); + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const allDays = Array.from(new Set( + Object.values(data).flat().map(d => d.day) + )).sort(); + + const maxScore = Math.max( + ...Object.values(data).flat().map(d => d.avg_score) + ); + + const padding = 50; + const chartW = canvas.width - padding * 2; + const chartH = canvas.height - padding * 2; + + // Axes + ctx.strokeStyle = "#000"; + ctx.beginPath(); + ctx.moveTo(padding, padding); + ctx.lineTo(padding, canvas.height - padding); + ctx.lineTo(canvas.width - padding, canvas.height - padding); + ctx.stroke(); + + const colors = ["#007bff", "#28a745", "#dc3545", "#ffc107", "#6f42c1"]; + let playerIndex = 0; + + for (const [player, entries] of Object.entries(data)) { + ctx.strokeStyle = colors[playerIndex % colors.length]; + ctx.beginPath(); + + entries.forEach((entry, i) => { + const x = padding + (i * (chartW / (allDays.length - 1))); + const y = canvas.height - padding - ((entry.avg_score / maxScore) * chartH); + + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + + // draw point + ctx.beginPath(); + ctx.arc(x, y, 3, 0, 2 * Math.PI); + ctx.fillStyle = ctx.strokeStyle; + ctx.fill(); + }); + + ctx.stroke(); + + // Legend + ctx.fillStyle = ctx.strokeStyle; + ctx.fillText(player, canvas.width - 100, padding + playerIndex * 20); + playerIndex++; + } + + // X axis labels + ctx.fillStyle = "#000"; + allDays.forEach((day, i) => { + const x = padding + (i * (chartW / (allDays.length - 1))); + ctx.fillText(day.slice(5), x - 10, canvas.height - padding + 15); + }); +} + diff --git a/static/color.js b/static/color.js new file mode 100644 index 0000000..3060062 --- /dev/null +++ b/static/color.js @@ -0,0 +1,21 @@ +export const COLORS = [ + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "pink", + "cyan", + "red", + "black" +]; + +export function colorToClass(color) { + return `color-${color}`; +} + +export function applyColor(element, color) { + element.classList.add(colorToClass(color)); +} + diff --git a/static/config.js b/static/config.js new file mode 100644 index 0000000..db31eeb --- /dev/null +++ b/static/config.js @@ -0,0 +1,2 @@ +export const DEBUG = true; + diff --git a/static/emoji.js b/static/emoji.js new file mode 100644 index 0000000..6630bb4 --- /dev/null +++ b/static/emoji.js @@ -0,0 +1,74 @@ +export const SHAPES = [ + "ring_x", + "ring_cross", + "circle", + "square", + "triangle_up", + "triangle_down", + "triangle_left", + "triangle_right", + "ring_x", + "ring_cross", + "cross", + "star", + "heart", + "diamond", + "arrow_up", + "arrow_down", + "arrow_left", + "arrow_right", + "asterisk", + "double_excl", + "question_double", + "question_excl", + "excl_question", + "spiral", + "dots1", + "dots2", + "flower", + "pulsar", + "mandala1", + "mandala2", + "mandala3", + "mandala4", + "mandala5" +];; + +export function shapeToEmoji(shape) { + const map = { + ring_x: "แณ", + ring_cross: "แช ", + circle: "โ—", + square: "โ– ", + triangle_up: "โ–ฒ", + triangle_down: "โ–ผ", + triangle_left: "โ—€", + triangle_right: "โ–ถ", + cross: "โœ–", + star: "โ˜…", + heart: "โ™ฅ", + diamond: "โ—†", + arrow_up: "โ†‘", + arrow_down: "โ†“", + arrow_left: "โ†", + arrow_right: "โ†’", + asterisk: "โ€ป", + double_excl: "โ€ผ", + question_double: "โ‡", + question_excl: "โˆ", + excl_question: "โ‰", + spiral: "๊ฉœ", + dots1: "๐ฎš", + dots2: "๐ฎ™", + flower: "๐ซฑ", + pulsar: "๐ซฐ", + mandala1: "๐ฉ•", + mandala2: "๐‘", + mandala3: "๐‘—Š", + mandala4: "๐‘—Œ", + mandala5: "๐‘—" + }; + const emoji = map[shape] || "?"; + return emoji; +} + diff --git a/static/help.js b/static/help.js new file mode 100644 index 0000000..ddd8157 --- /dev/null +++ b/static/help.js @@ -0,0 +1,30 @@ +import { DEBUG } from "./config.js"; + +export function showHelp() { + if (DEBUG) console.log("โ” showHelp() opened"); + const helpOverlay = document.createElement("div"); + helpOverlay.id = "help-overlay"; + helpOverlay.innerHTML = ` +
+

๐Ÿง  How to Play

+

This is a memory game with 4 dimensions:

+ +

You see one tile โ€“ then after a short pause the board changes. Your task: guess which dimension stayed the same.

+

Use the buttons to select the unchanged dimension. You can only pick once per round.

+ +
+ `; + + document.body.appendChild(helpOverlay); + + document.getElementById("close-help").onclick = () => { + helpOverlay.remove(); + if (DEBUG) console.log("โŒ showHelp() closed"); + }; +} + diff --git a/static/index.html b/static/index.html index f8bff1d..b04f08c 100644 --- a/static/index.html +++ b/static/index.html @@ -1,27 +1,65 @@ - + - - WordCraze - + + + ByThePowerOfMemory + -
-

๐Ÿง  WordCraze

+
+

ByThePowerOfMemory

-
- -
+
-
-
_ _ _
- - -
Versuche รผbrig: 8
-
-
+
+ + + + +
+
+ + + + + +
- +
+

โœ… Correct: 0

+

โŒ Wrong: 0

+
+ +
+ +
+ + + + diff --git a/static/logic.js b/static/logic.js new file mode 100644 index 0000000..2f5d298 --- /dev/null +++ b/static/logic.js @@ -0,0 +1,81 @@ +import { SHAPES, shapeToEmoji } from "./emoji.js"; +import { COLORS } from "./color.js"; +import { numberToEmoji } from "./number.js"; + +export let level = 1; + +export function nextLevel() { + level = Math.min(level + 1, 3); +} + +export function resetLevel() { + level = 1; +} + +export function getGridSize(lvl = level) { + if (lvl <= 1) return 2; + if (lvl === 2) return 3; + return 4; +} + +export function getGridPositions(size) { + const positions = []; + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + positions.push([y, x]); + } + } + return positions; +} + +export function generateRandomTile(lvl = level) { + const gridSize = getGridSize(lvl); + const POSITIONS = getGridPositions(gridSize); + const index = Math.floor(Math.random() * POSITIONS.length); + const position = POSITIONS[index]; + + return { + shape: SHAPES[Math.floor(Math.random() * SHAPES.length)], + color: COLORS[Math.floor(Math.random() * COLORS.length)], + number: Math.floor(Math.random() * 10), + position + }; +} + +export function mutateTile(tile, lvl = level) { + const dimensions = ["shape", "color", "number", "position"]; + const changes = dimensions.filter(() => Math.random() < 0.5); + + const newTile = { ...tile }; + const POSITIONS = getGridPositions(getGridSize(lvl)); + + for (const dim of changes) { + switch (dim) { + case "shape": + newTile.shape = pickOther(SHAPES, tile.shape); + break; + case "color": + newTile.color = pickOther(COLORS, tile.color); + break; + case "number": + newTile.number = pickOther([...Array(10).keys()], tile.number); + break; + case "position": + newTile.position = pickOther(POSITIONS, tile.position); + break; + } + } + return newTile; +} + +function pickOther(arr, exclude) { + const options = arr.filter(x => JSON.stringify(x) !== JSON.stringify(exclude)); + return options[Math.floor(Math.random() * options.length)]; +} + +export function getChangedDimensions(a, b) { + return ["shape", "color", "number", "position"].filter(dim => { + return JSON.stringify(a[dim]) !== JSON.stringify(b[dim]); + }); +} + diff --git a/static/number.js b/static/number.js new file mode 100644 index 0000000..4dc3ee7 --- /dev/null +++ b/static/number.js @@ -0,0 +1,17 @@ +export function numberToEmoji(num) { + const map = { + 0: "0๏ธโƒฃ", + 1: "1๏ธโƒฃ", + 2: "2๏ธโƒฃ", + 3: "3๏ธโƒฃ", + 4: "4๏ธโƒฃ", + 5: "5๏ธโƒฃ", + 6: "6๏ธโƒฃ", + 7: "7๏ธโƒฃ", + 8: "8๏ธโƒฃ", + 9: "9๏ธโƒฃ", + 10: "๐Ÿ”Ÿ" + }; + return map[num] || String(num); +} + diff --git a/static/overlay.js b/static/overlay.js new file mode 100644 index 0000000..8548175 --- /dev/null +++ b/static/overlay.js @@ -0,0 +1,60 @@ +import { state } from "./state.js"; + +export function showOverlay() { + const overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.background = "rgba(0, 0, 0, 0.7)"; + overlay.style.display = "flex"; + overlay.style.flexDirection = "column"; + overlay.style.alignItems = "center"; + overlay.style.justifyContent = "center"; + overlay.style.color = "white"; + overlay.style.zIndex = "9999"; + + const statsBox = document.createElement("div"); + statsBox.style.background = "#222"; + statsBox.style.padding = "1em 2em"; + statsBox.style.borderRadius = "12px"; + statsBox.style.maxWidth = "90%"; + statsBox.style.maxHeight = "80%"; + statsBox.style.overflowY = "auto"; + + const title = document.createElement("h2"); + title.textContent = "Spielverlauf"; + title.style.marginBottom = "0.5em"; + statsBox.appendChild(title); + + if (state.gameHistory.length === 0) { + const empty = document.createElement("p"); + empty.textContent = "Noch keine Daten vorhanden."; + statsBox.appendChild(empty); + } else { + const list = document.createElement("ul"); + list.style.listStyle = "none"; + list.style.padding = "0"; + state.gameHistory.forEach(entry => { + const li = document.createElement("li"); + li.textContent = `${entry.time.replace(/T/, " ").slice(0, 19)} โ€” ${entry.result}`; + list.appendChild(li); + }); + statsBox.appendChild(list); + } + + const close = document.createElement("button"); + close.textContent = "SchlieรŸen"; + close.style.marginTop = "1em"; + close.style.padding = "0.5em 1em"; + close.style.border = "none"; + close.style.borderRadius = "6px"; + close.style.cursor = "pointer"; + close.onclick = () => document.body.removeChild(overlay); + + statsBox.appendChild(close); + overlay.appendChild(statsBox); + document.body.appendChild(overlay); +} + diff --git a/static/render.js b/static/render.js new file mode 100644 index 0000000..f7916e8 --- /dev/null +++ b/static/render.js @@ -0,0 +1,72 @@ +import { shapeToEmoji } from "./emoji.js"; +import { DEBUG } from "./config.js"; +import { getTileFeedback } from "./tile.js"; +import { getGridPositions } from "./logic.js"; + +export function createCanvas() { + const canvas = document.createElement("canvas"); + canvas.id = "game-canvas"; + canvas.width = 600; + canvas.height = 600; + return canvas; +} + +export function renderTiles(tiles, selectedIndices, changedIndices, showFeedback, gridSize) { + if (DEBUG) console.log("๐Ÿ”„ renderTiles called", { + tiles, + selectedIndices, + changedIndices, + showFeedback, + gridSize + }); + const gameArea = document.getElementById("game-area"); + gameArea.innerHTML = ""; + + const grid = document.createElement("div"); + grid.className = "grid"; + grid.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`; + + const validPositions = getGridPositions(gridSize).map(pos => pos.toString()); + + tiles.forEach((tile, idx) => { + if (!tile.position || tile.position.length !== 2 || !validPositions.includes(tile.position.toString())) { + if (DEBUG) console.warn("โ— Ungรผltige tile.position", tile); + return; + } + const [row, col] = tile.position; + const div = document.createElement("div"); + div.className = "tile"; + div.style.gridRowStart = row + 1; + div.style.gridColumnStart = col + 1; + + let feedbackIcon = ""; + if (showFeedback) { + const fb = getTileFeedback(idx, selectedIndices, changedIndices); + if (fb === "correct") feedbackIcon = "โœ…"; + if (fb === "wrong") feedbackIcon = "โŒ"; + if (fb === "missed") feedbackIcon = "โš ๏ธ"; + } + + div.innerHTML = ` +
${shapeToEmoji(tile.shape)}
+
${tile.number}
+ ${feedbackIcon} + `; + + if (!showFeedback) { + div.onclick = () => { + if (selectedIndices.includes(idx)) { + selectedIndices.splice(selectedIndices.indexOf(idx), 1); + } else { + selectedIndices.push(idx); + } + renderTiles(tiles, selectedIndices, changedIndices, false, gridSize); + }; + } + + grid.appendChild(div); + }); + + gameArea.appendChild(grid); +} + diff --git a/static/round.js b/static/round.js new file mode 100644 index 0000000..5d73fe5 --- /dev/null +++ b/static/round.js @@ -0,0 +1,86 @@ +import { generateRandomTile, mutateTile, getGridSize } from "./logic.js"; +import { renderTiles, createCanvas } from "./render.js"; +import { resetScore, state } from "./state.js"; +import { enableButtons } from "./button.js"; +import { DEBUG } from "./config.js"; + +export function startRound() { + const canvas = createCanvas(); + const gameArea = document.getElementById("game-area"); + gameArea.innerHTML = ""; + gameArea.appendChild(canvas); + + const ctx = canvas?.getContext("2d"); + if (DEBUG) console.log("startRound called"); + if (DEBUG) console.log("canvas element:", canvas); + + if (!canvas || !ctx) { + if (DEBUG) console.warn("game-canvas or context not available"); + if (DEBUG) { + fetch("/api/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: "Canvas or 2D context not available at round start", + timestamp: new Date().toISOString() + }) + }); + } + return; + } + + resetScore(); + const size = getGridSize(); + + if (!state.previousTile) { + state.previousTile = {}; + } + if (!state.currentTile) { + state.currentTile = {}; + } + + state.previousTile.shape = null; + state.previousTile.color = null; + state.previousTile.number = null; + state.previousTile.position = null; + + const generated = generateRandomTile(); + if (DEBUG) console.log("๐Ÿ†• generated tile:", generated); + Object.assign(state.previousTile, generated); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + renderTiles([state.previousTile], [], [], false, size); + + setTimeout(() => { + const mutated = mutateTile(state.previousTile); + if (!mutated || !mutated.position) { + if (DEBUG) { + fetch("/api/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: "Mutation failed or position missing", + timestamp: new Date().toISOString() + }) + }); + } + return; + } + if (DEBUG) console.log("๐Ÿ”€ mutated tile:", mutated); + Object.assign(state.currentTile, mutated); + ctx.clearRect(0, 0, canvas.width, canvas.height); + renderTiles([state.currentTile], [], [], false, size); + enableButtons(); + if (DEBUG) { + fetch("/api/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: "Round started: dimensions mutated", + timestamp: new Date().toISOString() + }) + }); + } + }, 2000); +} + diff --git a/static/score.js b/static/score.js new file mode 100644 index 0000000..94decfa --- /dev/null +++ b/static/score.js @@ -0,0 +1,43 @@ +import { state, updateScore } from "./state.js"; +import { DEBUG } from "./config.js"; +import { startRound } from "./round.js"; +import { playSound } from "./sound.js"; + +export function guess(dimension) { + if (DEBUG) console.log("๐Ÿง  guess() triggered", { dimension }); + if (DEBUG) console.log("previousTile:", state.previousTile); + if (DEBUG) console.log("currentTile:", state.currentTile); + const result = state.previousTile[dimension] === state.currentTile[dimension]; + const btn = document.getElementById(`${dimension}-btn`); + btn.textContent = result ? "โœ…" : "โŒ"; + btn.disabled = true; + playSound(result ? "correct" : "wrong"); + updateScore(result); + + const scoreCorrect = parseInt(localStorage.getItem("correct")) || 0; + const scoreWrong = parseInt(localStorage.getItem("wrong")) || 0; + const total = scoreCorrect + scoreWrong; + if (total === 4) { + setTimeout(() => startRound(), 1000); + } +} + +export function calculateScore(selected, correct) { + const correctSet = new Set(correct); + const selectedSet = new Set(selected); + const correctGuesses = selected.filter(i => correctSet.has(i)); + const wrongGuesses = selected.filter(i => !correctSet.has(i)); + const missed = correct.filter(i => !selectedSet.has(i)); + + correctGuesses.forEach(i => tiles[i].feedback = "correct"); + wrongGuesses.forEach(i => tiles[i].feedback = "wrong"); + missed.forEach(i => tiles[i].feedback = "missed"); + + return { + correctGuesses, + wrongGuesses, + missed, + score: correctGuesses.length - wrongGuesses.length + }; +} + diff --git a/static/sound.js b/static/sound.js new file mode 100644 index 0000000..e115481 --- /dev/null +++ b/static/sound.js @@ -0,0 +1,14 @@ +export function playSound(name) { + const text = name === "correct" ? "Correct" : name === "wrong" ? "Wrong" : name; + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = "en-US"; + + const voices = speechSynthesis.getVoices(); + const robotVoice = voices.find(v => + v.name.toLowerCase().includes("google") || v.name.toLowerCase().includes("robot") + ); + + if (robotVoice) utterance.voice = robotVoice; + speechSynthesis.speak(utterance); +} + diff --git a/static/state.js b/static/state.js new file mode 100644 index 0000000..6a4c003 --- /dev/null +++ b/static/state.js @@ -0,0 +1,69 @@ +import { DEBUG } from "./config.js"; + +export const state = { + previousTile: null, + currentTile: null, + scoreCorrect: 0, + scoreWrong: 0, + gameHistory: [] +}; + +export function resetScore() { + if (DEBUG) console.log("๐Ÿ”„ resetScore() called"); + state.scoreCorrect = 0; + state.scoreWrong = 0; + localStorage.setItem("correct", "0"); + localStorage.setItem("wrong", "0"); + localStorage.setItem("history", JSON.stringify([])); + document.getElementById("score-correct").textContent = "0"; + document.getElementById("score-wrong").textContent = "0"; +} + +export function updateScore(isCorrect) { + if (DEBUG) console.log("๐ŸŽฏ updateScore() called", { isCorrect }); + if (isCorrect) { + state.scoreCorrect++; + localStorage.setItem("correct", state.scoreCorrect); + document.getElementById("score-correct").textContent = state.scoreCorrect; + } else { + state.scoreWrong++; + localStorage.setItem("wrong", state.scoreWrong); + document.getElementById("score-wrong").textContent = state.scoreWrong; + } + logGuess(isCorrect); +} + +export function loadScore() { + const savedCorrect = parseInt(localStorage.getItem("correct")) || 0; + const savedWrong = parseInt(localStorage.getItem("wrong")) || 0; + const savedHistory = JSON.parse(localStorage.getItem("history")) || []; + state.scoreCorrect = savedCorrect; + state.scoreWrong = savedWrong; + state.gameHistory = savedHistory; + document.getElementById("score-correct").textContent = savedCorrect; + document.getElementById("score-wrong").textContent = savedWrong; +} + +export function logGuess(isCorrect) { + const timestamp = new Date().toISOString(); + const entry = { time: timestamp, result: isCorrect ? "โœ…" : "โŒ" }; + state.gameHistory.push(entry); + localStorage.setItem("history", JSON.stringify(state.gameHistory)); + if (DEBUG) console.log("๐Ÿ•“ Logged round: ", entry); +} + +export function submit() { + if (DEBUG) console.log("submit() triggered"); +} + +export function restart() { + resetScore(); + if (DEBUG) console.log("restart() triggered"); +} + +export function startGame() { + resetScore(); + loadScore(); + if (DEBUG) console.log("startGame() triggered"); +} + diff --git a/static/style.css b/static/style.css index 9013ad6..290c91b 100644 --- a/static/style.css +++ b/static/style.css @@ -1,71 +1,161 @@ body { - font-family: sans-serif; + font-family: system-ui, sans-serif; + background-color: #f8f9fa; + margin: 0; + padding: 0; + text-align: center; +} + +.container { max-width: 960px; margin: 0 auto; - padding: 2rem; - background: #f9f9f9; - color: #333; - text-align: center; + padding: 1rem; + box-sizing: border-box; } h1 { text-align: center; - margin-bottom: 2rem; -} - -.controls { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - margin-bottom: 2rem; -} - -.controls label, -.controls button { - font-size: 1rem; - margin: 0 auto; -} - -.game { - text-align: center; -} - -.word { - font-size: 2rem; - letter-spacing: 0.5rem; margin-bottom: 1rem; } -input[type="text"] { - font-size: 1.5rem; - text-align: center; - padding: 0.3rem; - width: 3rem; +#game-canvas, #scoreChart { + width: 100%; + height: auto; + display: block; + margin: 0 auto 0.5rem; } -.feedback { - margin-top: 1rem; - font-weight: bold; - color: #444; +#game-area > div.grid { + display: grid; + position: relative; + max-width: 480px; + aspect-ratio: 1 / 1; + margin: 0 auto; + gap: 6px; + padding: 4px; + place-items: center; +} + max-width: 480px; + aspect-ratio: 1 / 1; + margin: 0 auto; + display: grid; + gap: 6px; + padding: 4px; } -.attempts { - margin-top: 0.5rem; - font-size: 0.9rem; - color: #888; +.button-bar { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + margin: 1rem 0; } -button { - padding: 0.5rem 1rem; - border: 1px solid #ccc; - border-radius: 6px; - background: white; +.button-bar button { + font-size: 1rem; + padding: 8px 16px; + border: 1px solid #999; cursor: pointer; - transition: background 0.2s ease; } -button:hover { - background: #efefef; +#start-btn { + background-color: #d4edda; + border-color: #b7dfc3; +} + +#start-btn:hover { + background-color: #c3e6cb; +} + +#restart-btn { + background-color: #f8d7da; + border-color: #f5c6cb; +} + +#restart-btn:hover { + background-color: #f1b0b7; +} + +#submit-btn { + background-color: #e0e0e0; + border-color: #ccc; +} + +#submit-btn:hover { + background-color: #d0d0d0; +} + +button:disabled { + background-color: #ccc; + color: #666; + cursor: not-allowed; +} + +.correct { + border: 2px solid #28a745; + box-shadow: 0 0 4px #28a745; +} + +.wrong { + border: 2px solid #dc3545; + box-shadow: 0 0 4px #dc3545; +} + +.missed { + border: 2px solid #ffc107; + box-shadow: 0 0 4px #ffc107; +} + +#scoreboard { + max-width: 480px; + margin: 0.5rem auto; + padding: 0.5rem; + background-color: #f4f4f4; + border: 1px solid #ccc; + font-size: 1.1rem; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + justify-items: center; +} + +.color-red { color: #e74c3c; } +.color-blue { color: #3498db; } +.color-green { color: #2ecc71; } +.color-yellow { color: #f1c40f; } +.color-orange { color: #e67e22; } +.color-purple { color: #9b59b6; } +.color-pink { color: #ff69b4; } +.color-cyan { color: #00bcd4; } +.color-grey { color: #95a5a6; } +.color-brown { color: #8d6e63; } + +.flash { + animation: flash-reset 0.3s ease-in-out; +} + +@keyframes flash-reset { + 0% { background-color: #fff; } + 50% { background-color: #cce5ff; } + 100% { background-color: #fff; } +} + +@media (max-width: 500px) { + .button-bar { + flex-direction: column; + align-items: center; + } + + .button-bar button { + width: 100%; + max-width: 200px; + font-size: 0.95rem; + padding: 10px 20px; + } + + #scoreboard { + font-size: 0.95rem; + padding: 0.75rem; + } } diff --git a/static/tile.js b/static/tile.js new file mode 100644 index 0000000..f657d03 --- /dev/null +++ b/static/tile.js @@ -0,0 +1,69 @@ +import { SHAPES } from "./emoji.js"; + +export function generateRandomTiles(gridSize = 4, shapePoolSize = 4, numberPoolSize = 5) { + const totalTiles = gridSize * gridSize; + const shapes = SHAPES.sort(() => 0.5 - Math.random()).slice(0, shapePoolSize); + const numbers = Array.from({ length: numberPoolSize }, (_, i) => i + 1); + + const positions = getGridPositions(gridSize); + const tiles = Array.from({ length: totalTiles }, (_, i) => ({ + shape: shapes[Math.floor(Math.random() * shapes.length)], + number: numbers[Math.floor(Math.random() * numbers.length)], + position: positions[i] + })); + + return tiles; +} + +export function createModifiedTiles(originalTiles, changes = 2) { + const newTiles = originalTiles.map(t => ({ ...t })); + const changeIndices = new Set(); + + while (changeIndices.size < changes) { + changeIndices.add(Math.floor(Math.random() * newTiles.length)); + } + + for (const index of changeIndices) { + let newShape; + do { + newShape = SHAPES[Math.floor(Math.random() * SHAPES.length)]; + } while (newShape === newTiles[index].shape); + + newTiles[index].shape = newShape; + } + + return { modifiedTiles: newTiles, changedIndices: [...changeIndices] }; +} + +export function getGridPositions(gridSize) { + const positions = []; + for (let row = 0; row < gridSize; row++) { + for (let col = 0; col < gridSize; col++) { + positions.push([row, col]); + } + } + return positions; +} + +export function getTileFeedback(index, selected, changed) { + if (selected.includes(index) && changed.includes(index)) return "correct"; + if (selected.includes(index) && !changed.includes(index)) return "wrong"; + if (!selected.includes(index) && changed.includes(index)) return "missed"; + return null; +} + +export function calculateScore(selected, correct) { + const correctSet = new Set(correct); + const selectedSet = new Set(selected); + const correctGuesses = selected.filter(i => correctSet.has(i)); + const wrongGuesses = selected.filter(i => !correctSet.has(i)); + const missed = correct.filter(i => !selectedSet.has(i)); + + return { + correctGuesses, + wrongGuesses, + missed, + score: correctGuesses.length - wrongGuesses.length + }; +} +