edu-boardgame-generator/minigames/memory.js
Spiel-Generator Workshop 016be6ea90 feat: initial project structure with modular mini-game system
- editor.html: 5-step game editor with live Python code split-screen
- game.html: playable board game engine
- codegen.js: Python live-code generator (extracted from editor)
- logo.png: PH Weingarten logo (extracted from Base64)
- minigames/_api.js: shared Mini-Game API (makeCanvas, onResult, helpers)
- 12 Mini-Games as individual modules (launch + preview API):
  snake, flappy, memory, quiz, reaction,
  basketball, catch, maze, simon, typing, puzzle, spotdiff
- README.md with architecture docs and deployment guide

Refactoring highlights:
  - editor.html: 251 KB → 102 KB (-59%)
  - Mini-games fully decoupled, each ~100-200 lines
  - All 12 games now have working launch() + preview()
  - Maze uses recursive backtracker algorithm
  - spotdiff uses canvas-drawn scene with 5 differences
2026-03-14 22:12:25 +00:00

142 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* minigames/memory.js
* 🃏 Memory — Finde alle Paare, so schnell wie möglich!
*/
window.MG_memory = (function() {
const ID = 'memory';
const EMOJI = '🃏';
const NAME = 'Memory';
const DESC = 'Finde alle Paare — so schnell wie möglich!';
const CONTROLS = 'Mausklick / Tippen';
const MULTI = 1;
function run(wrap, W, H, cfg, onDone) {
const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H);
const theme = cfg.theme || { primary: '#8b5cf6' };
const EMOJIS = cfg.emojis || ['🐱','🐶','🦊','🐸','🦋','🐠'];
const all = [...EMOJIS, ...EMOJIS].sort(() => Math.random() - 0.5);
let cards = all.map((e, i) => ({ e, i, open: false }));
let flipped = [], matched = [], checking = false, moves = 0;
let stopped = false, raf, startTime = null, elapsed = 0;
// Layout: 4×3
const COLS = 4, ROWS = Math.ceil(all.length / 4);
const PAD = 8;
const cw = Math.floor((W - PAD * (COLS + 1)) / COLS);
const ch = Math.floor((H - PAD * (ROWS + 2) - 24) / ROWS);
const ox = (W - COLS * cw - PAD * (COLS - 1)) / 2;
const oy = 30;
function cardAt(mx, my) {
for (let ri = 0; ri < ROWS; ri++) {
for (let ci = 0; ci < COLS; ci++) {
const idx = ri * COLS + ci;
if (idx >= cards.length) continue;
const x = ox + ci * (cw + PAD);
const y = oy + ri * (ch + PAD);
if (mx >= x && mx < x + cw && my >= y && my < y + ch) return cards[idx];
}
}
return null;
}
function onClick(e) {
if (checking) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
const card = cardAt(mx, my);
if (!card || card.open || matched.includes(card.i)) return;
if (!startTime) startTime = performance.now();
card.open = true;
flipped.push(card);
if (flipped.length === 2) {
moves++;
checking = true;
setTimeout(() => {
if (flipped[0].e === flipped[1].e) {
matched.push(flipped[0].i, flipped[1].i);
if (matched.length === cards.length) {
setTimeout(() => onDone(true), 600);
}
} else {
flipped.forEach(c => c.open = false);
}
flipped = [];
checking = false;
}, 700);
}
}
canvas.addEventListener('click', onClick);
// Karten-Flip-Animation (progress 01)
const flipAnim = new Map(); // card.i → { prog, dir }
function loop(ts) {
if (stopped) return;
raf = requestAnimationFrame(loop);
if (startTime) elapsed = (performance.now() - startTime) / 1000;
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, W, H);
// HUD
MGAPI.text(ctx, `🃏 ${matched.length / 2} / ${EMOJIS.length} | Züge: ${moves}`, W / 2, 16,
{ size: 12, color: theme.primary });
// Karten zeichnen
for (let ri = 0; ri < ROWS; ri++) {
for (let ci = 0; ci < COLS; ci++) {
const idx = ri * COLS + ci;
if (idx >= cards.length) continue;
const card = cards[idx];
const x = ox + ci * (cw + PAD);
const y = oy + ri * (ch + PAD);
const isM = matched.includes(card.i);
const isOpen = card.open || isM;
if (isM) {
MGAPI.roundRect(ctx, x, y, cw, ch, 8,
'rgba(16,185,129,0.15)', '#10b981');
} else if (isOpen) {
MGAPI.roundRect(ctx, x, y, cw, ch, 8,
`${theme.primary}22`, theme.primary);
} else {
MGAPI.roundRect(ctx, x, y, cw, ch, 8,
'rgba(255,255,255,0.04)', 'rgba(255,255,255,0.1)');
}
if (isOpen) {
ctx.font = `${Math.min(cw, ch) * 0.55}px serif`;
ctx.textAlign = 'center';
ctx.fillText(card.e, x + cw / 2, y + ch / 2 + ch * 0.15);
} else {
MGAPI.text(ctx, '?', x + cw / 2, y + ch / 2,
{ size: Math.floor(ch * 0.35), color: 'rgba(255,255,255,0.2)' });
}
}
}
}
raf = requestAnimationFrame(loop);
return {
stop() {
stopped = true;
cancelAnimationFrame(raf);
canvas.removeEventListener('click', onClick);
},
};
}
function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); }
function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); }
return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview };
})();