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