tetris klaar
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
'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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user