- 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
142 lines
4.7 KiB
JavaScript
142 lines
4.7 KiB
JavaScript
/**
|
||
* 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 0–1)
|
||
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 };
|
||
|
||
})();
|