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().gameOver}

${t().finalScore(game.score)}

`; overlay.style.display = 'flex'; } function loop(time) { if (game.lastTime === null) game.lastTime = time; const dt = time - game.lastTime; game.lastTime = time; const wasOver = game.over; game.tick(dt); game.draw(); updateStats(); if (game.over && !wasOver) { showGameOver(); } rafId = requestAnimationFrame(loop); } function restart() { const overlay = document.getElementById('arcade-result-overlay'); overlay.style.display = 'none'; overlay.innerHTML = ''; game.reset(); } function bindKeyboard() { window.addEventListener('keydown', (e) => { if (!game) return; if (game.over) { if (e.code === 'Space' || e.code === 'Enter') restart(); return; } switch (e.code) { case 'ArrowLeft': game.move(-1, 0); e.preventDefault(); break; case 'ArrowRight': game.move(1, 0); e.preventDefault(); break; case 'ArrowDown': if (game.move(0, 1)) game.score += 1; e.preventDefault(); break; case 'ArrowUp': game.rotate(); e.preventDefault(); break; case 'Space': game.hardDrop(); e.preventDefault(); break; } }); } function bindTouchControls(root) { const actions = { left: () => game.move(-1, 0), right: () => game.move(1, 0), down: () => { if (game.move(0, 1)) game.score += 1; }, rotate: () => game.rotate(), drop: () => game.hardDrop(), }; root.querySelectorAll('[data-input]').forEach((btn) => { const action = actions[btn.dataset.input]; btn.addEventListener('click', () => { if (game.over) return; action(); }); }); } function renderShell(root) { root.innerHTML = `
${t().score.toUpperCase()} 0
${t().lines.toUpperCase()} 0
${t().level.toUpperCase()} 1
${t().next}

${t().hint}

`; } // Обновляет статичные подписи без пересоздания игры — иначе переключение // языка посреди партии сбрасывало бы прогресс, как уже было с ./tanks. function updateStaticText() { const byId = (id) => document.getElementById(id); if (byId('arcade-crosslinks-label')) byId('arcade-crosslinks-label').textContent = t().crosslinks; if (byId('arcade-score-label')) byId('arcade-score-label').textContent = t().score.toUpperCase(); if (byId('arcade-lines-label')) byId('arcade-lines-label').textContent = t().lines.toUpperCase(); if (byId('arcade-level-label')) byId('arcade-level-label').textContent = t().level.toUpperCase(); if (byId('arcade-next-label')) byId('arcade-next-label').textContent = t().next; if (byId('arcade-hint')) byId('arcade-hint').textContent = t().hint; if (byId('arcade-drop-btn')) byId('arcade-drop-btn').textContent = t().hardDrop; if (game?.over) showGameOver(); } function init() { const root = document.getElementById('arcade-root'); renderShell(root); const canvas = document.getElementById('arcade-canvas'); const nextCanvas = document.getElementById('arcade-next'); game = new BlocksGame(canvas, nextCanvas); bindTouchControls(root.querySelector('.arcade-controls')); root.querySelector('#arcade-result-overlay').addEventListener('click', (e) => { if (e.target.closest('[data-action="again"]')) restart(); }); if (rafId) cancelAnimationFrame(rafId); rafId = requestAnimationFrame(loop); } bindKeyboard(); init(); initLangToggle(updateStaticText);