edu-boardgame-generator/minigames/puzzle.js

137 lines
4.6 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/puzzle.js
* 🧩 Zahlen-Puzzle — Schiebe die Kacheln in die richtige Reihenfolge! (15-Puzzle)
*/
window.MG_puzzle = (function() {
const ID = 'puzzle';
const EMOJI = '🧩';
const NAME = 'Zahlen-Puzzle';
const DESC = 'Schiebe die Kacheln in die richtige Reihenfolge (18)!';
const CONTROLS = 'Mausklick / Pfeiltasten';
const MULTI = 1;
function run(wrap, W, H, cfg, onDone) {
const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H);
const theme = cfg.theme || { primary: '#14b8a6' };
const SIZE = 3; // 3×3
const PAD = 20;
const cellW = Math.floor((W - PAD * 2) / SIZE);
const cellH = Math.floor((H - PAD * 2 - 30) / SIZE);
const OX = (W - SIZE * cellW) / 2;
const OY = 30 + (H - 30 - SIZE * cellH) / 2;
// Ziel: 1 2 3 / 4 5 6 / 7 8 _
const GOAL = [1,2,3,4,5,6,7,8,0];
let board = [...GOAL];
let moves = 0;
let stopped = false;
let raf, endTimer, solved = false;
// Mischeln (100 zufällige Züge)
function shuffle() {
for (let i = 0; i < 100; i++) {
const ei = board.indexOf(0);
const er = Math.floor(ei / SIZE), ec = ei % SIZE;
const nbr = [];
if (er > 0) nbr.push(ei - SIZE);
if (er < SIZE-1) nbr.push(ei + SIZE);
if (ec > 0) nbr.push(ei - 1);
if (ec < SIZE-1) nbr.push(ei + 1);
const ni = nbr[Math.floor(Math.random() * nbr.length)];
[board[ei], board[ni]] = [board[ni], board[ei]];
}
}
shuffle();
function slideAt(idx) {
if (solved) return;
const ei = board.indexOf(0);
const er = Math.floor(ei / SIZE), ec = ei % SIZE;
const tr = Math.floor(idx / SIZE), tc = idx % SIZE;
const adj = (er === tr && Math.abs(ec - tc) === 1) ||
(ec === tc && Math.abs(er - tr) === 1);
if (!adj) return;
[board[ei], board[idx]] = [board[idx], board[ei]];
moves++;
if (board.join() === GOAL.join()) {
solved = true;
endTimer = setTimeout(() => onDone(true), 900);
}
}
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1));
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
for (let i = 0; i < SIZE * SIZE; i++) {
const c = i % SIZE, r = Math.floor(i / SIZE);
const x = OX + c * cellW, y = OY + r * cellH;
if (mx >= x && mx < x + cellW - 4 && my >= y && my < y + cellH - 4)
slideAt(i);
}
});
const onKey = e => {
const ei = board.indexOf(0);
const map = { ArrowUp: SIZE, ArrowDown: -SIZE, ArrowLeft: 1, ArrowRight: -1 };
const di = map[e.key];
if (di && ei + di >= 0 && ei + di < SIZE * SIZE) {
e.preventDefault();
slideAt(ei + di);
}
};
document.addEventListener('keydown', onKey);
function loop() {
if (stopped) return;
raf = requestAnimationFrame(loop);
ctx.fillStyle = '#050508';
ctx.fillRect(0, 0, W, H);
MGAPI.text(ctx, `🧩 Züge: ${moves}`, W / 2, 18, { size: 12, color: theme.primary });
for (let i = 0; i < SIZE * SIZE; i++) {
const val = board[i];
const c = i % SIZE, r = Math.floor(i / SIZE);
const x = OX + c * cellW, y = OY + r * cellH;
if (val === 0) {
// Leerfeld
MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 8, 'rgba(255,255,255,0.03)', null);
continue;
}
const isCorrect = val === GOAL[i];
MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 10,
isCorrect ? `${theme.primary}33` : 'rgba(255,255,255,0.07)',
isCorrect ? theme.primary : 'rgba(255,255,255,0.15)');
MGAPI.text(ctx, String(val), x + cellW/2 - 2, y + cellH/2 + 2,
{ size: Math.floor(cellH * 0.38), family: "'Fredoka One',cursive",
color: isCorrect ? theme.primary : '#fff' });
}
if (solved)
MGAPI.resultScreen(ctx, W, H, true, `In ${moves} Zügen gelöst!`);
}
raf = requestAnimationFrame(loop);
return {
stop() {
stopped = true;
clearTimeout(endTimer);
cancelAnimationFrame(raf);
document.removeEventListener('keydown', onKey);
},
};
}
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 };
})();