Files
game1/game/js/tetris.js
T
martijn.diersmann@pm.me 4c1f670e9f tetris klaar
2026-09-26 20:38:55 +02:00

264 lines
6.8 KiB
JavaScript

'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();
}
}
}