Update 2025-04-13_17:31:30
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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/
|
111
static/app.js
111
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();
|
||||
});
|
||||
|
||||
|
76
static/button.js
Normal file
76
static/button.js
Normal 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
65
static/chart.js
Normal 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
21
static/color.js
Normal 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
2
static/config.js
Normal file
@ -0,0 +1,2 @@
|
||||
export const DEBUG = true;
|
||||
|
74
static/emoji.js
Normal file
74
static/emoji.js
Normal 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
30
static/help.js
Normal 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");
|
||||
};
|
||||
}
|
||||
|
@ -1,27 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WordCraze</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ByThePowerOfMemory</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>🧠 WordCraze</h1>
|
||||
<div class="container">
|
||||
<h1>ByThePowerOfMemory</h1>
|
||||
|
||||
<section class="controls">
|
||||
<button id="start">Spiel starten</button>
|
||||
</section>
|
||||
<div id="game-area"></div>
|
||||
|
||||
<section class="game">
|
||||
<div id="word" class="word">_ _ _</div>
|
||||
<input id="input" type="text" maxlength="1">
|
||||
<div id="feedback" class="feedback"></div>
|
||||
<div id="attempts" class="attempts">Versuche übrig: 8</div>
|
||||
</section>
|
||||
</main>
|
||||
<div class="button-bar">
|
||||
<button id="position-btn" >Position</button>
|
||||
<button id="color-btn" >Color</button>
|
||||
<button id="shape-btn" >Shape</button>
|
||||
<button id="number-btn" >Number</button>
|
||||
</div>
|
||||
<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>
|
||||
</html>
|
||||
|
||||
|
81
static/logic.js
Normal file
81
static/logic.js
Normal 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
17
static/number.js
Normal 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
60
static/overlay.js
Normal 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
72
static/render.js
Normal 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
86
static/round.js
Normal 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
43
static/score.js
Normal 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
14
static/sound.js
Normal 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
69
static/state.js
Normal 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");
|
||||
}
|
||||
|
190
static/style.css
190
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
69
static/tile.js
Normal file
69
static/tile.js
Normal 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
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user