Пинг-понг против компьютера на JavaScript
easy

Пинг-понг против компьютера на JavaScript

Лёгкая версия

В одном из наших проектов мы делали игру в пинг-понг на JavaScript, где нужно было играть вдвоём. Один игрок управлял одной ракеткой, второй — другой ракеткой, а мячик летал по написанным нами законам физики.

Следующий шаг: научим компьютер играть против человека. Сегодня реализуем самый простой вариант.

Что нам понадобится

Для работы нужен только исходный код старой игры и немного новой логики. Скопируйте код из выпадайки в новый документ, сохраните как HTML и откройте в браузере.

<!DOCTYPE html>
<html>
<head>
  <title>Пинг-понг на JavaScript</title>
  <!-- Задаём стили для всего документа -->
  <style>
    html,
    body {
      height: 100%;
      margin: 0;
    }
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>
<body>
  <!-- Рисуем игровое поле -->
  <canvas width="750" height="585" id="game"></canvas>
  <script>
    // Обращаемся к игровому полю из документа
    const canvas = document.getElementById('game');
    // Делаем поле двухмерным
    const context = canvas.getContext('2d');
    // Размер игровой клетки
    const grid = 15;
    // Высота платформы
    const paddleHeight = grid * 5; // 80
    // Задаём максимальное расстояние, на которое может подняться платформа
    const maxPaddleY = canvas.height - grid - paddleHeight;
    // Скорость платформы
    var paddleSpeed = 6;
    // Скорость мяча
    var ballSpeed = 5;
    // Описываем левую платформу
    const leftPaddle = {
      // Ставим её по центру
      x: grid * 2,
      y: canvas.height / 2 - paddleHeight / 2,
      // Ширина — одна клетка
      width: grid,
      // Высоту берём из константы
      height: paddleHeight,
      // Платформа на старте никуда не движется
      dy: 0
    };
    // Описываем правую платформу
    const rightPaddle = {
      // Ставим по центру с правой стороны
      x: canvas.width - grid * 3,
      y: canvas.height / 2 - paddleHeight / 2,
      // Задаём такую же ширину и высоту
      width: grid,
      height: paddleHeight,
      // Правая платформа тоже пока никуда не двигается
      dy: 0
    };
    // Описываем мячик
    const ball = {
      // Он появляется в самом центре поля
      x: canvas.width / 2,
      y: canvas.height / 2,
      // квадратный, размером с клетку
      width: grid,
      height: grid,
      // На старте мяч пока не забит, поэтому убираем признак того, что мяч нужно ввести в игру заново
      resetting: false,
      // Подаём мяч в правый верхний угол
      dx: ballSpeed,
      dy: -ballSpeed
    };
    // Проверка на то, пересекаются два объекта с известными координатами или нет
    // Подробнее тут: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
    function collides(obj1, obj2) {
      return obj1.x < obj2.x + obj2.width &&
        obj1.x + obj1.width > obj2.x &&
        obj1.y < obj2.y + obj2.height &&
        obj1.y + obj1.height > obj2.y;
    }
    // Главный цикл игры
    function loop() {
      // Очищаем игровое поле
      requestAnimationFrame(loop);
      context.clearRect(0, 0, canvas.width, canvas.height);
      // Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
      leftPaddle.y += leftPaddle.dy;
      rightPaddle.y += rightPaddle.dy;
      // Если левая платформа пытается вылезти за игровое поле вниз,
      if (leftPaddle.y < grid) {
        // то оставляем её на месте
        leftPaddle.y = grid;
      }
      // Проверяем то же самое сверху
      else if (leftPaddle.y > maxPaddleY) {
        leftPaddle.y = maxPaddleY;
      }
      // Если правая платформа пытается вылезти за игровое поле вниз,
      if (rightPaddle.y < grid) {
        // то оставляем её на месте
        rightPaddle.y = grid;
      }
      // Проверяем то же самое сверху
      else if (rightPaddle.y > maxPaddleY) {
        rightPaddle.y = maxPaddleY;
      }
      // Рисуем платформы белым цветом
      context.fillStyle = 'white';
      // Каждая платформа — прямоугольник
      context.fillRect(leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height);
      context.fillRect(rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height);
      // Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
      ball.x += ball.dx;
      ball.y += ball.dy;
      // Если мяч касается стены снизу — меняем направление по оси У на противоположное
      if (ball.y < grid) {
        ball.y = grid;
        ball.dy *= -1;
      }
      // Делаем то же самое, если мяч касается стены сверху
      else if (ball.y + grid > canvas.height - grid) {
        ball.y = canvas.height - grid * 2;
        ball.dy *= -1;
      }
      // Если мяч улетел за игровое поле влево или вправо — перезапускаем его
      if ((ball.x < 0 || ball.x > canvas.width) && !ball.resetting) {
        // Помечаем, что мяч перезапущен, чтобы не зациклиться
        ball.resetting = true;
        // Даём секунду на подготовку игрокам
        setTimeout(() => {
          // Всё, мяч в игре
          ball.resetting = false;
          // Снова запускаем его из центра
          ball.x = canvas.width / 2;
          ball.y = canvas.height / 2;
        }, 1000);
      }
      // Если мяч коснулся левой платформы,
      if (collides(ball, leftPaddle)) {
        // то отправляем его в обратном направлении
        ball.dx *= -1;
        // Увеличиваем координаты мяча на ширину платформы, чтобы не засчитался новый отскок
        ball.x = leftPaddle.x + leftPaddle.width;
      }
      // Проверяем и делаем то же самое для правой платформы
      else if (collides(ball, rightPaddle)) {
        ball.dx *= -1;
        ball.x = rightPaddle.x - ball.width;
      }
      // Рисуем мяч
      context.fillRect(ball.x, ball.y, ball.width, ball.height);
      // Рисуем стены
      context.fillStyle = 'lightgrey';
      context.fillRect(0, 0, canvas.width, grid);
      context.fillRect(0, canvas.height - grid, canvas.width, canvas.height);
      // Рисуем сетку посередине
      for (let i = grid; i < canvas.height - grid; i += grid * 2) {
        context.fillRect(canvas.width / 2 - grid / 2, i, grid, grid);
      }
      // Отслеживаем нажатия клавиш
      document.addEventListener('keydown', function (e) {
        // Если нажата клавиша вверх,
        if (e.which === 38) {
          // то двигаем правую платформу вверх
          rightPaddle.dy = -paddleSpeed;
        }
        // Если нажата клавиша вниз,
        else if (e.which === 40) {
          // то двигаем правую платформу вниз
          rightPaddle.dy = paddleSpeed;
        }
        // Если нажата клавиша W,
        if (e.which === 87) {
          // то двигаем левую платформу вверх
          leftPaddle.dy = -paddleSpeed;
        }
        // Если нажата клавиша S,
        else if (e.which === 83) {
          // то двигаем левую платформу вниз
          leftPaddle.dy = paddleSpeed;
        }
      });
      // А теперь следим за тем, когда кто-то отпустит клавишу, чтобы остановить движение платформы
      document.addEventListener('keyup', function (e) {
        // Если это стрелка вверх или вниз,
        if (e.which === 38 || e.which === 40) {
          // останавливаем правую платформу
          rightPaddle.dy = 0;
        }
        // А если это W или S,
        if (e.which === 83 || e.which === 87) {
          // останавливаем левую платформу
          leftPaddle.dy = 0;
        }
      });
    }
    // Запускаем игру
    requestAnimationFrame(loop);
  </script>
</body>
</html>

Новая логика игры

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

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

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

Начнём с мяча.

Замедляем мяч

Это самое простое изменение — достаточно поменять одну цифру в коде, как игра становится более понятной и управляемой:

// Скорость мяча
var ballSpeed = 4;

Если 4 для вас всё равно слишком быстро — поставьте 3, а если хотите занять детей — то 2.

Увеличиваем левую платформу

Находим код, который отвечает за левую платформу. За размер отвечает параметр высоты. Просто умножаем её на 2:

// Описываем левую платформу
 const leftPaddle = {
   // Ставим её по центру
   x: grid * 2,
   y: canvas.height / 2 - paddleHeight / 2,
   // Ширина — одна клетка
   width: grid,
   // Удваиваем размер платформы
   height: paddleHeight * 2,
   // Платформа на старте никуда не движется
   dy: 0
};

Пока ничего никуда не движется, но сейчас мы это исправим.

Запускаем платформу

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

leftPaddle.dy = paddleSpeed;

Но сразу после запуска игры платформа доезжает до края платформы, немного проваливается вниз, несмотря на свой размер, и останавливается. Нужно это исправить:

Делаем отскок от границ

Находим блок, который проверяет, вышли платформы за границы или нет. Правую пока не трогаем, нас интересует только левая, за которую играет компьютер. 

Наша задача — поменять условия так, чтобы они заставляли платформу саму двигаться вверх и вниз:

 // Если левая платформа пытается вылезти за игровое поле вниз,
      if (leftPaddle.y < grid) {
          // то оставляем её на месте
          leftPaddle.y = grid;
  leftPaddle.dy = paddleSpeed;
      }
      // Проверяем то же самое сверху
      else if (leftPaddle.y > maxPaddleY) {
          leftPaddle.y = maxPaddleY;
  leftPaddle.dy = -paddleSpeed;
      }

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

Исправляем баг с размером

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

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

    const LeftmaxPaddleY = canvas.height - grid - paddleHeight * 2;
    const RightmaxPaddleY = canvas.height - grid - paddleHeight;

Теперь видно, что левая платформа в 2 раза больше правой, и координаты крайней точки тоже изменились. Теперь нам нужно просмотреть весь код и найти все места, где используется старая константа maxPaddleY и заменить её на новую, учитывая сторону платформы.

На наше счастье, это нужно сделать только в блоке проверки границ при движении. Сразу исправим:

// Если левая платформа пытается вылезти за игровое поле вниз,
  if (leftPaddle.y < grid) {
        // то оставляем её на месте
    leftPaddle.y = grid;
    leftPaddle.dy = paddleSpeed;
  }
  // Проверяем то же самое сверху
  else if (leftPaddle.y > LeftmaxPaddleY) {
    leftPaddle.y = LeftmaxPaddleY;
    leftPaddle.dy = -paddleSpeed;
  }
  // Если правая платформа пытается вылезти за игровое поле вниз,
  if (rightPaddle.y < grid) {
 // то оставляем её на месте
    rightPaddle.y = grid;
  }
  // Проверяем то же самое сверху
  else if (rightPaddle.y > RightmaxPaddleY) {
    rightPaddle.y = RightmaxPaddleY;
  }

Теперь всё как нужно. Можно играть против роботизированной ракетки и, наверное, довольно часто выигрывать. 

<!DOCTYPE html>
<html>
<head>
  <title>Пинг-понг на JavaScript</title>
  <!-- Задаём стили для всего документа -->
  <style>
    html,
    body {
      height: 100%;
      margin: 0;
    }
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>
<body>
  <!-- Рисуем игровое поле -->
  <canvas width="750" height="585" id="game"></canvas>
  <script>
    // Обращаемся к игровому полю из документа
    const canvas = document.getElementById('game');
    // Делаем поле двухмерным
    const context = canvas.getContext('2d');
    // Размер игровой клетки
    const grid = 15;
    // Высота платформы
    const paddleHeight = grid * 5; // 80
    // Задаём максимальное расстояние, на которое могут двигаться платформы
    const LeftmaxPaddleY = canvas.height - grid - paddleHeight * 2;
    const RightmaxPaddleY = canvas.height - grid - paddleHeight;
    // Скорость платформы
    var paddleSpeed = 6;
    // Скорость мяча
    var ballSpeed = 4;
    // Описываем левую платформу
    const leftPaddle = {
      // Ставим её по центру
      x: grid * 2,
      y: canvas.height / 2 - paddleHeight / 2,
      // Ширина — одна клетка
      width: grid,
      // Высоту берём из константы
      height: paddleHeight * 2,
      // Платформа на старте никуда не движется
      dy: 0
    };
    leftPaddle.dy = paddleSpeed;
    // Описываем правую платформу
    const rightPaddle = {
      // Ставим по центру с правой стороны
      x: canvas.width - grid * 3,
      y: canvas.height / 2 - paddleHeight / 2,
      // Задаём такую же ширину и высоту
      width: grid,
      height: paddleHeight,
      // Правая платформа тоже пока никуда не двигается
      dy: 0
    };
    // Описываем мячик
    const ball = {
      // Он появляется в самом центре поля
      x: canvas.width / 2,
      y: canvas.height / 2,
      // квадратный, размером с клетку
      width: grid,
      height: grid,
      // На старте мяч пока не забит, поэтому убираем признак того, что мяч нужно ввести в игру заново
      resetting: false,
      // Подаём мяч в правый верхний угол
      dx: ballSpeed,
      dy: -ballSpeed
    };
    // Проверка на то, пересекаются два объекта с известными координатами или нет
    // Подробнее тут: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
    function collides(obj1, obj2) {
      return obj1.x < obj2.x + obj2.width &&
        obj1.x + obj1.width > obj2.x &&
        obj1.y < obj2.y + obj2.height &&
        obj1.y + obj1.height > obj2.y;
    }
    // Главный цикл игры
    function loop() {
      // Очищаем игровое поле
      requestAnimationFrame(loop);
      context.clearRect(0, 0, canvas.width, canvas.height);
      // Если платформы на предыдущем шаге куда-то двигались — пусть продолжают двигаться
      leftPaddle.y += leftPaddle.dy;
      rightPaddle.y += rightPaddle.dy;
      // Если левая платформа пытается вылезти за игровое поле вниз,
      if (leftPaddle.y < grid) {
        // то оставляем её на месте
        leftPaddle.y = grid;
        leftPaddle.dy = paddleSpeed;
      }
      // Проверяем то же самое сверху
      else if (leftPaddle.y > LeftmaxPaddleY) {
        leftPaddle.y = LeftmaxPaddleY;
        leftPaddle.dy = -paddleSpeed;
      }
      // Если правая платформа пытается вылезти за игровое поле вниз,
      if (rightPaddle.y < grid) {
        // то оставляем её на месте
        rightPaddle.y = grid;
      }
      // Проверяем то же самое сверху
      else if (rightPaddle.y > RightmaxPaddleY) {
        rightPaddle.y = RightmaxPaddleY;
      }


      // Рисуем платформы белым цветом
      context.fillStyle = 'white';
      // Каждая платформа — прямоугольник
      context.fillRect(leftPaddle.x, leftPaddle.y, leftPaddle.width, leftPaddle.height);
      context.fillRect(rightPaddle.x, rightPaddle.y, rightPaddle.width, rightPaddle.height);
      // Если мяч на предыдущем шаге куда-то двигался — пусть продолжает двигаться
      ball.x += ball.dx;
      ball.y += ball.dy;
      // Если мяч касается стены снизу — меняем направление по оси У на противоположное
      if (ball.y < grid) {
        ball.y = grid;
        ball.dy *= -1;
      }
      // Делаем то же самое, если мяч касается стены сверху
      else if (ball.y + grid > canvas.height - grid) {
        ball.y = canvas.height - grid * 2;
        ball.dy *= -1;
      }
      // Если мяч улетел за игровое поле влево или вправо — перезапускаем его
      if ((ball.x < 0 || ball.x > canvas.width) && !ball.resetting) {
        // Помечаем, что мяч перезапущен, чтобы не зациклиться
        ball.resetting = true;
        // Даём секунду на подготовку игрокам
        setTimeout(() => {
          // Всё, мяч в игре
          ball.resetting = false;
          // Снова запускаем его из центра
          ball.x = canvas.width / 2;
          ball.y = canvas.height / 2;
        }, 1000);
      }
      // Если мяч коснулся левой платформы,
      if (collides(ball, leftPaddle)) {
        // то отправляем его в обратном направлении
        ball.dx *= -1;
        // Увеличиваем координаты мяча на ширину платформы, чтобы не засчитался новый отскок
        ball.x = leftPaddle.x + leftPaddle.width;
      }
      // Проверяем и делаем то же самое для правой платформы
      else if (collides(ball, rightPaddle)) {
        ball.dx *= -1;
        ball.x = rightPaddle.x - ball.width;
      }


      // Рисуем мяч
      context.fillRect(ball.x, ball.y, ball.width, ball.height);
      // Рисуем стены
      context.fillStyle = 'lightgrey';
      context.fillRect(0, 0, canvas.width, grid);
      context.fillRect(0, canvas.height - grid, canvas.width, canvas.height);
      // Рисуем сетку посередине
      for (let i = grid; i < canvas.height - grid; i += grid * 2) {
        context.fillRect(canvas.width / 2 - grid / 2, i, grid, grid);
      }
      // Отслеживаем нажатия клавиш
      document.addEventListener('keydown', function (e) {
        // Если нажата клавиша вверх,
        if (e.which === 38) {
          // то двигаем правую платформу вверх
          rightPaddle.dy = -paddleSpeed;
        }
        // Если нажата клавиша вниз,
        else if (e.which === 40) {
          // то двигаем правую платформу вниз
          rightPaddle.dy = paddleSpeed;
        }
        // Если нажата клавиша W,
        if (e.which === 87) {
          // то двигаем левую платформу вверх
          leftPaddle.dy = -paddleSpeed;
        }
        // Если нажата клавиша S,
        else if (e.which === 83) {
          // то двигаем левую платформу вниз
          leftPaddle.dy = paddleSpeed;
        }
      });
      // А теперь следим за тем, когда кто-то отпустит клавишу, чтобы остановить движение платформы
      document.addEventListener('keyup', function (e) {
        // Если это стрелка вверх или вниз,
        if (e.which === 38 || e.which === 40) {
          // останавливаем правую платформу
          rightPaddle.dy = 0;
        }
        // А если это W или S,
        if (e.which === 83 || e.which === 87) {
          // останавливаем левую платформу
          leftPaddle.dy = 0;
        }
      });
    }
    // Запускаем игру
    requestAnimationFrame(loop);
  </script>
</body>
</html>

Что дальше

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

Но это потом. А сейчас — обыграйте глупую платформу.

Обложка:

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

Корректор:

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

Вёрстка:

Маша Климентьева

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