Update 2025-04-13_17:31:30

This commit is contained in:
root
2025-04-13 17:31:30 +02:00
parent 06fb52f574
commit 3013631fd7
19 changed files with 1043 additions and 130 deletions

21
.gitignore vendored Normal file
View File

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

View File

@ -1,75 +1,60 @@
let currentWord = []; import { setupButtons } from "./button.js";
let attemptsLeft = 0; 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"); let selectedIndices = [];
const input = document.getElementById("input"); let correctChangedTiles = [];
const feedback = document.getElementById("feedback"); let showFeedback = false;
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);
function renderWord() { let tiles = [];
wordDisplay.textContent = currentWord.join(" ");
attemptsDisplay.textContent = `Versuche übrig: ${attemptsLeft}`; 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() { function handleSubmit() {
const res = await fetch("/api/start", { showFeedback = true;
method: "POST" renderTiles();
});
const data = await res.json(); const result = calculateScore(selectedIndices, correctChangedTiles);
if (!data.word) { playSound(result.score > 0 ? "correct" : "wrong");
feedback.textContent = "❌ Fehler: Keine gültige Antwort vom Server.";
return;
}
currentWord = data.word; document.getElementById("submit-btn").disabled = true;
attemptsLeft = data.attempts;
topicLabel.textContent = `🧠 Thema: ${data.topic || '(unbekannt)'}`; setTimeout(() => {
feedback.textContent = `Spiel gestartet (${data.source})`; startGame();
renderWord(); }, 2500);
input.focus();
} }
async function guessLetter() { function resetGame() {
if (!currentWord.length) { selectedIndices = [];
feedback.textContent = "❌ Bitte zuerst ein Spiel starten!"; correctChangedTiles = [];
return; showFeedback = false;
} tiles = [];
renderTiles();
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();
} }
startBtn.addEventListener("click", startGame); window.addEventListener("load", () => {
input.addEventListener("keypress", (e) => { startRound();
if (e.key === "Enter") guessLetter();
}); });

76
static/button.js Normal file
View File

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

65
static/chart.js Normal file
View File

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

21
static/color.js Normal file
View File

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

2
static/config.js Normal file
View File

@ -0,0 +1,2 @@
export const DEBUG = true;

74
static/emoji.js Normal file
View File

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

30
static/help.js Normal file
View File

@ -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 = `
<div class="help-content">
<h2>🧠 How to Play</h2>
<p>This is a memory game with 4 dimensions:</p>
<ul>
<li><b>Position</b> where the tile appears</li>
<li><b>Color</b> the color of the emoji</li>
<li><b>Shape</b> the emoji symbol</li>
<li><b>Number</b> the digit inside</li>
</ul>
<p>You see one tile then after a short pause the board changes. Your task: guess which <b>dimension stayed the same</b>.</p>
<p>Use the buttons to select the unchanged dimension. You can only pick once per round.</p>
<button id="close-help">Close</button>
</div>
`;
document.body.appendChild(helpOverlay);
document.getElementById("close-help").onclick = () => {
helpOverlay.remove();
if (DEBUG) console.log("❌ showHelp() closed");
};
}

View File

@ -1,27 +1,65 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>WordCraze</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/static/style.css"> <title>ByThePowerOfMemory</title>
<link rel="stylesheet" href="/static/style.css" />
</head> </head>
<body> <body>
<main> <div class="container">
<h1>🧠 WordCraze</h1> <h1>ByThePowerOfMemory</h1>
<section class="controls"> <div id="game-area"></div>
<button id="start">Spiel starten</button>
</section>
<section class="game"> <div class="button-bar">
<div id="word" class="word">_ _ _</div> <button id="position-btn" >Position</button>
<input id="input" type="text" maxlength="1"> <button id="color-btn" >Color</button>
<div id="feedback" class="feedback"></div> <button id="shape-btn" >Shape</button>
<div id="attempts" class="attempts">Versuche übrig: 8</div> <button id="number-btn" >Number</button>
</section> </div>
</main> <div class="button-bar">
<button id="start-btn">Start Game</button>
<button id="submit-btn" disabled>Submit</button>
<button id="restart-btn">Restart</button>
<button id="help-btn">❓ Help</button>
<button id="stats-btn">📊 Stats</button>
</div>
<script type="module" src="/static/app.js"></script> <div id="scoreboard">
<p>✅ Correct: <span id="score-correct">0</span></p>
<p>❌ Wrong: <span id="score-wrong">0</span></p>
</div>
<div id="chart-container">
<canvas id="scoreChart" width="800" height="300"></canvas>
</div>
</div>
<script type="module">
import { startRound } from "/static/round.js";
import { setupButtons } from "/static/button.js";
import { guess } from "/static/score.js";
import { submit, restart, startGame } from "/static/state.js";
import { showOverlay } from "/static/overlay.js";
window.addEventListener("DOMContentLoaded", () => {
setupButtons({
onStart: startGame,
onSubmit: submit,
onRestart: restart,
onColor: () => guess("color"),
onPosition: () => guess("position"),
onShape: () => guess("shape"),
onNumber: () => guess("number")
});
document.getElementById("stats-btn").addEventListener("click", showOverlay);
startRound();
});
</script>
<script type="module" src="/static/chart.js"></script>
</body> </body>
</html> </html>

81
static/logic.js Normal file
View File

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

17
static/number.js Normal file
View File

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

60
static/overlay.js Normal file
View File

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

72
static/render.js Normal file
View File

@ -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 = `
<div class="emoji">${shapeToEmoji(tile.shape)}</div>
<div>${tile.number}</div>
${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);
}

86
static/round.js Normal file
View File

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

43
static/score.js Normal file
View File

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

14
static/sound.js Normal file
View File

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

69
static/state.js Normal file
View File

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

View File

@ -1,71 +1,161 @@
body { 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; max-width: 960px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 1rem;
background: #f9f9f9; box-sizing: border-box;
color: #333;
text-align: center;
} }
h1 { h1 {
text-align: center; 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; margin-bottom: 1rem;
} }
input[type="text"] { #game-canvas, #scoreChart {
font-size: 1.5rem; width: 100%;
text-align: center; height: auto;
padding: 0.3rem; display: block;
width: 3rem; margin: 0 auto 0.5rem;
} }
.feedback { #game-area > div.grid {
margin-top: 1rem; display: grid;
font-weight: bold; position: relative;
color: #444; 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 { .button-bar {
margin-top: 0.5rem; display: flex;
font-size: 0.9rem; flex-wrap: wrap;
color: #888; justify-content: center;
gap: 10px;
margin: 1rem 0;
} }
button { .button-bar button {
padding: 0.5rem 1rem; font-size: 1rem;
border: 1px solid #ccc; padding: 8px 16px;
border-radius: 6px; border: 1px solid #999;
background: white;
cursor: pointer; cursor: pointer;
transition: background 0.2s ease;
} }
button:hover { #start-btn {
background: #efefef; 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;
}
} }

69
static/tile.js Normal file
View File

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