'use strict'; /* Helpers */ function cloneMatrix(matrix) { return matrix.map((row) => row.slice()); } /* * Roteert een matrix 90 graden. * dir = 1 : rechtsom (met de klok mee) * dir = -1 : linksom (tegen de klok in) */ function rotateMatrix(matrix, dir) { const n = matrix.length; const m = matrix[0].length; const out = Array.from({ length: m }, () => Array(n).fill(0)); for (let r = 0; r < n; r++) { for (let c = 0; c < m; c++) { if (!matrix[r][c]) continue; if (dir > 0) out[c][n - 1 - r] = 1; else out[m - 1 - c][r] = 1; } } return out; } /** * Volledige Tetris-spelstaat en -regels. * De UI hangt een onUpdate-callback op voor het verversen van de panelen. */ class TetrisGame { constructor() { this.onUpdate = null; this.reset(); } notify() { if (this.onUpdate) this.onUpdate(); } reset() { this.board = Array.from({ length: CONSTANTS.ROWS }, () => Array(CONSTANTS.COLS).fill(null) ); this.queue = []; // komende bloktypes (7-bag) this.hold = null; // vastgehouden bloktype this.canHold = true; // mag er een keer vastgehouden worden? this.piece = null; // actief blok this.score = 0; this.lines = 0; this.level = 1; this.over = false; this.paused = false; this.locking = false; // true tijdens wis-animatie this.flashLines = []; // rijen die op dit moment flitsen this.dropTimer = 0; this.dropInterval = this.gravity(); this.spawn(); } /* ---------- 7-bag generator: elk blok komt 1x per zak voor ---------- */ generateBag() { const bag = PIECE_TYPES.slice(); for (let i = bag.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [bag[i], bag[j]] = [bag[j], bag[i]]; } this.queue.push(...bag); } nextType() { while (this.queue.length === 0) this.generateBag(); return this.queue.shift(); } get preview() { return this.queue.slice(0, CONSTANTS.PREVIEW_COUNT); } /* ---------- spawn / plaatsing ---------- */ placePiece(type) { const shape = cloneMatrix(SHAPES[type]); this.piece = { type, matrix: shape, x: Math.floor((CONSTANTS.COLS - shape[0].length) / 2), y: 0, color: CONSTANTS.COLORS[type] }; if (this.collides(this.piece.matrix, this.piece.x, this.piece.y)) { this.over = true; // bord is te vol: game over } } spawn() { const type = this.nextType(); this.placePiece(type); this.canHold = true; this.dropTimer = 0; this.dropInterval = this.gravity(); } gravity() { return CONSTANTS.GRAVITY[Math.min(this.level - 1, CONSTANTS.GRAVITY.length - 1)]; } /* ---------- botsing / beweging ---------- */ collides(matrix, x, y) { for (let r = 0; r < matrix.length; r++) { for (let c = 0; c < matrix[r].length; c++) { if (!matrix[r][c]) continue; const bx = x + c; const by = y + r; if (bx < 0 || bx >= CONSTANTS.COLS || by >= CONSTANTS.ROWS) return true; if (by >= 0 && this.board[by][bx]) return true; } } return false; } ghostY() { let y = this.piece.y; while (!this.collides(this.piece.matrix, this.piece.x, y + 1)) y++; return y; } move(dx) { if (this.over || this.paused || this.locking) return false; if (!this.collides(this.piece.matrix, this.piece.x + dx, this.piece.y)) { this.piece.x += dx; return true; } return false; } rotate(dir) { if (this.over || this.paused || this.locking) return false; const rotated = rotateMatrix(this.piece.matrix, dir); for (const [xOff, yOff] of WALL_KICKS) { if (!this.collides(rotated, this.piece.x + xOff, this.piece.y + yOff)) { this.piece.matrix = rotated; this.piece.x += xOff; this.piece.y += yOff; return true; } } return false; } stepDown() { if (this.collides(this.piece.matrix, this.piece.x, this.piece.y + 1)) { this.lock(); return false; // kon niet verder: blok is vastgezet } this.piece.y++; return true; } softDrop() { if (this.over || this.paused || this.locking) return; if (this.stepDown()) this.addScore(CONSTANTS.SCORE.SOFT_DROP); } hardDrop() { if (this.over || this.paused || this.locking) return; let dropped = 0; while (this.stepDown()) dropped++; this.addScore(dropped * CONSTANTS.SCORE.HARD_DROP); } holdPiece() { if (this.over || this.paused || this.locking || !this.canHold) return; const current = this.piece.type; this.canHold = false; if (this.hold) { const swap = this.hold; this.hold = current; this.placePiece(swap); } else { this.hold = current; this.spawn(); this.canHold = false; // spawn() zet dit weer op true; hier terugzetten } this.dropTimer = 0; this.notify(); } addScore(pts) { this.score += pts; this.notify(); } /* ---------- vastzetten + rijen wissen ---------- */ lock() { const { matrix, x, y, color } = this.piece; for (let r = 0; r < matrix.length; r++) { for (let c = 0; c < matrix[r].length; c++) { if (!matrix[r][c]) continue; const by = y + r; if (by < 0) { this.over = true; // blok steekt te ver boven het bord uit return; } this.board[by][x + c] = color; } } const fullRows = []; for (let r = 0; r < CONSTANTS.ROWS; r++) { if (this.board[r].every((cell) => cell !== null)) fullRows.push(r); } if (fullRows.length === 0) { this.spawn(); this.notify(); return; } // wis-animatie: kort flitsen, daarna pas de rijen daadwerkelijk verwijderen this.locking = true; this.flashLines = fullRows.slice(); const cleared = fullRows.length; const rowsToRemove = fullRows.slice(); setTimeout(() => { this.locking = false; this.flashLines = []; const kept = this.board.filter((row, r) => !rowsToRemove.includes(r)); while (kept.length < CONSTANTS.ROWS) { kept.unshift(Array(CONSTANTS.COLS).fill(null)); } this.board = kept; this.lines += cleared; this.addScore(SCORE_POINTS[cleared] * this.level); const newLevel = Math.floor(this.lines / CONSTANTS.SCORE.LINES_PER_LEVEL) + 1; if (newLevel !== this.level) { this.level = newLevel; this.dropInterval = this.gravity(); } if (!this.over) this.spawn(); this.notify(); }, 140); this.notify(); } /* ---------- updates door de game-loop (delta-tijd in ms) ---------- */ update(dtMs) { if (this.over || this.paused || this.locking) return; this.dropTimer += dtMs; while (this.dropTimer >= this.dropInterval) { this.dropTimer -= this.dropInterval; if (this.locking || this.over) return; this.stepDown(); } } }