Работа мечты в один клик 💼

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!
💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.
Как получить оффер? 📌 Зарегистрируйся 📌 Пройди AI-интервью 📌 Получи обратную связь сразу же!
HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀
Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp
Тетрис на JavaScript + изучение современных возможностей языка
Лучший способ узнавать новое и закреплять полученные знания – практика. Лучшая практика в программировании – создание игр. Лучшая игра в мире – Тетрис. Сегодня мы будем узнавать новое в процессе написания тетриса на javaScript.

В конце руководства у нас будет полностью функционирующая игра с уровнями сложности и системой очков. По ходу дела разберемся с важными игровыми концепциями, вроде графики и игрового цикла, а также научимся определять коллизии блоков и изучим возможности современного JavaScript (ES6):
Весь код проекта вы можете найти в github-репозитории.
Тетрис
Эта всемирно известная игра появилась в далеком 1984 году. Придумал ее русский программист Алексей Пажитнов. Правила очень просты и известны каждому. Сверху вниз падают фигурки разной формы, которые можно вращать и перемещать. Игрок должен складывать их внизу игрового поля. Если получается заполнить целый ряд, он пропадает. Игра заканчивается, когда башня из фигурок достигает верха игрового поля.
Тетрис – великолепный выбор для первого знакомства с гейм-разработкой. Он достаточно прост для программирования, но в то же время содержит все принципиальные игровые элементы. К тому же в нем максимально простая графика.
Структура проекта
Для удобства разобьем весь проект на отдельные файлы:

constants.js
– конфигурация и правила игры;board.js
- логика игрового поля:piece.js
– управление фрагментами-тетрамино;main.js
– инициализация и управление всей игрой;index.html
– веб-страница с html-кодом проекта и подключением ресурсов;styles.css
- стили для оформления игры;README.md
.
Сразу же подключим все нужное в index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Tetris</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<script type="text/javascript" src="constants.js"></script>
<script type="text/javascript" src="board.js"></script>
<script type="text/javascript" src="piece.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
Создание каркаса
Игровое поле состоит из 10 колонок и 20 рядов. Эти значения будут часто использоваться, поэтому мы сделаем их константами и вынесем в отдельный файл constants.js
. В этом файле также укажем размер одного блока.
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
Для отрисовки графики будем использовать холст – элемент HTML5 canvas. Добавим его в html-файл с инфраструктурой будущей игры:
<div class="grid">
<canvas id="board" class="game-board"></canvas>
<div class="right-column">
<div>
<h1>TETRIS</h1>
<p>Score: <span id="score">0</span></p>
<p>Lines: <span id="lines">0</span></p>
<p>Level: <span id="level">0</span></p>
<canvas id="next" class="next"></canvas>
</div>
<button onclick="play()" class="play-button">Play</button>
</div>
</div>
Теперь в главном скрипте проекта main.js
нужно найти элемент холста и получить контекст 2D для рисования:
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
// Устанавливаем размеры холста
ctx.canvas.width = COLS * BLOCK_SIZE;
ctx.canvas.height = ROWS * BLOCK_SIZE;
// Устанавливаем масштаб
ctx.scale(BLOCK_SIZE, BLOCK_SIZE);
Здесь мы используем установленные ранее константные значения.
Метод scale используется, чтобы избежать постоянного умножения всех значений на BLOCK_SIZE
и упростить код.
Оформление
Для оформления такой ретро-игры идеально подходит пиксельный стиль, поэтому мы будем использовать шрифт Press Start 2P. Подключите его в секции head
:
<link
href="https://fonts.googleapis.com/css?family=Press+Start+2P"
rel="stylesheet"
/>
Теперь добавим основные стили в style.css
:
* {
font-family: 'Press Start 2P', cursive;
}
.grid {
display: grid;
grid-template-columns: 320px 200px;
}
.right-column {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.game-board {
border: solid 2px;
}
.play-button {
background-color: #4caf50;
font-size: 16px;
padding: 15px 30px;
cursor: pointer;
}
Для разметки используются системы CSS Grid и Flexbox.
Вот, что у нас получилось:

Игровое поле
Поле состоит из клеточек, у которых есть два состояния: занята и свободна. Можно было бы просто представить клетку булевым значением, но мы собираемся раскрасить каждую фигурку в свой цвет. Лучше использовать числа: пустая клетка – 0, а занятая – от 1 до 7, в зависимости от цвета.
Само поле будет представлено в виде двумерного массива (матрицы). Каждый ряд – массив клеток, а массив рядов – это, собственно, поле.
Создадим отдельный класс для представления игрового поля – Board
и разместим его в файле board.js
. Сразу же добавим пару важных методов:
class Board {
constructor() {
this.piece = null;
}
// Сбрасывает игровое поле перед началом новой игры
reset() {
this.grid = this.getEmptyBoard();
}
// Создает матрицу нужного размера, заполненную нулями
getEmptyBoard() {
return Array.from(
{length: ROWS}, () => Array(COLS).fill(0)
);
}
}
Для создания пустой матрицы поля и заполнения ее нулями используются методы массивов: Array.from() и Array.fill().
Теперь создадим экземпляр класса Board
в основном файле игры.
Функция play
будет вызвана при нажатии на кнопку Play. Она очистит игровое поле с помощью метода reset
:
let board = new Board();
function play() {
board.reset();
// наглядное представление матрицы игрового поля
console.table(board.grid);
}
Для наглядного представления матрицы удобно использовать метод console.table:

Тетрамино
Каждая фигурка в тетрисе состоит из четырех блоков и называется тетрамино. Всего комбинаций семь – дадим каждой из них имя (I, J, L, O, S, T, Z) и свой цвет:

Для удобства вращения каждое тетрамино будет представлено в виде квадратной матрицы 3х3. Например, J-тетрамино выглядит так:
[2, 0, 0],
[2, 2, 2],
[0, 0, 0];
Для представления I-тетрамино потребуется матрица 4x4.
Заведем отдельный класс Piece
для фигурок, чтобы отслеживать их положение на доске, а также хранить цвет и форму. Чтобы фигурки могли отрисовывать себя на поле, нужно передать им контекст рисования:
class Piece {
constructor(ctx) {
this.ctx = ctx;
this.color = 'blue';
this.shape = [
[2, 0, 0],
[2, 2, 2],
[0, 0, 0]
];
// Начальная позиция
this.x = 3;
this.y = 0;
}
}
Блок фигурки занимает несколько клеток на поле, но раскрашены должны быть только те, значение которых больше 0
. Для отрисовки нужно перебрать все ячейки блока в двойном цикле:
draw() {
this.ctx.fillStyle = this.color;
this.shape.forEach((row, y) => {
row.forEach((value, x) => {
// this.x, this.y - левый верхний угол фигурки на игровом поле
// x, y - координаты ячейки относительно матрицы фигурки (3х3)
// this.x + x - координаты ячейки на игровом поле
if (value > 0) {
this.ctx.fillRect(this.x + x, this.y + y, 1, 1);
}
});
});
}
Итак, нарисуем первое тетрамино на поле:
function play() {
board.reset();
let piece = new Piece(ctx);
piece.draw();
board.piece = piece;
}
Активная фигурка сохраняется в свойстве board.piece
для удобного доступа.

Управление с клавиатуры
Передвигать фигурки по полю (влево, вправо и вниз) можно с помощью клавиш-стрелок.
Добавим в класс Piece
метод move
, которые будет изменять текущие координаты тетрамино на поле.
move(p) {
this.x = p.x;
this.y = p.y;
}
Перечисления
Коды клавиш будут храниться в файле constants.js
. Для этого удобно применять специальный тип данных – перечисление (enum). В JavaScript нет встроенных перечислений, поэтому мы воспользуемся обычным объектом:
const KEY = {
LEFT: 37,
RIGHT: 39,
DOWN: 40
}
Object.freeze(KEY);
Ключевое слово const
не работает с полями объектов, а лишь запрещает переприсваивание данных в переменную KEY
. Чтобы сделать константы в объекте неизменяемыми, мы используем метод Object.freeze(). При этом важно помнить две вещи:
- Для правильной работы этого метода нужен строгий режим выполнения;
- Метод работает только на один уровень вложенности.
Вычисляемые имена свойств
Теперь нужно сопоставить коды клавиш и действия, которые следует выполнить при их нажатии.
ES6 позволяет добавлять в объекты свойства с вычисляемыми именами. Другими словами, в имени свойства можно использовать переменные и даже выражения.
Для установки такого свойства нужны квадратные скобки:
const X = 'x';
const a = { [X]: 5 };
console.log(a.x); // 5
Для перемещения тетрамино мы будем стирать старое отображение и копировать его в новых координатах. Чтобы получить эти новые координаты, сначала скопируем текущие, а затем изменим нужную (x
или y
) на единицу.
Так как координаты являются примитивными значениями, мы можем использовать spread-оператор, чтобы перенести их в новый объект. В ES6 существует еще один механизм копирования: Object.assign().
const moves = {
[KEY.LEFT]: p => ({ ...p, x: p.x - 1 }),
[KEY.RIGHT]: p => ({ ...p, x: p.x + 1 }),
[KEY.DOWN]: p => ({ ...p, y: p.y + 1 })
};
В объекте moves
теперь хранятся функции вычисления новых координат для каждой клавиши. Получить их можно так:
const p = this.moves[event.key](this.piece);
Очень важно, что при этом не меняются текущие координаты самого тетрамино, так как нажатие клавиши не всегда будет приводить к реальному изменению положения.
Теперь добавим обработчик для события keydown:
document.addEventListener('keydown', event => {
if (moves[event.keyCode]) {
// отмена действий по умолчанию
event.preventDefault();
// получение новых координат фигурки
let p = moves[event.keyCode](board.piece);
// проверка нового положения
if (board.valid(p)) {
// реальное перемещение фигурки, если новое положение допустимо
board.piece.move(p);
// стирание старого отображения фигуры на холсте
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
board.piece.draw();
}
}
});
Метод board.valid()
будет реализован в следующем разделе. Его задача – определять допустимость новых координат на игровом поле.
valid(p) {
return true;
}

Обнаружение столкновений
Если бы фигурки тетриса могли проходить сквозь друг друга, а также сквозь пол и стены игрового поля, игра не имела бы смысла. Важно проверить возможные столкновения элементов перед изменением их положения.
Возможные столкновения одного тетрамино:
- с полом при движении вниз;
- со стенками игрового поля при движении вправо или влево;
- с другими тетрамино, уже размещенными на поле.
Фигурки можно будет вращать, поэтому при вращении тоже нужно учитывать возможные столкновения.
Мы уже умеем вычислять новую позицию фигурки на поле при нажатии клавиш-стрелок. Теперь нужно добавить проверку на ее допустимость. Для этого мы должны проверить все клетки, которые будет занимать тетрамино в новом положении.
Для такой проверки удобно использовать метод массива every(). Для каждой клетки в матрице тетрамино нужно определить абсолютные координаты на игровом поле, а затем проверить, свободно ли это место и не выходит ли оно за границы поля.
insideWalls(x) {
return x >= 0 && x < COLS;
}
aboveFloor(y) {
return y <= ROWS;
}
// не занята ли клетка поля другими фигурками
notOccupied(x, y) {
return this.grid[y] && this.grid[y][x] === 0;
}
valid(p) {
return p.shape.every((row, dy) => {
return row.every((value, dx) => {
let x = p.x + dx;
let y = p.y + dy;
return value === 0 ||
(this.insideWalls(x) && this.aboveFloor(y) && this.notOccupied(x, y));
});
});
}
Пустые клетки матрицы тетрамино при этом не учитываются.
Если проверка прошла удачно, передвигаем фигурку в новое место.
if (this.valid(p)) {
this.piece.move(p);
}

Теперь мы можем добавить возможность ускоренного падения (hard drop) фигурок при нажатии на пробел. Тетрамино при этом будет падать пока не столкнется с чем-нибудь.
const KEY = {
SPACE: 32,
// ...
}
moves = {
[KEY.SPACE]: p => ({ ...p, y: p.y + 1 })
// ...
};
// В обработчике события keydown
if (event.keyCode === KEY.SPACE) {
// Жесткое падение
while (board.valid(p)) {
board.piece.move(p);
// стирание старого отображения фигуры на холсте
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
board.piece.draw();
p = moves[KEY.DOWN](board.piece);
}
} else if (board.valid(p)) {
// ...
}
Вращение
Фигурки можно вращать относительно их «центра масс»:

Чтобы реализовать такую возможность, нам понадобятся базовые знания линейной алгебры. Мы должны транспонировать матрицу, а затем умножить ее на матрицу преобразования, которая изменит порядок столбцов.

На JavaScript это выглядит так:
// Транспонирование матрицы тетрамино
for (let y = 0; y < p.shape.length; ++y) {
for (let x = 0; x < y; ++x) {
[p.shape[x][y], p.shape[y][x]] =
[p.shape[y][x], p.shape[x][y]];
}
}
// Изменение порядка колонок
p.shape.forEach(row => row.reverse());
Эту функцию можно использовать для вращения фигурок, но перед началом манипуляций с матрицей, ее нужно скопировать, чтобы не допускать мутаций. Вместо spread-оператора, который работает лишь на один уровень в глубину, мы используем трюк с сериализацией – превратим матрицу в JSON-строку, а затем распарсим ее.
rotate(p){
// Клонирование матрицы
let clone = JSON.parse(JSON.stringify(p));
// алгоритм вращения
return clone;
}
Теперь при нажатии на клавишу Вверх, активная фигурка будет вращаться:
const KEY = {
UP: 38,
// ...
}
const moves = {
[KEY.UP]: (p) => board.rotate(p),
// ...
}

Случайный выбор фигурок
Чтобы каждый раз появлялись разные фигурки, придется реализовать рандомизацию, следуя стандарту SRS (Super Rotation System).
Добавим цвета и формы фигурок в файл constants.js:
const COLORS = [
'cyan',
'blue',
'orange',
'yellow',
'green',
'purple',
'red'
];
const SHAPES = [
[[0, 0, 0, 0], [1, 1, 1, 1], [0, 0, 0, 0], [0, 0, 0, 0]],
[[2, 0, 0], [2, 2, 2], [0, 0, 0]],
[[0, 0, 3], [3, 3, 3], [0, 0, 0]],
[[4, 4], [4, 4]],
[[0, 5, 5], [5, 5, 0], [0, 0, 0]],
[[0, 6, 0], [6, 6, 6], [0, 0, 0]],
[[7, 7, 0], [0, 7, 7], [0, 0, 0]]
];
Теперь нужно случайным образом выбрать порядковый номер тетрамино:
// параметр noOfTypes - количество вариантов
randomizeTetrominoType(noOfTypes) {
return Math.floor(Math.random() * noOfTypes);
}
На этом этапе мы можем выбирать тип фигурки случайным образом при создании.
Добавим в класс Piece
метод spawn
:
constructor(ctx) {
this.ctx = ctx;
this.spawn();
}
spawn() {
this.typeId = this.randomizeTetrominoType(COLORS.length - 1);
this.shape = SHAPES[this.typeId];
this.color = COLORS[this.typeId];
this.x = 0;
this.y = 0;
}
// расположить фигурку в центре поля
setStartPosition() {
this.x = this.typeId === 4 ? 4 : 3;
}

Игровой цикл
Почти во всех играх есть одна главная функция, которая постоянно делает что-то, даже если игрок пассивен – это игровой цикл. Нам он тоже понадобится, чтобы фигурки постоянно генерировались и падали сверху вниз на игровом поле.
RequestAnimationFrame
Для совершения циклических действий удобно использовать метод requestAnimationFrame. Он сообщает браузеру о том, что нужно сделать, а браузер выполняет это во время следующей перерисовки экрана.
Мы создадим функцию animate
, которая будет производить все необходимые перерисовки для одного цикла анимации, а затем рекурсивно вызовет сама себя для выполнения перерисовок следующего цикла. Самый первый вызов функции animate
будет происходить внутри функции play
.
animate() {
board.piece.draw();
requestAnimationFrame(this.animate.bind(this));
}
Таймер
Нам также потребуется таймер, чтобы в каждом фрейме анимации "ронять" активное тетрамино вниз. Возьмем готовый пример с MDN и немного модифицируем его.
Для начала создадим объект для хранения нужной информации:
const time = { start: 0, elapsed: 0, level: 1000 };
В цикле мы будем обновлять это состояние и отрисовывать текущее отображение:
function animate(now = 0) {
// обновить истекшее время
time.elapsed = now - time.start;
// если время отображения текущего фрейма прошло
if (time.elapsed > time.level) {
// начать отсчет сначала
time.start = now;
// "уронить" активную фигурку
board.drop();
}
// очистить холст для отрисовки нового фрейма
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// отрисовать игровое поле
board.draw();
requestAnimationFrame(animate);
}
function play() {
board.reset();
let piece = new Piece(ctx);
board.piece = piece;
board.piece.setStartPosition();
animate();
}
draw() {
this.piece.draw();
}
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
}
}

Заморозка состояния
При достижении активной фигуркой низа игрового поля, ее нужно «заморозить» в текущем положении и создать новое активное тетрамино.
Для этого добавим классу Board
метод freeze
. Он будет сохранять положение фигурки в матрице игрового поля:
freeze() {
this.piece.shape.forEach((row, y) => {
row.forEach((value, x) => {
if (value > 0) {
this.grid[y + this.piece.y][x + this.piece.x] = value;
}
});
});
}
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
} else {
this.freeze();
console.table(this.grid);
this.piece = new Piece(this.ctx);
this.piece.setStartPosition();
}
}
Теперь при достижении фигуркой низа поля, мы увидим в консоли, что матрица самого поля изменилась:

Добавим метод для отрисовки целого поля (с уже «замороженными» тетрамино):
constructor(ctx) {
this.ctx = ctx;
this.piece = null;
}
draw() {
this.piece.draw();
this.drawBoard();
}
drawBoard() {
this.grid.forEach((row, y) => {
row.forEach((value, x) => {
if (value > 0) {
this.ctx.fillStyle = COLORS[value];
this.ctx.fillRect(x, y, 1, 1);
}
});
});
}
Обратите внимание, что теперь объекту игрового поля тоже нужен контекст рисования, не забудьте передать его:
let board = new Board(ctx);

Очистка линий
Главная задача игры – собирать из блоков целые ряды, которые должны пропадать с поля, освобождая место для новых фигурок.

Добавим в класс Board метод для проверки, не собрана ли целая линия, которую можно удалить, и удаления всех таких линий:
clearLines() {
let lines = 0;
this.grid.forEach((row, y) => {
// Если все клетки в ряду заполнены
if (row.every(value => value > 0)) {
lines++;
// Удалить этот ряд
this.grid.splice(y, 1);
// Добавить наверх поля новый пустой ряд клеток
this.grid.unshift(Array(COLS).fill(0));
}
});
}
Его нужно вызывать каждый раз после «заморозки» активного тетрамино при достижении низа игрового поля:
drop() {
// ...
this.freeze();
this.clearLines();
// ...
}

Система баллов
Чтобы сделать игру еще интереснее, нужно добавить баллы за сбор целых рядов.
const POINTS = {
SINGLE: 100,
DOUBLE: 300,
TRIPLE: 500,
TETRIS: 800,
SOFT_DROP: 1,
HARD_DROP: 2
}
Object.freeze(POINTS);
Чем больше рядов собрано за один цикл, тем больше будет начислено очков.
Для хранения информации создадим новый объект accountValues
.
let accountValues = {
score: 0,
}
При каждом изменении счета нужно обновлять данные на экране. Для этого мы обратимся к возможностям метапрограммирования в JavaScript – Proxy.
Прокси позволяет отслеживать обращение к свойствам объекта, например, для их чтения (get) или обновления (set) и реализовывать собственную логику:
// Обновление данных на экране
function updateAccount(key, value) {
let element = document.getElementById(key);
if (element) {
element.textContent = value;
}
}
// Проксирование доступа к свойствам accountValues
let account = new Proxy(accountValues, {
set: (target, key, value) => {
target[key] = value;
updateAccount(key, value);
return true;
}
});
Теперь при каждом изменении свойств объекта account
будет вызываться функция updateAccount
.
Добавим логику начисления очков в обработчик события keydown
:
if (event.keyCode === KEY.SPACE) {
while (board.valid(p)) {
account.score += POINTS.HARD_DROP;
board.piece.move(p);
p = moves[KEY.DOWN](board.piece);
}
} else if (board.valid(p)) {
board.piece.move(p);
if (event.keyCode === KEY.DOWN) {
account.score += POINTS.SOFT_DROP;
}
}
и в метод очистки собранных рядов:
getLineClearPoints(lines) {
return lines === 1 ? POINTS.SINGLE :
lines === 2 ? POINTS.DOUBLE :
lines === 3 ? POINTS.TRIPLE :
lines === 4 ? POINTS.TETRIS :
0;
}
clearLines() {
// ...
if (lines > 0) {
// Добавить очки за собранные линии
account.score += this.getLineClearPoints(lines);
}
}

Уровни
Чем лучше вы играете в тетрис, тем быстрее должны падать фигурки, чтобы вам не стало скучно. Придется добавить уровни сложности в нашу игру, постепенно увеличивая частоту фреймов игрового цикла.
const LINES_PER_LEVEL = 10;
const LEVEL = {
0: 800,
1: 720,
2: 630,
3: 550,
// ...
}
Object.freeze(LEVEL);
Информация на экране о количестве собранных линий и текущем уровне должна обновляться при каждом изменении. Для этого добавим два новых поля (lines
и levels
) в объект accountValues
.
let accountValues = {
score: 0,
lines: 0,
level: 0
}
Они будут обновляться по той же схеме, что и score
, с помощью прокси-объекта.
Напишем отдельную функцию resetGame, в которую поместим всю логику для начала новой игры:
function resetGame() {
account.score = 0;
account.lines = 0;
account.level = 0;
board.reset();
let piece = new Piece(ctx);
board.piece = piece;
board.piece.setStartPosition();
}
function play() {
resetGame();
animate();
}
Теперь нужно немного обновить логику начисления очков за собранные линии. С каждым уровнем очков должно быть больше.
getLinesClearedPoints(lines, level) {
const lineClearPoints =
lines === 1
? POINTS.SINGLE
: lines === 2
? POINTS.DOUBLE
: lines === 3
? POINTS.TRIPLE
: lines === 4
? POINTS.TETRIS
: 0;
return (level + 1) * lineClearPoints;
}
clearLines() {
// ...
if (lines > 0) {
// Добавить очки за собранные линии
account.score += this.getLinesClearPoints(lines, account.level);
account.lines += lines;
// Если собрано нужное кол-во линий, перейти на новый уровень
if (account.lines >= LINES_PER_LEVEL) {
// увеличить уровень
account.level++;
// сбросить счетчик линий
account.lines -= LINES_PER_LEVEL;
// увеличить скорость
time.level = LEVEL[account.level];
}
}
}
При сборке каждых десяти рядов, уровень будет повышаться, а скорость – увеличиваться.

Завершение игры
Игра завершается, когда пирамида фигурок достигает самого верха игрового поля.
Мы должны добавить проверку внутри методаboard.drop
. Если y-координата тетрамино равна нулю, значит, ему уже некуда падать.
drop() {
let p = moves[KEY.DOWN](this.piece);
if (this.valid(p)) {
this.piece.move(p);
} else {
this.freeze();
this.clearLines();
console.table(this.grid);
if (this.piece.y === 0) {
// Game over
return false;
}
this.piece = new Piece(this.ctx);
this.piece.setStartPosition();
}
return true;
}
Теперь метод drop
возвращает false
, если игру пора останавливать, и true
– в остальных случаях.
Чтобы прервать игровой цикл, нужно отменить последний вызов requestAnimationFrame
. Также нужно показать пользователю сообщение.
let requestId;
function gameOver() {
cancelAnimationFrame(requestId);
this.ctx.fillStyle = 'black';
this.ctx.fillRect(1, 3, 8, 1.2);
this.ctx.font = '1px Arial';
this.ctx.fillStyle = 'red';
this.ctx.fillText('GAME OVER', 1.8, 4);
}
function animate(now = 0) {
time.elapsed = now - time.start;
if (time.elapsed > time.level) {
time.start = now;
if (!board.drop()) {
gameOver();
return;
}
}
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
board.draw();
requestId = requestAnimationFrame(animate);
}

Следующая фигура
Для удобства игрока мы можем добавить подсказку – какая фигурка будет следующей. Для этого используем еще один холст меньшего размера:
<canvas id="next" class="next"></canvas>
Получим его контекст для рисования и установим размеры:
const canvasNext = document.getElementById('next');
const ctxNext = canvasNext.getContext('2d');
ctxNext.canvas.width = 4 * BLOCK_SIZE;
ctxNext.canvas.height = 4 * BLOCK_SIZE;
ctxNext.scale(BLOCK_SIZE, BLOCK_SIZE);
let board = new Board(ctx, ctxNext);
Осталось внести изменения в метод board.drop
:
constructor(ctx, ctxNext) {
this.ctx = ctx;
this.ctxNext = ctxNext;
this.piece = null;
this.next = null;
}
reset() {
this.grid = this.getEmptyBoard();
this.piece = new Piece(this.ctx);
this.piece.setStartPosition();
this.getNewPiece();
}
getNewPiece() {
this.next = new Piece(this.ctxNext);
this.ctxNext.clearRect(
0,
0,
this.ctxNext.canvas.width,
this.ctxNext.canvas.height
);
this.next.draw();
}
drop() {
// ...
this.freeze();
// ...
this.piece = this.next;
this.piece.ctx = this.ctx;
this.piece.setStartPosition();
this.getNewPiece();
}
Теперь игрок знает, какое тетрамино будет следующим, и может выстраивать стратегию игры.

Разработка игр - интересный и эффективный способ изучения программирования. А как учитесь вы?