---
title: "Пинг-понг для двоих на JavaScript: ИИ соперника с задержкой реакции и честная подача"
description: "Как собрать пинг-понг в браузере на JavaScript: управление для двоих на одной клавиатуре через KeyboardEvent.code, отскоки и ускорение мяча с потолком скорости, подача к проигравшему и компьютерный соперник, который предсказывает точку прилёта с отскоками, но реагирует с задержкой и ошибается. Три уровня сложности с измеренной долей отбитых мячей, код проверен в Node.js 24."
slug: ping-pong-na-javascript-ii-sopernika-i-podacha
date: 2026-11-05
updated: 2026-11-05
scheduled_at: "2026-11-05T19:00:00+03:00"
rubric: praktika-i-uroki
tags: ["ping-pong", "ii-vragov", "javascript", "shkolnikam", "novichkam"]
reading_time: 10
draft: false
summary: "Пинг-понг собирается из поля, двух ракеток, мяча с отскоками и счёта. Самое интересное — компьютерный соперник: он должен предсказывать, где мяч пересечёт его линию с учётом отскоков от стен, но смотреть на мяч не каждый кадр, двигаться с ограниченной скоростью и ошибаться тем сильнее, чем быстрее мяч. В нашем прогоне на 1000 ударов лёгкий соперник отбивает 89% медленных и 54% быстрых мячей, а сложный — 100% и 89%: проиграть ему можно, но непросто."
---

## Коротко

Пинг-понг для двоих — это поле, две ракетки, мяч с отскоками, подача и счёт, а для игры против компьютера ещё и соперник, который предсказывает точку прилёта мяча с отскоками. Чтобы соперника можно было обыграть, он смотрит на мяч раз в 0,1–0,35 с, двигает ракетку с ограниченной скоростью и ошибается сильнее на быстрых мячах. Ниже — параметры поля, формула предсказания, таблица трёх уровней сложности с измеренной долей отбитых мячей и код, проверенный в Node.js 24.

## Содержание

1. Поле, ракетки и счёт
2. Управление для двоих на одной клавиатуре
3. Отскоки, ускорение и потолок скорости
4. Подача к проигравшему
5. Предсказание точки прилёта с отскоками
6. Соперник, которого можно обыграть
7. Таблица: три уровня сложности
8. Код на JavaScript
9. Практический сценарий: игра за вечер
10. Когда вам это не подойдёт

## Поле, ракетки и счёт

Для примера возьмём поле 800×450 пикселей, ракетки 12×80 у левого и правого края и мяч радиусом 8 пикселей. Ракетка правого игрока стоит передней гранью на x = 760, левого — на x = 52. Игра идёт до 11 очков; это наш выбор для короткой партии, его легко поменять. Очко получает игрок, мимо которого не пролетел мяч.

## Управление для двоих на одной клавиатуре

Двум игрокам на одной клавиатуре дают разнесённые клавиши: левому — `KeyW` и `KeyS`, правому — `ArrowUp` и `ArrowDown`. Проверять нужно `KeyboardEvent.code`, а не `key`: по MDN, `code` описывает физическую клавишу и не зависит от раскладки, поэтому на AZERTY, где клавиша с кодом `KeyQ` печатает A, управление останется на тех же местах. Подробнее о слое действий и геймпадах — в статье про ввод, ссылка в конце.

Держите набор нажатых кодов и двигайте ракетку на `скорость × dt` в кадре, а не на фиксированный шаг по событию нажатия: событие повтора клавиши приходит с задержкой, и движение было бы рывками.

## Отскоки, ускорение и потолок скорости

От верхней и нижней стены мяч отражается сменой знака вертикальной скорости, а от ракетки — сменой знака горизонтальной и новым углом, который зависит от точки удара: чем ближе к краю ракетки, тем круче. Этот приём из аркадного Pong разобран в статье об истории механик 2D-игр.

После каждого удара скорость растёт на 5% до потолка 900 пикс/с. Потолок защищает от туннелирования:

```text
шаг за кадр = 900 пикс/с ÷ 60 = 15 пикс
защита      = толщина ракетки + диаметр мяча = 12 + 16 = 28 пикс
15 < 28 — мяч не проскочит ракетку
```

## Подача к проигравшему

Подача идёт из центра поля к игроку, который проиграл очко, под случайным углом не круче 30° и после паузы в 1 секунду. Пауза даёт время вернуть взгляд к центру, а ограничение угла исключает подачу почти вертикально, когда мяч долго скачет между стенами. Стартовая скорость после подачи сбрасывается к начальной — 360–450 пикс/с.

## Предсказание точки прилёта с отскоками

Точку, где мяч пересечёт линию ракетки, считают одной формулой: где был бы мяч без стен, а затем «складывают» путь по высоте поля:

```text
t      = (x_ракетки − x_мяча) / vx
сырой  = y_мяча + vy × t − r
пролёт = H − 2r
m      = ((сырой mod 2·пролёт) + 2·пролёт) mod 2·пролёт
y      = r + m,             если m ≤ пролёт
y      = r + 2·пролёт − m,  иначе
```

Для мяча с x = 160, y = 100 и скоростью (300; −300) формула даёт y = 368, и пошаговая симуляция с шагом 1/6000 с приходит туда же.

## Соперник, которого можно обыграть

Идеальное предсказание делает соперника непобедимым, поэтому ему добавляют три ограничения:

1. **Задержка реакции.** Цель пересчитывается не каждый кадр, а раз в `reaction` секунд. Между пересчётами соперник едет к старой цели.
2. **Предельная скорость ракетки.** Если мяч прилетает далеко от ракетки, соперник не успевает.
3. **Ошибка прицела.** К предсказанию добавляется случайное смещение до `error` пикселей, умноженное на отношение скорости мяча к 360 пикс/с, — быстрые мячи он читает хуже.

Когда мяч летит от соперника, тот возвращается в центр: это и выглядит «по-человечески», и оставляет шанс сыграть в угол.

## Таблица: три уровня сложности

Параметры ниже — наш стартовый набор; доля отбитых ударов измерена в коде из этой статьи на 1000 ударах игрока из-под своей ракетки, под углом до 45°, с ракеткой соперника в случайной точке.

| Уровень | Реакция | Скорость ракетки | Ошибка | Отбито при 450 пикс/с | при 750 | при 900 |
|---|---|---|---|---|---|---|
| Лёгкий | 0,35 с | 260 пикс/с | 50 пикс | 89% | 61% | 54% |
| Средний | 0,20 с | 380 пикс/с | 30 пикс | 100% | 90% | 78% |
| Сложный | 0,14 с | 460 пикс/с | 26 пикс | 100% | 96% | 89% |

Медленный мяч средний и сложный соперники отбивают всегда, поэтому выигрывать у них нужно скоростью и углом. Если игроки жалуются, что сложный «читерит», увеличьте реакцию или ошибку, а не снижайте скорость ракетки — иначе он будет двигаться неестественно медленно.

## Код на JavaScript

Код содержит предсказание точки прилёта, подачу, соперника с тремя ограничениями и прогон, который измеряет долю отбитых ударов. Генератор случайных чисел с зерном делает проверку повторяемой. Код проверен в Node.js 24, вывод совпадает с комментариями.

```js
const W = 800, H = 450, R = 8, PADDLE_H = 80;
const AI_X = 760; // передняя грань ракетки соперника

// Детерминированный генератор, чтобы проверки повторялись
function rng(seed) {
  return () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// Где мяч пересечёт линию x = targetX с учётом отскоков от верха и низа
function predictY(ball, targetX) {
  const t = (targetX - ball.x) / ball.vx;
  const raw = ball.y + ball.vy * t - R;
  const span = H - 2 * R;
  const m = ((raw % (2 * span)) + 2 * span) % (2 * span); // «складываем» путь
  return R + (m <= span ? m : 2 * span - m);
}

// Подача: к проигравшему очко, угол не круче 30°
function serve(rand, towardRight, speed) {
  const a = (rand() * 2 - 1) * (Math.PI / 6);
  const dir = towardRight ? 1 : -1;
  return { x: W / 2, y: H / 2 + (rand() * 2 - 1) * 100, vx: dir * speed * Math.cos(a), vy: speed * Math.sin(a) };
}

// Уровни сложности соперника
const LEVELS = {
  easy: { reaction: 0.35, maxSpeed: 260, error: 50 },
  normal: { reaction: 0.2, maxSpeed: 380, error: 30 },
  hard: { reaction: 0.14, maxSpeed: 460, error: 26 },
};

// Соперник «смотрит» на мяч раз в reaction секунд и ошибается тем сильнее, чем быстрее мяч
function makeAI(level, rand) {
  return { y: H / 2, target: H / 2, timer: 0, level, rand };
}

function updateAI(ai, ball, dt) {
  const L = ai.level;
  ai.timer -= dt;
  if (ai.timer <= 0) {
    ai.timer = L.reaction;
    if (ball.vx > 0) {
      const speed = Math.hypot(ball.vx, ball.vy);
      const err = (ai.rand() * 2 - 1) * L.error * (speed / 360);
      ai.target = predictY(ball, AI_X) + err;
    } else {
      ai.target = H / 2; // мяч летит к игроку — возвращаемся в центр
    }
  }
  const d = ai.target - ai.y;
  if (Math.abs(d) > 4) ai.y += Math.sign(d) * Math.min(Math.abs(d), L.maxSpeed * dt);
}

// Удар игрока: мяч уходит от его ракетки (x = 52) под углом до 45°
function playerShot(rand, speed) {
  const a = (rand() * 2 - 1) * (Math.PI / 4);
  return { x: 52, y: R + rand() * (H - 2 * R), vx: speed * Math.cos(a), vy: speed * Math.sin(a) };
}

// Прогон: какую долю ударов игрока соперник отбивает
function hitRate(levelName, speed, serves, seed) {
  const rand = rng(seed);
  let hits = 0;
  for (let s = 0; s < serves; s++) {
    const ball = playerShot(rand, speed);
    const ai = makeAI(LEVELS[levelName], rand);
    ai.y = R + rand() * (H - 2 * R); // ракетка там, где осталась после прошлого розыгрыша
    const dt = 1 / 60;
    while (ball.x < AI_X) {
      updateAI(ai, ball, dt);
      ball.x += ball.vx * dt;
      ball.y += ball.vy * dt;
      if (ball.y < R) { ball.y = 2 * R - ball.y; ball.vy = -ball.vy; }
      if (ball.y > H - R) { ball.y = 2 * (H - R) - ball.y; ball.vy = -ball.vy; }
    }
    if (Math.abs(ball.y - ai.y) <= PADDLE_H / 2 + R) hits++;
  }
  return Math.round((hits / serves) * 100);
}

// Проверка предсказания против пошаговой симуляции с мелким шагом
const b = { x: 160, y: 100, vx: 300, vy: -300 };
const sim = { ...b };
while (sim.x < AI_X) {
  const dt = 1 / 6000;
  sim.x += sim.vx * dt; sim.y += sim.vy * dt;
  if (sim.y < R) { sim.y = 2 * R - sim.y; sim.vy = -sim.vy; }
  if (sim.y > H - R) { sim.y = 2 * (H - R) - sim.y; sim.vy = -sim.vy; }
}
console.log(predictY(b, AI_X), Math.round(sim.y)); // 368 368

for (const name of ['easy', 'normal', 'hard']) {
  console.log(name, hitRate(name, 450, 1000, 7) + '%', hitRate(name, 750, 1000, 7) + '%', hitRate(name, 900, 1000, 7) + '%');
}
// easy 89% 61% 54%
// normal 100% 90% 78%
// hard 100% 96% 89%
```

Функция `serve` в прогоне не используется — она нужна в самой игре после каждого очка.

## Практический сценарий: игра за вечер

Пинг-понг собирается за 6 шагов:

1. **Поле и отрисовка.** Холст 800×450, ракетки и мяч прямоугольниками, счёт сверху по центру.
2. **Ввод.** Набор нажатых `code`, движение ракеток на `скорость × dt`, ограничение по краям поля.
3. **Мяч.** Движение на `v × dt`, отскок от стен, отскок от ракеток с углом по точке удара и ускорением 5% до 900 пикс/с.
4. **Очко и подача.** Мяч ушёл за край — очко сопернику, пауза 1 с, подача к проигравшему.
5. **Компьютер.** Вместо управления правой ракеткой с клавиатуры вызывайте `updateAI`; уровень выбирается в меню.
6. **Проверка.** Запустите прогон `hitRate` после каждого изменения параметров, чтобы сложность не уехала незаметно.

## Когда вам это не подойдёт

Схема «предсказание плюс ограничения» хороша для пинг-понга и похожих игр, но не для всех:

- **Соперник с несколькими объектами:** в хоккее или футболе сверху нужно выбирать, за кем следить и куда бежать, — это задача для деревьев поведения или выбора по полезности.
- **Физика с вращением мяча:** если мяч закручивается и траектория искривляется, формула со «складыванием» перестаёт работать, и точку прилёта ищут пошаговой симуляцией вперёд.
- **Сетевая игра для двоих:** задержка сети требует отдельной синхронизации мяча, и локальной логики из этой статьи недостаточно.

Мы разрабатываем **J2DRU** — браузерный 2D игровой движок, где управление с клавиатуры, мыши, геймпада и касанием входит в проверенные внутри разработки основы, а клавиши можно переназначать. Движок в разработке и публично пока не открыт, заявку можно оставить на [странице раннего доступа](/early-access?utm_source=blog&utm_medium=article&utm_campaign=ping-pong-na-javascript-ii-sopernika-i-podacha) (подача заявки не гарантирует приглашение).

**Источники (проверено: ноябрь 2026):** [MDN — KeyboardEvent: code property](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code) (физическая клавиша, пример KeyQ на AZERTY). Параметры поля, уровни сложности и доля отбитых ударов — наш пример и наши измерения в Node.js 24.

**Читайте также:**
- [От первых аркад и «Змейки» до инди-хитов: история ключевых механик 2D-игр](/blog/istoriya-klyuchevyh-mehanik-2d-igr)
- [Действия ввода вместо клавиш: KeyboardEvent.code, Gamepad API, мёртвая зона](/blog/vvod-klaviatura-gejmpad-kasanie-v-2d-igre)
- [От Breakout до Arkanoid: история арканоидов](/blog/istoriya-arkanoidov-ot-breakout-do-arkanoid-1986)
