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 = [];
|
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
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>
|
<!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
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 {
|
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
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