Пинг-понг против компьютера на JavaScript
Пинг-понг на JavaScript
Пинг-понг против компьютера на JavaScript
Непобедимый пинг-понг на JavaScript Добавляем секретный уровень в пинг-понг на 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>

Что дальше

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

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