tetris klaar
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
|
||||
/* Centrale configuratie voor het spel */
|
||||
const CONSTANTS = {
|
||||
COLS: 10, // breedte van het bord
|
||||
ROWS: 20, // hoogte van het bord
|
||||
CELL: 30, // pixelgrootte van een cel op het bord
|
||||
PREVIEW_CELL: 18, // pixelgrootte van een cel in de previews
|
||||
PREVIEW_COUNT: 3, // aantal zichtbare volgende blokken
|
||||
COLORS: {
|
||||
I: '#00e5f0',
|
||||
O: '#f7d000',
|
||||
T: '#b14ce0',
|
||||
S: '#35d23d',
|
||||
Z: '#f03c3c',
|
||||
J: '#3c6cf0',
|
||||
L: '#f09a20'
|
||||
},
|
||||
SCORE: {
|
||||
SINGLE: 100, // 1 rij
|
||||
DOUBLE: 300, // 2 rijen
|
||||
TRIPLE: 500, // 3 rijen
|
||||
TETRIS: 800, // 4 rijen
|
||||
SOFT_DROP: 1, // punt per cel bij zachte drop
|
||||
HARD_DROP: 2, // punten per cel bij harde drop
|
||||
LINES_PER_LEVEL: 10 // aantal rijen nodig voor een nieuw level
|
||||
},
|
||||
GRAVITY: [800, 720, 630, 550, 470, 380, 300, 220, 160, 100,
|
||||
80, 70, 60, 50, 40, 30, 20, 15, 10, 10], // ms per zetje per level
|
||||
HIGHSCORE_KEY: 'neo-tetris.highscores',
|
||||
PLAYER_NAME_KEY: 'neo-tetris.playername',
|
||||
MAX_HIGHSCORES: 10
|
||||
};
|
||||
|
||||
/* Punten per aantal tegelijk gewiste rijen (index = aantal rijen) */
|
||||
const SCORE_POINTS = [0, 100, 300, 500, 800];
|
||||
|
||||
/* Definitie van de 7 tetromino's als matrices (1 = gevuld blokje) */
|
||||
const SHAPES = {
|
||||
I: [
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]
|
||||
],
|
||||
O: [
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
],
|
||||
T: [
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
S: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0]
|
||||
],
|
||||
Z: [
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
J: [
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
],
|
||||
L: [
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
};
|
||||
|
||||
const PIECE_TYPES = Object.keys(SHAPES);
|
||||
|
||||
/* Eenvoudige wall-kicks die proberen een blokje langs obstakels te schuiven bij rotatie */
|
||||
const WALL_KICKS = [
|
||||
[ 0, 0],
|
||||
[-1, 0],
|
||||
[ 1, 0],
|
||||
[ 0, -1],
|
||||
[-2, 0],
|
||||
[ 2, 0]
|
||||
];
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
'use strict';
|
||||
|
||||
/* ---------- DOM-referenties ---------- */
|
||||
const boardCanvas = document.getElementById('canvas-board');
|
||||
const holdCanvas = document.getElementById('canvas-hold');
|
||||
const nextCanvas = document.getElementById('canvas-next');
|
||||
|
||||
const elScore = document.getElementById('stat-score');
|
||||
const elLines = document.getElementById('stat-lines');
|
||||
const elLevel = document.getElementById('stat-level');
|
||||
|
||||
const overlay = document.getElementById('overlay');
|
||||
const overlayTitle = document.getElementById('overlay-title');
|
||||
const overlaySub = document.getElementById('overlay-sub');
|
||||
const nameForm = document.getElementById('name-form');
|
||||
const nameInput = document.getElementById('name-input');
|
||||
const nameError = document.getElementById('name-error');
|
||||
const submitBtn = document.getElementById('btn-submit');
|
||||
const btnPrimary = document.getElementById('btn-primary');
|
||||
const btnSecondary = document.getElementById('btn-secondary');
|
||||
|
||||
const highscoreList = document.getElementById('highscore-list');
|
||||
|
||||
const game = new TetrisGame();
|
||||
game.onUpdate = renderUI;
|
||||
|
||||
let gameOverHandled = false;
|
||||
|
||||
/* ---------- highscore-opslag in localStorage ---------- */
|
||||
function loadScores() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CONSTANTS.HIGHSCORE_KEY);
|
||||
const list = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(list) ? list : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveScores(list) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
CONSTANTS.HIGHSCORE_KEY,
|
||||
JSON.stringify(list.slice(0, CONSTANTS.MAX_HIGHSCORES))
|
||||
);
|
||||
} catch (e) {
|
||||
/* opslag niet beschikbaar (bv. private browsing) - negeren */
|
||||
}
|
||||
}
|
||||
|
||||
function qualifies(score) {
|
||||
if (score <= 0) return false;
|
||||
const list = loadScores();
|
||||
if (list.length < CONSTANTS.MAX_HIGHSCORES) return true;
|
||||
return score > list[list.length - 1].score;
|
||||
}
|
||||
|
||||
function recordScore(name, score, lines, level) {
|
||||
const list = loadScores();
|
||||
const entry = {
|
||||
name: name || 'Speler',
|
||||
score,
|
||||
lines,
|
||||
level,
|
||||
date: new Date().toISOString()
|
||||
};
|
||||
list.push(entry);
|
||||
list.sort((a, b) => b.score - a.score || a.date.localeCompare(b.date));
|
||||
saveScores(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderHighscores() {
|
||||
const list = loadScores();
|
||||
highscoreList.innerHTML = '';
|
||||
if (list.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'no-scores';
|
||||
li.textContent = 'Nog geen scores - wees de eerste!';
|
||||
highscoreList.appendChild(li);
|
||||
return;
|
||||
}
|
||||
list.forEach((entry, i) => {
|
||||
const li = document.createElement('li');
|
||||
const rank = document.createElement('span');
|
||||
rank.className = 'hs-rank';
|
||||
rank.textContent = i + 1;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'hs-name';
|
||||
name.textContent = entry.name;
|
||||
const score = document.createElement('span');
|
||||
score.className = 'hs-score';
|
||||
score.textContent = entry.score;
|
||||
li.appendChild(rank);
|
||||
li.appendChild(name);
|
||||
li.appendChild(score);
|
||||
highscoreList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- UI-panelen verversen ---------- */
|
||||
function renderUI() {
|
||||
elScore.textContent = game.score;
|
||||
elLines.textContent = game.lines;
|
||||
elLevel.textContent = game.level;
|
||||
drawHold(holdCanvas, game.hold);
|
||||
drawNext(nextCanvas, game);
|
||||
}
|
||||
|
||||
|
||||
/* ---------- overlays (pauze / game over) ---------- */
|
||||
function setOverlay(title, sub, opts = {}) {
|
||||
overlayTitle.textContent = title;
|
||||
overlaySub.textContent = sub || '';
|
||||
nameForm.classList.toggle('hidden', !opts.showName);
|
||||
btnPrimary.textContent = opts.primaryLabel || 'Opnieuw spelen';
|
||||
btnSecondary.classList.toggle('hidden', !opts.showSecondary);
|
||||
overlay.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideOverlay() {
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
if (game.over) return;
|
||||
game.paused = !game.paused;
|
||||
if (game.paused) {
|
||||
setOverlay('PAUZE', 'Druk op P of "Doorgaan" om verder te spelen.', {
|
||||
showSecondary: true,
|
||||
primaryLabel: 'Opnieuw spelen'
|
||||
});
|
||||
} else {
|
||||
hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
game.reset();
|
||||
game.paused = false;
|
||||
gameOverHandled = false;
|
||||
nameForm.classList.add('hidden');
|
||||
nameError.classList.add('hidden');
|
||||
hideOverlay();
|
||||
renderUI();
|
||||
}
|
||||
|
||||
function onGameOver() {
|
||||
const topTen = qualifies(game.score);
|
||||
if (topTen) {
|
||||
nameInput.value = localStorage.getItem(CONSTANTS.PLAYER_NAME_KEY) || '';
|
||||
setOverlay('GAME OVER', 'Top 10! Voer je naam in:', {
|
||||
showName: true,
|
||||
primaryLabel: 'Opnieuw spelen'
|
||||
});
|
||||
nameInput.focus();
|
||||
} else {
|
||||
setOverlay('GAME OVER', `Score: ${game.score}`, {
|
||||
primaryLabel: 'Opnieuw spelen'
|
||||
});
|
||||
}
|
||||
renderHighscores();
|
||||
}
|
||||
|
||||
function handleSubmitScore() {
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) {
|
||||
nameError.classList.remove('hidden');
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(CONSTANTS.PLAYER_NAME_KEY, name);
|
||||
recordScore(name.slice(0, 12), game.score, game.lines, game.level);
|
||||
nameForm.classList.add('hidden');
|
||||
overlaySub.textContent = 'Score opgeslagen in de highscore-lijst!';
|
||||
renderHighscores();
|
||||
btnPrimary.focus();
|
||||
}
|
||||
|
||||
/* ---------- game-loop ---------- */
|
||||
let lastTime = performance.now();
|
||||
|
||||
function loop(now) {
|
||||
const dt = Math.min((now - lastTime) / 1000, 0.25);
|
||||
lastTime = now;
|
||||
|
||||
game.update(dt * 1000);
|
||||
drawBoard(boardCanvas, game);
|
||||
renderUI();
|
||||
|
||||
if (game.over && !gameOverHandled) {
|
||||
gameOverHandled = true;
|
||||
onGameOver();
|
||||
}
|
||||
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
/* ---------- toetsenbord ---------- */
|
||||
const KEYMAP = {
|
||||
ArrowLeft: { fn: () => game.move(-1), prevent: true },
|
||||
ArrowRight: { fn: () => game.move(1), prevent: true },
|
||||
ArrowDown: { fn: () => game.softDrop(), prevent: true },
|
||||
ArrowUp: { fn: () => game.rotate(1), prevent: true },
|
||||
KeyX: { fn: () => game.rotate(1), prevent: true },
|
||||
KeyZ: { fn: () => game.rotate(-1), prevent: false },
|
||||
Space: { fn: () => game.hardDrop(), prevent: true },
|
||||
KeyC: { fn: () => game.holdPiece(), prevent: false },
|
||||
KeyP: { fn: () => togglePause(), prevent: false },
|
||||
KeyR: { fn: () => restartGame(), prevent: false }
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
// typen in het naamveld: alleen Enter voor opslaan
|
||||
if (event.target === nameInput) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleSubmitScore();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !overlay.classList.contains('hidden')) {
|
||||
if (game.over) restartGame();
|
||||
else if (game.paused) togglePause();
|
||||
return;
|
||||
}
|
||||
|
||||
const binding = KEYMAP[event.code];
|
||||
if (!binding) return;
|
||||
if (binding.prevent) event.preventDefault();
|
||||
binding.fn();
|
||||
});
|
||||
|
||||
/* ---------- knoppen + touch-controls ---------- */
|
||||
btnPrimary.addEventListener('click', () => restartGame());
|
||||
btnSecondary.addEventListener('click', () => {
|
||||
if (game.paused) togglePause();
|
||||
});
|
||||
submitBtn.addEventListener('click', handleSubmitScore);
|
||||
// voorkom dat Enter in het naamveld de pagina ververst (default form-submit)
|
||||
nameForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
handleSubmitScore();
|
||||
});
|
||||
|
||||
const ACTION_MAP = {
|
||||
rotate: () => game.rotate(1),
|
||||
rotateCcw: () => game.rotate(-1),
|
||||
hold: () => game.holdPiece(),
|
||||
hard: () => game.hardDrop(),
|
||||
left: () => game.move(-1),
|
||||
right: () => game.move(1),
|
||||
drop: () => game.softDrop()
|
||||
};
|
||||
|
||||
document.querySelectorAll('.t-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (ACTION_MAP[btn.dataset.action]) ACTION_MAP[btn.dataset.action]();
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- start ---------- */
|
||||
renderUI();
|
||||
renderHighscores();
|
||||
requestAnimationFrame(loop);
|
||||
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
/* Tekenen van een blokje met simpele 3D-schaduwen */
|
||||
function drawCell(ctx, px, py, size, color) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(px, py, size, size);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.22)';
|
||||
ctx.fillRect(px, py, size, size * 0.28);
|
||||
ctx.fillRect(px, py, size * 0.28, size);
|
||||
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.25)';
|
||||
ctx.fillRect(px, py + size * 0.72, size, size * 0.28);
|
||||
ctx.fillRect(px + size * 0.72, py, size * 0.28, size);
|
||||
}
|
||||
|
||||
/* Ghost piece: alleen een omlijning, half transparant */
|
||||
function drawGhostCell(ctx, px, py, size, color) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.45;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = Math.max(2, size * 0.12);
|
||||
ctx.strokeRect(px + 3, py + 3, size - 6, size - 6);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/* Het hoofd-bord */
|
||||
function drawBoard(canvas, game) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const size = CONSTANTS.CELL;
|
||||
const W = CONSTANTS.COLS * size;
|
||||
const H = CONSTANTS.ROWS * size;
|
||||
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// achtergrond met subtiel verloop
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
||||
grad.addColorStop(0, '#121722');
|
||||
grad.addColorStop(1, '#070a11');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// gridlijnen
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let c = 1; c < CONSTANTS.COLS; c++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(c * size, 0);
|
||||
ctx.lineTo(c * size, H);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let r = 1; r < CONSTANTS.ROWS; r++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, r * size);
|
||||
ctx.lineTo(W, r * size);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// vastgezette blokken
|
||||
for (let r = 0; r < CONSTANTS.ROWS; r++) {
|
||||
for (let c = 0; c < CONSTANTS.COLS; c++) {
|
||||
const color = game.board[r][c];
|
||||
if (color) drawCell(ctx, c * size, r * size, size, color);
|
||||
}
|
||||
}
|
||||
|
||||
// flitsende rijen tijdens het wissen
|
||||
if (game.flashLines.length) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.75)';
|
||||
for (const r of game.flashLines) {
|
||||
ctx.fillRect(0, r * size, W, size);
|
||||
}
|
||||
}
|
||||
|
||||
// ghost + actief blok
|
||||
if (game.piece && !game.over) {
|
||||
if (!game.locking) {
|
||||
const ghostY = game.ghostY();
|
||||
if (ghostY !== game.piece.y) {
|
||||
const m = game.piece.matrix;
|
||||
for (let r = 0; r < m.length; r++) {
|
||||
for (let c = 0; c < m[r].length; c++) {
|
||||
if (!m[r][c]) continue;
|
||||
drawGhostCell(ctx, (game.piece.x + c) * size, (ghostY + r) * size, size, game.piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
const m = game.piece.matrix;
|
||||
for (let r = 0; r < m.length; r++) {
|
||||
for (let c = 0; c < m[r].length; c++) {
|
||||
if (!m[r][c]) continue;
|
||||
const py = (game.piece.y + r) * size;
|
||||
if (py < 0) continue; // boven het bord: niet tekenen
|
||||
drawCell(ctx, (game.piece.x + c) * size, py, size, game.piece.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Kleine preview (hold) */
|
||||
function drawHold(canvas, type) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const cell = CONSTANTS.PREVIEW_CELL;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (!type) return;
|
||||
const shape = SHAPES[type];
|
||||
const w = shape[0].length * cell;
|
||||
const h = shape.length * cell;
|
||||
const ox = (canvas.width - w) / 2;
|
||||
const oy = (canvas.height - h) / 2;
|
||||
for (let r = 0; r < shape.length; r++) {
|
||||
for (let c = 0; c < shape[r].length; c++) {
|
||||
if (!shape[r][c]) continue;
|
||||
drawCell(ctx, ox + c * cell, oy + r * cell, cell, CONSTANTS.COLORS[type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Volgende blokken onder elkaar */
|
||||
function drawNext(canvas, game) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const cell = CONSTANTS.PREVIEW_CELL;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const list = game.preview;
|
||||
const band = canvas.height / CONSTANTS.PREVIEW_COUNT;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const type = list[i];
|
||||
const shape = SHAPES[type];
|
||||
const w = shape[0].length * cell;
|
||||
const h = shape.length * cell;
|
||||
const ox = (canvas.width - w) / 2;
|
||||
const oy = i * band + (band - h) / 2;
|
||||
for (let r = 0; r < shape.length; r++) {
|
||||
for (let c = 0; c < shape[r].length; c++) {
|
||||
if (!shape[r][c]) continue;
|
||||
drawCell(ctx, ox + c * cell, oy + r * cell, cell, CONSTANTS.COLORS[type]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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