137 lines
4.6 KiB
JavaScript
137 lines
4.6 KiB
JavaScript
/**
|
||
* 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 (1–8)!';
|
||
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 };
|
||
|
||
})();
|