edu-boardgame-generator/minigames/spotdiff.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

202 lines
8.2 KiB
JavaScript
Raw Permalink 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/spotdiff.js
* 🔍 Fehler finden — Finde alle Unterschiede zwischen den zwei Bildern!
*/
window.MG_spotdiff = (function() {
const ID = 'spotdiff';
const EMOJI = '🔍';
const NAME = 'Fehler finden';
const DESC = 'Finde alle Unterschiede zwischen den zwei Bildern!';
const CONTROLS = 'Mausklick';
const MULTI = 1;
// ── Bild-Sets (canvas-gezeichnete Szenen) ────────────────────────────────
// Jede Szene besteht aus drawA(ctx,W,H) und drawB(ctx,W,H),
// plus einer Liste von Unterschied-Hotspots { x, y, r } (relativ, 01)
const SCENES = [
{
name: 'Bauernhof',
drawA(ctx, W, H) {
// Himmel
ctx.fillStyle = '#7dd3fc'; ctx.fillRect(0, 0, W, H * 0.55);
// Gras
ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45);
// Sonne
ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.18, H*0.1, 0, Math.PI*2); ctx.fill();
// Haus
ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28);
ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill();
// Fenster (2)
ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07);
ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07);
// Tür
ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.39, H*0.46, W*0.05, H*0.12);
// Baum (3 Kreise)
ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.38, W*0.06, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.46, W*0.05, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.44, W*0.05, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08);
// Wolke
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(W*0.55, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(W*0.62, H*0.11, W*0.06, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(W*0.69, H*0.14, W*0.05, 0, Math.PI*2); ctx.fill();
},
drawB(ctx, W, H) {
// Himmel (UNTERSCHIED 1: dunkleres Blau)
ctx.fillStyle = '#38bdf8'; ctx.fillRect(0, 0, W, H * 0.55);
// Gras
ctx.fillStyle = '#4ade80'; ctx.fillRect(0, H * 0.55, W, H * 0.45);
// Sonne (UNTERSCHIED 2: weiter oben)
ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(W*0.15, H*0.10, H*0.1, 0, Math.PI*2); ctx.fill();
// Haus
ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.3, H*0.3, W*0.25, H*0.28);
ctx.fillStyle = '#7f1d1d'; ctx.beginPath(); ctx.moveTo(W*0.27,H*0.3); ctx.lineTo(W*0.425,H*0.12); ctx.lineTo(W*0.58,H*0.3); ctx.fill();
// Fenster — nur EINES (UNTERSCHIED 3)
ctx.fillStyle = '#bae6fd'; ctx.fillRect(W*0.34, H*0.38, W*0.06, H*0.07);
ctx.fillStyle = '#f87171'; ctx.fillRect(W*0.45, H*0.38, W*0.06, H*0.07); // rotes Fenster
// Tür
ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.39, H*0.46, W*0.05, H*0.12);
// Baum (kleiner — UNTERSCHIED 4)
ctx.fillStyle = '#16a34a'; ctx.beginPath(); ctx.arc(W*0.75, H*0.42, W*0.04, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#15803d'; ctx.beginPath(); ctx.arc(W*0.72, H*0.48, W*0.035, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#14532d'; ctx.beginPath(); ctx.arc(W*0.79, H*0.46, W*0.035, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#78350f'; ctx.fillRect(W*0.74, H*0.5, W*0.02, H*0.08);
// Wolke fehlt (UNTERSCHIED 5)
},
// Hotspots in Bild B (relativ 0-1)
spots: [
{ x: 0.42, y: 0.12, r: 0.07, label: 'Himmelfarbe' },
{ x: 0.15, y: 0.10, r: 0.08, label: 'Sonne' },
{ x: 0.48, y: 0.41, r: 0.06, label: 'Rotes Fenster' },
{ x: 0.75, y: 0.44, r: 0.07, label: 'Kleinerer Baum' },
{ x: 0.62, y: 0.13, r: 0.08, label: 'Fehlende Wolke' },
],
},
];
function run(wrap, W, H, cfg, onDone) {
const scene = SCENES[0];
const theme = cfg.theme || { primary: '#84cc16' };
// Zwei Canvas nebeneinander
const halfW = Math.floor((W - 8) / 2);
const imgH = H - 64;
const container = document.createElement('div');
container.style.cssText = 'position:relative;width:100%;';
wrap.appendChild(container);
const canvasA = document.createElement('canvas');
canvasA.width = halfW; canvasA.height = imgH;
canvasA.style.cssText = `display:inline-block;border-radius:8px;cursor:default;`;
const canvasB = document.createElement('canvas');
canvasB.width = halfW; canvasB.height = imgH;
canvasB.style.cssText = `display:inline-block;border-radius:8px;cursor:crosshair;margin-left:8px;`;
container.appendChild(canvasA);
container.appendChild(canvasB);
// HUD-Canvas
const hud = document.createElement('canvas');
hud.width = W; hud.height = 48;
hud.style.display = 'block';
container.appendChild(hud);
const ctxA = canvasA.getContext('2d');
const ctxB = canvasB.getContext('2d');
const ctxH = hud.getContext('2d');
scene.drawA(ctxA, halfW, imgH);
scene.drawB(ctxB, halfW, imgH);
let found = new Set();
let marks = []; // { x, y, ok }
let stopped = false;
let endTimer;
function drawMarks() {
scene.drawB(ctxB, halfW, imgH); // neu zeichnen
marks.forEach(m => {
ctxB.strokeStyle = m.ok ? '#22c55e' : '#ef4444';
ctxB.lineWidth = 3;
ctxB.beginPath();
ctxB.arc(m.x, m.y, 18, 0, Math.PI * 2);
ctxB.stroke();
if (m.ok) {
ctxB.fillStyle = 'rgba(34,197,94,0.25)';
ctxB.fill();
}
});
}
function drawHUD() {
ctxH.clearRect(0, 0, W, 48);
MGAPI.text(ctxH, `🔍 ${found.size} / ${scene.spots.length} Unterschiede gefunden`, W/2, 16,
{ size: 13, color: theme.primary });
MGAPI.text(ctxH, 'Klicke auf die Unterschiede im rechten Bild', W/2, 36,
{ size: 11, color: 'rgba(255,255,255,0.4)' });
}
canvasB.addEventListener('click', e => {
if (found.size >= scene.spots.length) return;
const rect = canvasB.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvasB.width / rect.width);
const my = (e.clientY - rect.top) * (canvasB.height / rect.height);
// Nächsten Spot suchen
let hit = null;
scene.spots.forEach((s, i) => {
if (found.has(i)) return;
const sx = s.x * halfW, sy = s.y * imgH;
const d = Math.sqrt((mx - sx) ** 2 + (my - sy) ** 2);
if (d < s.r * Math.min(halfW, imgH)) hit = i;
});
if (hit !== null) {
found.add(hit);
const sx = scene.spots[hit].x * halfW;
const sy = scene.spots[hit].y * imgH;
marks.push({ x: sx, y: sy, ok: true });
// Auch in Bild A markieren
ctxA.strokeStyle = '#22c55e';
ctxA.lineWidth = 3;
ctxA.beginPath(); ctxA.arc(sx, sy, 18, 0, Math.PI * 2); ctxA.stroke();
ctxA.fillStyle = 'rgba(34,197,94,0.2)'; ctxA.fill();
} else {
marks.push({ x: mx, y: my, ok: false });
setTimeout(() => {
marks = marks.filter(m => m.ok);
drawMarks(); drawHUD();
}, 600);
}
drawMarks();
drawHUD();
if (found.size >= scene.spots.length && !endTimer) {
endTimer = setTimeout(() => onDone(true), 900);
// Overlay
ctxH.fillStyle = 'rgba(16,185,129,0.9)';
MGAPI.roundRect(ctxH, W/2-120, 4, 240, 38, 8, 'rgba(16,185,129,0.9)', null);
MGAPI.text(ctxH, '🎉 Alle Unterschiede gefunden!', W/2, 24,
{ size: 14, family: "'Fredoka One',cursive", color: '#fff' });
}
});
drawHUD();
return {
stop() {
stopped = true;
clearTimeout(endTimer);
},
};
}
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 };
})();