import { getLang, initLangToggle } from './i18n.js'; const COLS = 10; const ROWS = 20; const CELL = 24; const GRAVITY_START_MS = 800; const GRAVITY_MIN_MS = 100; const SHAPES = { I: [[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]], J: [[1, 0, 0], [1, 1, 1], [0, 0, 0]], L: [[0, 0, 1], [1, 1, 1], [0, 0, 0]], O: [[1, 1], [1, 1]], S: [[0, 1, 1], [1, 1, 0], [0, 0, 0]], T: [[0, 1, 0], [1, 1, 1], [0, 0, 0]], Z: [[1, 1, 0], [0, 1, 1], [0, 0, 0]], }; const COLORS = { I: '#00ff41', O: '#9fa3a0', T: '#39e075', S: '#7cffb2', Z: '#1fae56', J: '#c7c7c7', L: '#0a8f2c', }; const PIECE_TYPES = Object.keys(SHAPES); const STR = { ru: { hint: 'стрелки — двигать, вверх — вращать, пробел — сброс', score: 'счёт', lines: 'линии', level: 'уровень', next: 'дальше', hardDrop: 'сброс', gameOver: 'ИГРА ОКОНЧЕНА', finalScore: (s) => `счёт: ${s}`, again: 'ещё раз', crosslinks: 'ещё игры:', }, en: { hint: 'arrows to move, up to rotate, space to hard-drop', score: 'score', lines: 'lines', level: 'level', next: 'next', hardDrop: 'drop', gameOver: 'GAME OVER', finalScore: (s) => `score: ${s}`, again: 'play again', crosslinks: 'more games:', }, }; function t() { return STR[getLang()]; } function emptyBoard() { return Array.from({ length: ROWS }, () => Array(COLS).fill(null)); } function rotateMatrix(matrix) { const n = matrix.length; const result = Array.from({ length: n }, () => Array(n).fill(0)); for (let y = 0; y < n; y++) { for (let x = 0; x < n; x++) { result[x][n - 1 - y] = matrix[y][x]; } } return result; } function randomType() { return PIECE_TYPES[Math.floor(Math.random() * PIECE_TYPES.length)]; } class BlocksGame { constructor(canvas, nextCanvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.nextCanvas = nextCanvas; this.nextCtx = nextCanvas.getContext('2d'); canvas.width = COLS * CELL; canvas.height = ROWS * CELL; this.reset(); } reset() { this.board = emptyBoard(); this.score = 0; this.lines = 0; this.level = 1; this.over = false; this.nextType = randomType(); this.spawn(); this.dropTimer = 0; this.lastTime = null; } spawn() { const type = this.nextType; this.nextType = randomType(); const matrix = SHAPES[type].map((row) => row.slice()); this.piece = { type, matrix, x: Math.floor((COLS - matrix.length) / 2), y: 0, }; if (this.collides(this.piece.matrix, this.piece.x, this.piece.y)) { this.over = true; } } collides(matrix, offX, offY) { for (let y = 0; y < matrix.length; y++) { for (let x = 0; x < matrix[y].length; x++) { if (!matrix[y][x]) continue; const bx = offX + x; const by = offY + y; if (bx < 0 || bx >= COLS || by >= ROWS) return true; if (by >= 0 && this.board[by][bx]) return true; } } return false; } move(dx, dy) { if (this.over) return false; const p = this.piece; if (!this.collides(p.matrix, p.x + dx, p.y + dy)) { p.x += dx; p.y += dy; return true; } return false; } rotate() { if (this.over) return; const p = this.piece; const rotated = rotateMatrix(p.matrix); const kicks = [0, 1, -1, 2, -2]; for (const kick of kicks) { if (!this.collides(rotated, p.x + kick, p.y)) { p.matrix = rotated; p.x += kick; return; } } } hardDrop() { if (this.over) return; while (this.move(0, 1)) { this.score += 1; } this.lock(); } lock() { const p = this.piece; for (let y = 0; y < p.matrix.length; y++) { for (let x = 0; x < p.matrix[y].length; x++) { if (p.matrix[y][x]) { const by = p.y + y; const bx = p.x + x; if (by >= 0) this.board[by][bx] = p.type; } } } this.clearLines(); this.spawn(); } clearLines() { let cleared = 0; this.board = this.board.filter((row) => { const full = row.every((cell) => cell); if (full) cleared++; return !full; }); while (this.board.length < ROWS) { this.board.unshift(Array(COLS).fill(null)); } if (cleared > 0) { const points = [0, 100, 300, 500, 800][cleared] || 800; this.score += points * this.level; this.lines += cleared; this.level = 1 + Math.floor(this.lines / 10); } } gravityInterval() { return Math.max(GRAVITY_MIN_MS, GRAVITY_START_MS - (this.level - 1) * 60); } tick(dtMs) { if (this.over) return; this.dropTimer += dtMs; const interval = this.gravityInterval(); if (this.dropTimer >= interval) { this.dropTimer = 0; if (!this.move(0, 1)) { this.lock(); } } } drawCell(ctx, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x * CELL + 1, y * CELL + 1, CELL - 2, CELL - 2); ctx.strokeStyle = '#0a0e0c'; ctx.lineWidth = 1; ctx.strokeRect(x * CELL + 1, y * CELL + 1, CELL - 2, CELL - 2); } draw() { const ctx = this.ctx; ctx.fillStyle = '#0a0e0c'; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); for (let y = 0; y < ROWS; y++) { for (let x = 0; x < COLS; x++) { if (this.board[y][x]) { this.drawCell(ctx, x, y, COLORS[this.board[y][x]]); } } } const p = this.piece; if (p) { for (let y = 0; y < p.matrix.length; y++) { for (let x = 0; x < p.matrix[y].length; x++) { if (p.matrix[y][x] && p.y + y >= 0) { this.drawCell(ctx, p.x + x, p.y + y, COLORS[p.type]); } } } } // сетка поверх, лёгкая ctx.strokeStyle = 'rgba(80, 255, 140, 0.06)'; for (let x = 1; x < COLS; x++) { ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, this.canvas.height); ctx.stroke(); } for (let y = 1; y < ROWS; y++) { ctx.beginPath(); ctx.moveTo(0, y * CELL); ctx.lineTo(this.canvas.width, y * CELL); ctx.stroke(); } this.drawNext(); } drawNext() { const ctx = this.nextCtx; const size = this.nextCanvas.width; ctx.fillStyle = '#0f1512'; ctx.fillRect(0, 0, size, size); const matrix = SHAPES[this.nextType]; const cell = Math.floor(size / 4); const offset = (4 - matrix.length) / 2; for (let y = 0; y < matrix.length; y++) { for (let x = 0; x < matrix[y].length; x++) { if (matrix[y][x]) { ctx.fillStyle = COLORS[this.nextType]; ctx.fillRect((x + offset) * cell + 1, (y + offset) * cell + 1, cell - 2, cell - 2); } } } } } let game = null; let rafId = null; function updateStats() { document.getElementById('arcade-score').textContent = game.score; document.getElementById('arcade-lines').textContent = game.lines; document.getElementById('arcade-level').textContent = game.level; } function showGameOver() { const overlay = document.getElementById('arcade-result-overlay'); overlay.innerHTML = `
${t().finalScore(game.score)}
${t().crosslinks} ./tanks · ./invaders
${t().hint}