Свой тетрис на JavaScript: прокачиваем проект
medium

Свой тетрис на JavaScript: прокачиваем проект

Доработки, чтобы получилась настоящая игра.

Как-то раз мы писали собственный тетрис на JavaScript. Мы закончили на том, что у нас есть простое игровое поле, фигуры и базовая логика игры. Ещё игра умеет останавливаться, когда для фигур больше нет места. Но этого недостаточно, чтобы считаться полноценной игрой. 

Чтобы это исправить, вот что мы добавим сегодня в игру:

  • ускорение падения на каждом уровне,
  • подсчёт очков,
  • вывод на экран текущего уровня и набранных очков,
  • систему рейтингов для игроков.

Всё это несложно, но требует внимания. Чтобы доработать игру, будем использовать код из первой части проекта:

<!DOCTYPE html>
<html>
<head>
  <title>Тетрис</title>
    <style>
      /*настройка стилей страницы*/

      html, body {
        height: 100%;
        margin: 0;
      }
      /*делаем чёрный фон и выравниваем всё по центру*/
      body {
        background: black;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      /*толщина холста — 1 пиксель, белый цвет*/
      canvas {
        border: 1px solid white;
      }
    </style>
</head>

<body>
  <!-- рисуем поле для игры -->
  <canvas width="320" height="640" id="game"></canvas>
  <script>
    // License CC0 1.0 Universal 
    // https://gist.github.com/straker/3c98304f8a6a9174efd8292800891ea1
    // https://tetris.fandom.com/wiki/Tetris_Guideline

    // получаем доступ к холсту
    const canvas = document.getElementById('game');
    const context = canvas.getContext('2d');
    // размер квадратика
    const grid = 32;
    // массив с последовательностями фигур, на старте — пустой
    var tetrominoSequence = [];

    // с помощью двумерного массива следим за тем, что находится в каждой клетке игрового поля
    // размер поля — 10 на 20, и несколько строк ещё находится за видимой областью
    var playfield = [];

    // заполняем сразу массив пустыми ячейками
    for (let row = -2; row < 20; row++) {
      playfield[row] = [];

      for (let col = 0; col < 10; col++) {
        playfield[row][col] = 0;
      }
    }

    // как рисовать каждую фигуру
    // https://tetris.fandom.com/wiki/SRS
    const tetrominos = {
      '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],
      ],
      'Z': [
        [1,1,0],
        [0,1,1],
        [0,0,0],
      ],
      'T': [
        [0,1,0],
        [1,1,1],
        [0,0,0],
      ]
    };

    // цвет каждой фигуры
    const colors = {
      'I': 'cyan',
      'O': 'yellow',
      'T': 'purple',
      'S': 'green',
      'Z': 'red',
      'J': 'blue',
      'L': 'orange'
    };

    // счётчик
    let count = 0;
    // текущая фигура в игре
    let tetromino = getNextTetromino();
    // следим за кадрами анимации, чтобы если что — остановить игру
    let rAF = null;  
    // флаг конца игры, на старте — неактивный
    let gameOver = false;


    // Функция возвращает случайное число в заданном диапазоне
    // https://stackoverflow.com/a/1527820/2124254
    function getRandomInt(min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);

      return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    // создаём последовательность фигур, которая появится в игре
    //https://tetris.fandom.com/wiki/Random_Generator
    function generateSequence() {
      // тут — сами фигуры
      const sequence = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'];

      while (sequence.length) {
        // случайным образом находим любую из них
        const rand = getRandomInt(0, sequence.length - 1);
        const name = sequence.splice(rand, 1)[0];
        // помещаем выбранную фигуру в игровой массив с последовательностями
        tetrominoSequence.push(name);
      }
    }

    // получаем следующую фигуру
    function getNextTetromino() {
      // если следующей нет — генерируем
      if (tetrominoSequence.length === 0) {
        generateSequence();
      }
      // берём первую фигуру из массива
      const name = tetrominoSequence.pop();
      // сразу создаём матрицу, с которой мы отрисуем фигуру
      const matrix = tetrominos[name];

      // I и O стартуют с середины, остальные — чуть левее
      const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

      // I начинает с 21 строки (смещение -1), а все остальные — со строки 22 (смещение -2)
      const row = name === 'I' ? -1 : -2;

      // вот что возвращает функция 
      return {
        name: name,      // название фигуры (L, O, и т. д.)
        matrix: matrix,  // матрица с фигурой
        row: row,        // текущая строка (фигуры стартуют за видимой областью холста)
        col: col         // текущий столбец
      };
    }

    // поворачиваем матрицу на 90 градусов
    // https://codereview.stackexchange.com/a/186834
    function rotate(matrix) {
      const N = matrix.length - 1;
      const result = matrix.map((row, i) =>
        row.map((val, j) => matrix[N - j][i])
      );
      // на входе матрица, и на выходе тоже отдаём матрицу
      return result;
    }

    // проверяем после появления или вращения, может ли матрица (фигура) быть в этом месте поля или она вылезет за его границы
    function isValidMove(matrix, cellRow, cellCol) {
      // проверяем все строки и столбцы
      for (let row = 0; row < matrix.length; row++) {
        for (let col = 0; col < matrix[row].length; col++) {
          if (matrix[row][col] && (
              // если выходит за границы поля…
              cellCol + col < 0 ||
              cellCol + col >= playfield[0].length ||
              cellRow + row >= playfield.length ||
              // …или пересекается с другими фигурами
              playfield[cellRow + row][cellCol + col])
            ) {
            // то возвращаем, что нет, так не пойдёт
            return false;
          }
        }
      }
      // а если мы дошли до этого момента и не закончили раньше — то всё в порядке
      return true;
    }

    // когда фигура окончательна встала на своё место
    function placeTetromino() {
      // обрабатываем все строки и столбцы в игровом поле
      for (let row = 0; row < tetromino.matrix.length; row++) {
        for (let col = 0; col < tetromino.matrix[row].length; col++) {
          if (tetromino.matrix[row][col]) {

            // если край фигуры после установки вылезает за границы поля, то игра закончилась
            if (tetromino.row + row < 0) {
              return showGameOver();
            }
            // если всё в порядке, то записываем в массив игрового поля нашу фигуру
            playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
          }
        } 
      }

      // проверяем, чтобы заполненные ряды очистились снизу вверх
      for (let row = playfield.length - 1; row >= 0; ) {
        // если ряд заполнен
        if (playfield[row].every(cell => !!cell)) {

          // очищаем его и опускаем всё вниз на одну клетку
          for (let r = row; r >= 0; r--) {
            for (let c = 0; c < playfield[r].length; c++) {
              playfield[r][c] = playfield[r-1][c];
            }
          }
        }
        else {
          // переходим к следующему ряду
          row--;
        }
      }
      // получаем следующую фигуру
      tetromino = getNextTetromino();
    }

      // показываем надпись Game Over
      function showGameOver() {
        // прекращаем всю анимацию игры
        cancelAnimationFrame(rAF);
        // ставим флаг окончания
        gameOver = true;
        // рисуем чёрный прямоугольник посередине поля
        context.fillStyle = 'black';
        context.globalAlpha = 0.75;
        context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
        // пишем надпись белым моноширинным шрифтом по центру
        context.globalAlpha = 1;
        context.fillStyle = 'white';
        context.font = '36px monospace';
        context.textAlign = 'center';
        context.textBaseline = 'middle';
        context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
      }

    

    // главный цикл игры
    function loop() {
      // начинаем анимацию
      rAF = requestAnimationFrame(loop);
      // очищаем холст
      context.clearRect(0,0,canvas.width,canvas.height);

      // рисуем игровое поле с учётом заполненных фигур
      for (let row = 0; row < 20; row++) {
        for (let col = 0; col < 10; col++) {
          if (playfield[row][col]) {
            const name = playfield[row][col];
            context.fillStyle = colors[name];

            // рисуем всё на один пиксель меньше, чтобы получился эффект «в клетку»
            context.fillRect(col * grid, row * grid, grid-1, grid-1);
          }
        }
      }

      // рисуем текущую фигуру
      if (tetromino) {

        // фигура сдвигается вниз каждые 35 кадров
        if (++count > 35) {
          tetromino.row++;
          count = 0;

          // если движение закончилось — рисуем фигуру в поле и проверяем, можно ли удалить строки
          if (!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)) {
            tetromino.row--;
            placeTetromino();
          }
        }

        // не забываем про цвет текущей фигуры
        context.fillStyle = colors[tetromino.name];

        // отрисовываем её
        for (let row = 0; row < tetromino.matrix.length; row++) {
          for (let col = 0; col < tetromino.matrix[row].length; col++) {
            if (tetromino.matrix[row][col]) {

              // и снова рисуем на один пиксель меньше
              context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
            }
          }
        }
      }
    }

    // следим за нажатиями на клавиши
    document.addEventListener('keydown', function(e) {
      // если игра закончилась — сразу выходим
      if (gameOver) return;

      // стрелки влево и вправо
      if (e.which === 37 || e.which === 39) {
        const col = e.which === 37
          // если влево, то уменьшаем индекс в столбце, если вправо — увеличиваем
          ? tetromino.col - 1
          : tetromino.col + 1;

        // если так ходить можно, то запоминаем текущее положение 
        if (isValidMove(tetromino.matrix, tetromino.row, col)) {
          tetromino.col = col;
        }
      }

      // стрелка вверх — поворот
      if (e.which === 38) {
        // поворачиваем фигуру на 90 градусов
        const matrix = rotate(tetromino.matrix);
        // если так ходить можно — запоминаем
        if (isValidMove(matrix, tetromino.row, tetromino.col)) {
          tetromino.matrix = matrix;
        }
      }

      // стрелка вниз — ускорить падение
      if(e.which === 40) {
        // смещаем фигуру на строку вниз
        const row = tetromino.row + 1;
        // если опускаться больше некуда — запоминаем новое положение
        if (!isValidMove(tetromino.matrix, row, tetromino.col)) {
          tetromino.row = row - 1;
          // ставим на место и смотрим на заполненные ряды
          placeTetromino();
          return;
        }
        // запоминаем строку, куда стала фигура
        tetromino.row = row;
      }
    });

    // старт игры
    rAF = requestAnimationFrame(loop);
  </script>
</body>
</html>

Отображаем текущий уровень и набранные очки

Если мы сделаем вывод информации об уровнях и статистике внутри игрового стакана, это будет неудобно: текст будет наползать на фигуры и мешать игре. Значит, нам нужно добавить отдельный блок, где мы будем писать все игровые данные. Сделаем его сразу после объявления поля для игры и даём блоку имя score:

<!-- добавляем область для вывода информации о текущей игре -->

  <canvas style="border: 1px" width="320" height="100" id="score"></canvas>

Так как у нас в глобальных стилях прописана рамка для холста, а для отображения статистики рамка не нужна, поставим ей нулевую толщину.

Заметьте: теперь у нас на странице есть два объекта типа canvas — то есть два холста. В одном рисуется игра, в другом выводятся очки. Теперь мы сможем обращаться к этим холстам отдельно и рисовать в каждом то, что нам нужно, независимо друг от друга. 

Вот как получить доступ к новому холсту: 

// получаем доступ к холсту с игровой статистикой

    const canvasScore = document.getElementById('score');

    const contextScore = canvasScore.getContext('2d');

Добавим в скрипт новые переменные — они нам сразу пригодятся на следующем этапе:

// количество набранных очков на старте
let score = 0;
// рекорд игры
let record = 0;
// текущий уровень сложности
let level = 1;
// имя игрока с наибольшим рейтингом
let recordName = '';

Теперь выведем на экран всю игровую статистику. Для этого перед главным циклом игры сделаем новую функцию showScore():

function showScore() {
  contextScore.clearRect(0,0,canvasScore.width,canvasScore.height);  
  contextScore.globalAlpha = 1;
  contextScore.fillStyle = 'white';
  contextScore.font = '18px Courier New';
  contextScore.fillText('Уровень: ' + level, 15, 20);
  contextScore.fillText('Очков:   ' + score, 15, 50);
  contextScore.fillText('Чемпион: ' + recordName, 160, 20);
  contextScore.fillText('Рекорд:  ' + record, 160, 50);

}

Цифры вроде 15, 20, 50, 160 — это координаты по вертикали и горизонтали, где должны находиться наши текстовые блоки. Без этих координат они все налезут друг на друга. 

Последнее, что нам осталось сделать — вызвать эту функцию внутри основной функции loop() командой showScore().

Функция внутри основной функции loop() командой showScore()
У нас вывелась статистика, но программа пока никак её не считает. Исправим это.

Спрашиваем имя игрока

Чтобы знать, кому принадлежит рекорд игры, будем спрашивать имя при каждом запуске игры:

    name = prompt("Ваше имя", "");

Этот код мы напишем сразу после объявления всех переменных, чтобы он выполнился на старте один раз. Сменить имя в процессе игры будет нельзя, но это и не нужно. 

Считаем очки

Считать будем так: за каждую собранную линию начисляем 10 очков, по количеству кубиков в ней. В оригинальном тетрисе считается немного иначе, но для простоты сделаем так.

Для подсчёта очков находим в функции placeTetromino() проверку, заполнен ли ряд целиком, и сразу первым действием в этой проверке пишем:

score += 10;

Заметьте: нам не нужно городить новую сложную функцию по определению заполняемости строк — мы ее уже написали, когда программировали основную логику игры. Нужно просто дополнить уже существующую рабочую функцию одним новым действием — начислением очков. Если бы мы хотели добавить звуков или спецэффектов, мы бы точно так же добавили в эту функцию новые команды.

Теперь добавим проверку рекорда — побили мы его уже или нет. Для этого будем постоянно сравнивать текущие очки с рекордом, и если текущие больше — установим новый рекорд и запишем имя победителя:

// если игрок побил прошлый рекорд
if (score > record) {
  // ставим его очки как рекорд
  record = score;
  // меняем имя чемпиона
  recordName = name;
}

Запоминаем рекорды

Сейчас у игры есть существенный недостаток — если открыть страницу заново в том же браузере, она не вспомнит имя чемпиона. Когда мы делали свой туду-лист на JavaScript, то использовали для этого локальное хранилище браузера — localstorage. При новом открытии страницы можно будет взять данные о рекорде оттуда.

Возьмём из той статьи наш код для локального хранилища и поправим его под нынешнюю задачу — хранить имя игрока и его рекорд. Поставим этот код сразу после того, как спросим имя игрока на старте:

// Узнаём размер хранилища
var Storage_size = localStorage.length;
// Если в хранилище уже что-то есть…
if (Storage_size > 0) {
  // …то достаём оттуда значение рекорда и имя чемпиона
  record = localStorage.record;
  recordName = localStorage.recordName;
}

Но в хранилище ничего не появится автоматически — мы сами должны положить туда значение рекорда и имя чемпиона. Сделаем это в том же разделе, где мы начисляем очки за линии. Поправим немного, чтобы код выглядел так:

// если игрок побил прошлый рекорд
if (score > record) {
  // ставим его очки как рекорд
  record = score;
  // заносим в хранилище значение рекорда
  localStorage.record = record;
  // меняем имя чемпиона
  recordName = name;
  // заносим в хранилище его имя
  localStorage.recordName = recordName;

Теперь при каждом обновлении рекорда в хранилище сразу будет записываться актуальная информация. Даже если мы обновим страницу, записи останутся:

Свой тетрис на JavaScript: прокачиваем проект

Добавляем сложность и уровни

Чтобы играть было интереснее, сделаем так: с каждым новым уровнем фигуры будут падать всё быстрее. Для этого находим в коде строчку, которая отвечает за скорость движения фигур вниз и правим её следующим образом:

// фигура сдвигается вниз каждые 36 кадров минус значение текущего уровня. Чем больше уровень, тем быстрее падает.

if (++count > (36 - level)) {

Логика тут такая: чем выше уровень, тем меньше интервал обновления между сдвигами, что даёт нам ощущение повышенной скорости. Хотя на самом деле фигуры не падают быстрее, просто они меньше тупят.

Например, на первом уровне фигура сдвигается на одну клетку вниз каждые 35 кадров, а на 10 уровне — каждые 25 кадров, на треть быстрее.

Сами уровни будем считать там же, где и очки. Чтобы перейти на новый уровень, нужно набрать 100 очков. Зная это, легко получить значение уровня — достаточно разделить нацело количество очков на 100 и прибавить 1. Такой подход и даст нам нужный уровень.

Запишем это там же, где и идёт подсчёт очков:

// считаем уровень

level = Math.floor(score/100) + 1;

Теперь всё в порядке. Можете скопировать код себе или поиграть на странице проекта.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Тетрис</title>
    <style>
      /*настройки стилей страницы*/

      html, body {
        height: 100%;
        margin: 0;
      }
      /*делаем чёрный фон и выравниваем всё по центру*/
      body {
        background: black;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      /*толщина обводки холста — 1 пиксель, белый цвет*/
      canvas {
        border: 1px solid white;
      }


    </style>
</head>

<body>
  <!-- рисуем поле для игры -->
  <canvas width="320" height="640" id="game"></canvas>

  <!-- добавляем область для вывода информации о текущей игре -->
  <canvas style="border: 0px" width="320" height="100" id="score"></canvas>

  <script>
    // License CC0 1.0 Universal 
    // https://gist.github.com/straker/3c98304f8a6a9174efd8292800891ea1
    // https://tetris.fandom.com/wiki/Tetris_Guideline

    // получаем доступ к основному холсту
    const canvas = document.getElementById('game');
    const context = canvas.getContext('2d');

    // получаем доступ к холсту с игровой статистикой
    const canvasScore = document.getElementById('score');
    const contextScore = canvasScore.getContext('2d');

    // размер квадратика
    const grid = 32;
    // массив с последовательностями фигур, на старте — пустой
    var tetrominoSequence = [];

    // с помощью двумерного массива следим за тем, что находится в каждой клетке игрового поля
    // размер поля — 10 на 20, и несколько строк ещё находится за видимой областью
    var playfield = [];

    // заполняем сразу массив пустыми ячейками
    for (let row = -2; row < 20; row++) {
      playfield[row] = [];

      for (let col = 0; col < 10; col++) {
        playfield[row][col] = 0;
      }
    }

    // как рисовать каждую фигуру
    // https://tetris.fandom.com/wiki/SRS
    const tetrominos = {
      '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],
      ],
      'Z': [
        [1,1,0],
        [0,1,1],
        [0,0,0],
      ],
      'T': [
        [0,1,0],
        [1,1,1],
        [0,0,0],
      ]
    };

    // цвет каждой фигуры
    const colors = {
      'I': 'cyan',
      'O': 'yellow',
      'T': 'purple',
      'S': 'green',
      'Z': 'red',
      'J': 'blue',
      'L': 'orange'
    };

    // счётчик
    let count = 0;
    // текущая фигура в игре
    let tetromino = getNextTetromino();
    // следим за кадрами анимации, чтобы если что — остановить игру
    let rAF = null;  
    // флаг конца игры, на старте — неактивный
    let gameOver = false;

    // количество набранных очков на старте
    let score = 0;
    // рекорд игры
    let record = 0;
    // текущий уровень сложности
    let level = 1;
    // имя игрока с наибольшим рейтингом
    let recordName = '';

    // спрашиваем имя игрока при запуске
    name = prompt("Ваше имя", "");


    // Узнаём размер хранилища
    var Storage_size = localStorage.length;
    // Если в хранилище уже что-то есть…
    if (Storage_size > 0) {
      // …то достаём оттуда значение рекорда и имя чемпиона
      record = localStorage.record;
      recordName = localStorage.recordName;
    }

    // Функция возвращает случайное число в заданном диапазоне
    // https://stackoverflow.com/a/1527820/2124254
    function getRandomInt(min, max) {
      min = Math.ceil(min);
      max = Math.floor(max);

      return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    // создаём последовательность фигур, которая появится в игре
    //https://tetris.fandom.com/wiki/Random_Generator
    function generateSequence() {
      // тут — сами фигуры
      const sequence = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'];

      while (sequence.length) {
        // случайным образом находим любую из них
        const rand = getRandomInt(0, sequence.length - 1);
        const name = sequence.splice(rand, 1)[0];
        // помещаем выбранную фигуру в игровой массив с последовательностями
        tetrominoSequence.push(name);
      }
    }

    // получаем следующую фигуру
    function getNextTetromino() {
      // если следующей нет — генерируем
      if (tetrominoSequence.length === 0) {
        generateSequence();
      }
      // берём первую фигуру из массива
      const name = tetrominoSequence.pop();
      // сразу создаём матрицу, с которой мы отрисуем фигуру
      const matrix = tetrominos[name];

      // I и O стартуют с середины, остальные — чуть левее
      const col = playfield[0].length / 2 - Math.ceil(matrix[0].length / 2);

      // I начинает с 21 строки (смещение -1), а все остальные — со строки 22 (смещение -2)
      const row = name === 'I' ? -1 : -2;

      // вот что возвращает функция 
      return {
        name: name,      // название фигуры (L, O, и т. д.)
        matrix: matrix,  // матрица с фигурой
        row: row,        // текущая строка (фигуры стартуют за видимой областью холста)
        col: col         // текущий столбец
      };
    }

    // поворачиваем матрицу на 90 градусов
    // https://codereview.stackexchange.com/a/186834
    function rotate(matrix) {
      const N = matrix.length - 1;
      const result = matrix.map((row, i) =>
        row.map((val, j) => matrix[N - j][i])
      );
      // на входе матрица, и на выходе тоже отдаём матрицу
      return result;
    }

    // проверяем после появления или вращения, может ли матрица (фигура) быть в этом месте поля или она вылезет за его границы
    function isValidMove(matrix, cellRow, cellCol) {
      // проверяем все строки и столбцы
      for (let row = 0; row < matrix.length; row++) {
        for (let col = 0; col < matrix[row].length; col++) {
          if (matrix[row][col] && (
              // если выходит за границы поля…
              cellCol + col < 0 ||
              cellCol + col >= playfield[0].length ||
              cellRow + row >= playfield.length ||
              // …или пересекается с другими фигурами
              playfield[cellRow + row][cellCol + col])
            ) {
            // то возвращаем, что нет, так не пойдёт
            return false;
          }
        }
      }
      // а если мы дошли до этого момента и не закончили раньше — то всё в порядке
      return true;
    }

    // когда фигура окончательна встала на своё место
    function placeTetromino() {
      // обрабатываем все строки и столбцы в игровом поле
      for (let row = 0; row < tetromino.matrix.length; row++) {
        for (let col = 0; col < tetromino.matrix[row].length; col++) {
          if (tetromino.matrix[row][col]) {

            // если край фигуры после установки вылезает за границы поля, то игра закончилась
            if (tetromino.row + row < 0) {
              return showGameOver();
            }
            // если всё в порядке, то записываем в массив игрового поля нашу фигуру
            playfield[tetromino.row + row][tetromino.col + col] = tetromino.name;
          }
        } 
      }

      // проверяем, чтобы заполненные ряды очистились снизу вверх
      for (let row = playfield.length - 1; row >= 0; ) {
        // если ряд заполнен
        if (playfield[row].every(cell => !!cell)) {

          score += 10;
          // считаем уровень
          level = Math.floor(score/100) + 1;
          // если игрок побил прошлый рекорд
          if (score > record) {
            // ставим его очки как рекорд
            record = score;
            // заносим в хранилище значение рекорда
            localStorage.record = record;
            // меняем имя чемпиона
            recordName = name;
            // заносим в хранилище его имя
            localStorage.recordName = recordName;
          }

          // очищаем его и опускаем всё вниз на одну клетку
          for (let r = row; r >= 0; r--) {
            for (let c = 0; c < playfield[r].length; c++) {
              playfield[r][c] = playfield[r-1][c];
            }
          }
        }
        else {
          // переходим к следующему ряду
          row--;
        }
      }
      // получаем следующую фигуру
      tetromino = getNextTetromino();
    }

      // показываем надпись Game Over
      function showGameOver() {
        // прекращаем всю анимацию игры
        cancelAnimationFrame(rAF);
        // ставим флаг окончания
        gameOver = true;
        // рисуем чёрный прямоугольник посередине поля
        context.fillStyle = 'black';
        context.globalAlpha = 0.75;
        context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
        // пишем надпись белым моноширинным шрифтом по центру
        context.globalAlpha = 1;
        context.fillStyle = 'white';
        context.font = '36px monospace';
        context.textAlign = 'center';
        context.textBaseline = 'middle';
        context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);
      }

    function showScore() {
      contextScore.clearRect(0,0,canvasScore.width,canvasScore.height);
      contextScore.globalAlpha = 1;
      contextScore.fillStyle = 'white';
      contextScore.font = '18px Courier New';
      contextScore.fillText('Уровень: ' + level, 15, 20);
      contextScore.fillText('Очков:   ' + score, 15, 50);
      contextScore.fillText('Чемпион: ' + recordName, 160, 20);
      contextScore.fillText('Рекорд:  ' + record, 160, 50);

    }

    // главный цикл игры
    function loop() {
      // начинаем анимацию
      rAF = requestAnimationFrame(loop);
      // очищаем холст
      context.clearRect(0,0,canvas.width,canvas.height);

      // рисуем игровое поле с учётом заполненных фигур
      for (let row = 0; row < 20; row++) {
        for (let col = 0; col < 10; col++) {
          if (playfield[row][col]) {
            const name = playfield[row][col];
            context.fillStyle = colors[name];

            // рисуем всё на один пиксель меньше, чтобы получился эффект «в клетку»
            context.fillRect(col * grid, row * grid, grid-1, grid-1);
          }
        }
      }

      // выводим статистику
      showScore();

      // рисуем текущую фигуру
      if (tetromino) {

        // фигура сдвигается вниз каждые 36 кадров минус значение текущего уровня. Чем больше уровень, тем быстрее падает.
        if (++count > (36 - level)) {
          tetromino.row++;
          count = 0;

          // если движение закончилось — рисуем фигуру в поле и проверяем, можно ли удалить строки
          if (!isValidMove(tetromino.matrix, tetromino.row, tetromino.col)) {
            tetromino.row--;
            placeTetromino();
          }
        }

        // не забываем про цвет текущей фигуры
        context.fillStyle = colors[tetromino.name];

        // отрисовываем её
        for (let row = 0; row < tetromino.matrix.length; row++) {
          for (let col = 0; col < tetromino.matrix[row].length; col++) {
            if (tetromino.matrix[row][col]) {

              // и снова рисуем на один пиксель меньше
              context.fillRect((tetromino.col + col) * grid, (tetromino.row + row) * grid, grid-1, grid-1);
            }
          }
        }
      }
    }

    // следим за нажатиями на клавиши
    document.addEventListener('keydown', function(e) {
      // если игра закончилась — сразу выходим
      if (gameOver) return;

      // стрелки влево и вправо
      if (e.which === 37 || e.which === 39) {
        const col = e.which === 37
          // если влево, то уменьшаем индекс в столбце, если вправо — увеличиваем
          ? tetromino.col - 1
          : tetromino.col + 1;

        // если так ходить можно, то запоминаем текущее положение 
        if (isValidMove(tetromino.matrix, tetromino.row, col)) {
          tetromino.col = col;
        }
      }

      // стрелка вверх — поворот
      if (e.which === 38) {
        // поворачиваем фигуру на 90 градусов
        const matrix = rotate(tetromino.matrix);
        // если так ходить можно — запоминаем
        if (isValidMove(matrix, tetromino.row, tetromino.col)) {
          tetromino.matrix = matrix;
        }
      }

      // стрелка вниз — ускорить падение
      if(e.which === 40) {
        // смещаем фигуру на строку вниз
        const row = tetromino.row + 1;
        // если опускаться больше некуда — запоминаем новое положение
        if (!isValidMove(tetromino.matrix, row, tetromino.col)) {
          tetromino.row = row - 1;
          // ставим на место и смотрим на заполненные ряды
          placeTetromino();
          return;
        }
        // запоминаем строку, куда стала фигура
        tetromino.row = row;
      }
    });

    // старт игры
    rAF = requestAnimationFrame(loop);
  </script>
</body>
</html>

Код:

Стивен Ламбер и Михаил Полянин

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

Даня Берковский

Корректор:

Ирина Михеева

Вёрстка:

Мария Дронова

Соцсети:

Олег Вешкурцев

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium