# ИИ соперников Go Volt для разработчика

Текущий водитель — детерминированный автомат состояний с геометрическим следованием FAN маршруту. Он выдаёт команды управления, а движение, сцепление и удары рассчитывает общее нативное PC ядро. Трафик, датчики препятствий и применение оружия подключены; выбора альтернативных FAN ветвей, поиска бонусов и catch up в водителе нет.

Описание соответствует рабочему дереву `main` на 4 октября 2026 года. Оно объясняет фактически подключённый код порта и границу его источников. Версия без технических деталей — [AI_EXPLAINED_RU.md](AI_EXPLAINED_RU.md).

## Происхождение и статус алгоритма

Полный самостоятельный retail PC водитель в доступном архиве не восстановлен. Порт использует согласованное исключение из [AI_SOURCE.md](AI_SOURCE.md): адаптацию автомобильного AI из `original/Xbox/FrontEndSrc` и сверочной `original/Xbox/Src`. Это разрешённый Xbox донор, а не подтверждение точного retail PC AI. Автомобильная физика, CPU граница управления, конфигурации и ресурсы трасс рассматриваются отдельно по [SOURCE_POLICY.md](SOURCE_POLICY.md) и [PC_BASELINE.md](PC_BASELINE.md).

Игровая цепочка донора достижима: `GLP_GameLoop` вызывает физический цикл, тот обновляет AI часы и вызывает `CON_DoPlayerControl`; CPU путь приходит в `CAI_Process`, затем в `CAI_ProcessCall`. Из последнего вызываются обновление данных, состояние гонки, оружие и коррекции. В `FrontEndSrc` ориентиры: `gameloop.cpp:354, 371, 1093–1096`; `control.cpp:82–87`; `ai_car.cpp:1887–1894, 1910, 1967, 2016–2017, 2069, 2072–2078`. В `Src`: `gameloop.cpp:431, 1336–1339`; `control.cpp:84–86`; `ai_car.cpp:1923, 1932, 1950, 2007, 2057, 2109, 2112–2118`.

Инициализация также существует: `main.cpp` задаёт AI часы, `player.cpp` вызывает `CAI_InitCarAI`, которая задаёт начальное состояние. `OLD_AI_VERSION` в `ai_car.h` не включён, отладочный переключатель `gGazzasOvertake` инициализирован `FALSE`. При этом `_PC` определён в сохранённых `revolt.h`, рядом используется XTL, а `Src/sources.inc` задаёт Xbox приложение. Такие PC фрагменты внутри донора не доказывают shipping цепочку полного retail PC водителя.

| Часть | Статус и граница |
| --- | --- |
| Интерполированная цель, форма руля, отдельные AI часы, состояния гонки и полёта | Адаптация активного разрешённого AI донора |
| `SetupCar`, CPU знаковый квадрат руля, автомобильные силы и материалы | Удерживаемая поздняя PC граница; донорские настройки машин не подставляются |
| FAN, TAZ, PAN и финальные `Parameters.txt` | PC данные; интерпретация и преобразование единиц выполняются портом |
| Кривизна и запас сцепления для планирования скорости | Собственный защитный планировщик порта |
| Ограниченная величина коррекции руля при заносе, лучи, поиск проекции и перестановка сценой | Адаптации порта |
| Момент использования оружия | Политика Xbox донора с перечисленными ниже изменениями |

Аудит `audit_late_pc_ai.py` закрепляет 14 копий выбранных функций и PC границу входа. Различия этих копий: в `Src` есть clamp руля только для `_N64` и явные преобразования в `char`; `FrontEndSrc` полагается на присваивание. Основные выбранные функции гонки, полёта и часов совпадают. Аудит не покрывает весь AI, все оружейные функции или все константы адаптера и не заменяет проверку поведения.

## Компоненты и цепочка вызовов

| Файл | Функции и строки | Ответственность |
| --- | --- | --- |
| [race_scene.gd](../port/godot/scripts/race_scene.gd) | `_spawn_opponents` 351, `_step_vehicles_shared` 588, `_update_opponents` 512 | Создание машин, общий шаг, передача наблюдений и команд |
| [pc_ai_driver.gd](../port/godot/scripts/pc_ai_driver.gd) | `configure` 114, `will_decide` 230, `advance_physics` 236, `advance` 253 | Планировщик решений, маршрут и автомат состояний |
| [pc_ai_sensors.gd](../port/godot/scripts/pc_ai_sensors.gd) | `observe` 6, `handling_limits` 38, `_clearance` 58 | Соседи, боковое скольжение, живой запас сцепления и лучи |
| [legacy_race_route.gd](../port/godot/scripts/legacy_race_route.gd) | `load_track` 23, `parse_fan_bytes` 118, `_main_route` 223 | Чтение PC маршрутных файлов и основного кольца |
| [pc_weapon_ai.gd](../port/godot/scripts/pc_weapon_ai.gd) | `should_fire` 38 | Условия применения уже полученного предмета |
| [pc_pickups.gd](../port/godot/scripts/pc_pickups.gd) | `consider_ai` 364, `before_substep` 225, `after_substep` 295 | Общий инвентарь, сбор, запрос огня и жизненный цикл |
| [pc_weapons.gd](../port/godot/scripts/pc_weapons.gd) | `target_for` 367 | Общий выбор ракетной цели и исполнение оружия |

```mermaid
flowchart TD
    A[Общий физический шаг 8 мс] --> B[Гоночная сессия и обновление соперников]
    B --> C{Нужно новое решение}
    C -->|Да| D[Снимок датчиков и advance]
    C -->|Нет| E[Сохранённый output]
    D --> F{Запрошено восстановление}
    E --> F
    F -->|Да| G[Сцена выбирает свободную позицию]
    F -->|Нет| H[set_controls и consider_ai]
    H --> I[Подготовка PC управления и бонусов]
    G --> I
    I --> J[Парные контакты и нативная симуляция машин]
```

В Single Race `_advance_race_step` обновляет сессии и AI перед подготовкой нативного подшага. Затем сцена подготавливает управление, обновляет объекты и оружие, решает взаимные контакты машин и выполняет физику. Все машины получают общий план подшагов. Обычные самостоятельные `_physics_process` машин выключены при создании гонки.

В `_spawn_opponents` создаются три автомобиля из трёх следующих записей каталога после выбранной игроком машины, циклически. Для каждого создаются отдельные водитель и гоночная сессия. Начальный максимум передаётся как `config.top_speed * 0.44704`. До старта и после собственного финиша соперник получает нулевой газ и включённый тормоз. В Time Trial соперники не создаются.

## Контракт и единицы

`advance_physics(position, forward, velocity, grounded_wheels, delta, body_contacts, upright_dot, observation)` принимает мировые Godot векторы, метры, метры в секунду и секунды. `grounded_wheels` находится в диапазоне 0..4; `upright_dot` — скалярное произведение оси вверх автомобиля с мировой вертикалью. Скалярные наблюдения и геометрия проверяются на конечность; невалидное решение останавливает машину и переводит сцену в обработку ошибки.

Основной выход содержит `throttle`, `steer`, `brake`, `reverse`, `recovery`, `state`, `target`, `target_speed`, `route_index`, `route_distance`, `lookahead`, `wrong_way` и диагностические признаки. `throttle` и `steer` нормированы к диапазону −1..1. Фактически сцена передаёт в автомобиль только `throttle`, `steer`, `brake`. Поле `reverse` описывает режим; заднее движение запрашивается отрицательным газом. Поле `recovery` обрабатывает сама сцена.

`brake=true` в этом автомобильном API означает ручник с блокировкой задних колёс. Для обычного замедления водитель оставляет его `false` и использует отрицательный запрос двигателя.

Пространство PC переводится в Godot масштабом `0.005`, то есть 200 исходных единиц на метр, с преобразованием осей `(x, y, z) → (x, −y, −z)`. Пределы FAN в mph переводятся множителем `0.44704`. `RacingLineSpeed` сохраняется как исходный рейтинг 0..100; текущий водитель не превращает его в mph. Точное исходное преобразование этого рейтинга в скорость не восстановлено.

## Частота решений

Физика выполняется с подшагом 8 мс. Новый AI output рассчитывается с интервалом `DECISION_INTERVAL = 0.033` с; внутренние таймеры решения увеличиваются на `DECISION_DELTA = 1/30` с. Это сохраняет различие донора между целым `1000/30 = 33` мс и временем `ONE/30`.

`will_decide` — чистый запрос расписания. Сцена вызывает датчики только когда предстоит решение. `advance_physics` уменьшает остаток времени, возвращает прежний словарь целиком на промежуточных шагах и вызывает `advance` при наступлении срока. После задержки не создаётся очередь решений по устаревшему снимку.

Следовательно, таймеры состояния, цель, обнаружение препятствия и изменение эффективного максимума скорости видят мир только на новом решении. Комментарий внутри `advance` о чтении значения «каждый кадр» не отменяет удержания output. При 8 мс решения попадают на сетку подшагов; это не ровно один вызов каждые четыре шага. В проверке 20 секунд получается 607 решений на 2500 физических шагов.

Оружейный `consider_ai` вызывается сценой на подшаге с последним выходом водителя и текущим оружейным статусом. Его частота отдельно не ограничена расписанием водительских решений.

## Представление маршрута

`load_track` объединяет FAN навигацию, TAZ контрольные зоны, PAN прогресс, INF старт и TRI триггеры. Водитель получает массив `main_route_points` и соответствующие FAN узлы. FAN основной цикл строится по `Next[0]`; другие связи читаются как данные, но водитель не выбирает их во время гонки.

Для каждого узла есть две границы и доли `racing_line`, `overtaking_line`. Точки линий строятся как `boundary[1].lerp(boundary[0], fraction)`. Во время движения сначала смешиваются гоночная и обгонная точки по `_line_blend`, затем интерполируется цель внутри отрезка.

Привязка использует горизонтальную проекцию на сегмент с ограничением доли к 0..1. Оценка кандидата равна квадрату расстояния плюс `0.01 * (1 − forward.dot(segment_direction))`. Добавка только разрывает почти равные варианты у пересечения. Обычный поиск охватывает 4 сегмента позади, текущий и 18 впереди. Если найденный путь дальше 8 м, выполняется полный поиск кольца.

Внутренняя `route_distance` водителя измеряется по горизонтальным длинам базовых сегментов. Её нельзя без преобразования смешивать с длиной loader в 3D, сохранёнными дистанциями FAN или PAN дистанцией до финиша.

Обычная дальность цели задаётся выражением `lookahead = 3 + 0.1 * horizontal_speed` в метрах. При коррекции используется цель на 1 м впереди. Датчики получают цели через `get_sensor_targets` до текущего `advance`, поэтому их геометрия использует последнее сохранённое положение по маршруту и предыдущую дальность обзора.

## Руление и PC граница ввода

Для горизонтального направления на цель вычисляется знаковый угол `e = atan2(desired.dot(right), desired.dot(forward))`. Коэффициент живой машины из `handling_limits` равен `G = 45 / abs(degrees(FL.SteerRatio))`, то есть половине `90/fullLockAngle` из `SetupCar` и активной `CAI_ProcessCarSteering`.

Обычная команда имеет вид:

```text
u = sign(e) * min(1, sqrt(max(0, 1 - abs(cos(e)))) * G)
steer = clamp(round(16 * u) / 16, -1, 1)
```

Порт использует симметричную квантизацию 1/16. Это адаптация байтовой квантизации донора, не побайтная сборка исходного контроллера. Затем поздняя PC CPU граница применяет знаковый квадрат `steer * abs(steer)` и штатное изменение угла колёс. Маршрутный вход проходит этот квадрат один раз.

В маршрутных fixtures без автомобильных наблюдений существуют значения по умолчанию `STEER_CONVERSION = 1.15`, `LATERAL_ACCELERATION = 4.5`. Живая сцена передаёт параметры конкретной машины; эти значения по умолчанию не являются общей настройкой всех соперников.

## Планирование скорости и газа

Обычный предел берётся прежде всего из `observation.physics_top_speed_mps`. Сцена передаёт текущий нативный `get_report().top_speed_mps` и в этот ключ, и в `weapon_top_speed_mps`. Поэтому учитываются реальные изменения режима машины и активных эффектов. Без первого ключа используется максимум начального TopSpeed и положительного оружейного значения.

Явные поддержанные FAN приоритеты сохраняют предел `min(initial_top_speed, mph * 0.44704)`:

| Priority | Предел mph | Предел м/с до ограничения машиной |
| ---: | ---: | ---: |
| 4 | 25 | 11.1760 |
| 17 | 15 | 6.7056 |
| 18 | 20 | 8.9408 |
| 19 | 30 | 13.4112 |

`_speed_cap_ahead` выбирает минимум поддержанных ограничений в пределах 12 м по маршруту. Остальные типы Priority не становятся новыми ограничениями автоматически.

Планировщик кривизны — адаптация порта: вход в отдельное состояние тормозной зоны у донора отключён `if (0)`. Его нельзя описывать как перенесённую рабочую тормозную зону оригинала.

`handling_limits` вычисляет бюджет ускорения по слабейшему из четырёх текущих колёс:

```text
oil_i = 0.5 * (oil_time_i + 0.2) / 2.7, если oil_time_i < 2.5
oil_i = 1, иначе
a = max(0.001, min_i(kinetic_friction_i * roughness_i * oil_i) * gravity * 0.005)
```

Единица `a` — м/с². Это консервативная оценка при статической гравитационной нагрузке; аэродинамический прижим не добавляется. Например, у травы `roughness` равен 0.7; другое поле материала `Gripiness` нельзя подставлять вместо него.

Кривизна оценивается по двум соседним горизонтальным хордам длиной примерно 2 м. Для смещений `d = 0, 2, 4, 6, 8` м берутся точки `s+d`, `s+d+2`, `s+d+4`, то есть горизонт достигает 12 м. `k = angle / max(0.5, mean_chord_length)`, где угол в радианах, `k` в 1/м. Ограничение для каждого изгиба:

```text
v_limit = sqrt(0.5 * a / k + a * d)
target = min(route_cap, все v_limit)
heading_scale = clamp(1.15 - 0.45 * abs(e), 0.35, 1)
target_speed = clamp(target * heading_scale, min(3, target), target)
```

Половина бюджета оставлена для поворота, половина — для замедления. Кривизна считается по гоночной линии, а не по текущей смешанной линии обгона. При большом боковом удалении, оставшемся больше 8 м даже после полного поиска, цель скорости дополнительно ограничивается 5 м/с.

Если выбранный датчик вернул конечную дистанцию `c`, добавляется предел `max(0, (c − 0.25) / 0.35)` м/с. Он может снизить цель до нуля.

При `speed > target_speed + 0.1` и скорости вперёд больше 0.5 м/с команда двигателя равна `−clamp((speed − target_speed) / max(1, 0.25 * target_speed), 0.2, 1)`. Если скорость вперёд мала, подаётся ноль. В остальных случаях подаётся полный газ 1; дальнейшая коррекция заноса может уменьшить положительный запрос. Сам TopSpeed — параметр возможностей двигателя, а не гарантия достижения ровно этой скорости.

## Датчики препятствий

`_clearance` выполняет по три луча к каждой из двух авторских линий. Лучи идут по центру и со смещением на половину колеи; начало поднято на 0.04 м от позиции автомобиля. Длина — `clamp(length(linear_velocity) * 0.35 + 0.7, 0.7, 5)` м. Здесь используется полная пространственная скорость, в отличие от горизонтальной скорости основной цели.

Препятствием считается попадание с `abs(normal.dot(UP)) < 0.5`. Пологая поверхность не включается в эту оценку. Возвращается минимальная дистанция подходящего попадания или `INF` при его отсутствии.

Лучевая маска трассы — слой 1; автомобили находятся на слое 2. Поэтому эти лучи проверяют окружение, а не прогнозируют столкновения с автомобилями. Соседи передаются отдельно через `peers`.

Исходное `ProcessNearWallAvoidance` предсказывало положение машины и сравнивало его с плоскостями границ FAN узлов; порт заменил этот механизм шестью лучами геометрии. Адреса донора: `FrontEndSrc/ai_car.cpp:2420`, `Src/ai_car.cpp:2460`.

`_choose_avoidance_line` берёт предпочитаемую обгонной логикой линию. Если она перекрыта, а другая свободна либо имеет запас больше на 0.25 м, выбирается другая крайняя линия. Иначе сохраняется плавная смесь. Для ограничения скорости clearance выбирается дискретно: обгонная линия при смеси не меньше 0.5, гоночная иначе. Swept volume и проверка всей промежуточной смешанной траектории не выполняются.

## Трафик и обгон

`_update_traffic` выбирает ближайшего подходящего лидера по следующим условиям:

1. Горизонтальное прямое расстояние больше 0.001 м и не больше 4 м; смещение направлено вперёд относительно автомобиля.
2. При наличии зон лидер находится в той же TAZ зоне или следующей с циклическим переходом.
3. Положительный разрыв по маршруту больше 0.001 м и меньше 2.5 м.

Если переданы круги обеих машин, контракт умеет считать абсолютный разрыв по кругам. Текущий `_observe_driver` передаёт зоны и состояние обгона, но не `lap` и не `peer.route_distance`. В живой гонке используется локальная проекция соседа и циклическая разность дистанций. Скорость соседа передаётся и валидируется, но не участвует в выборе лидера или оценке сближения.

Если лидер обгоняет, destination ставится в 0, иначе в 1. Для AI признак поступает из последнего output; для игрока он оценивается по близости к двум линиям. Таймер снова ставится в 4 с при каждом обнаружении лидера; без лидера уменьшается, после истечения destination возвращается в 0.

`line_blend` меняется через `move_toward` со скоростью 0.1 в секунду внутреннего времени решений. Полный переход 0→1 занял бы около 10 с. Флаг `overtaking` означает намерение или ненулевую смесь, а не факт завершённого обгона. Отдельной проверки бокового свободного коридора, относительной скорости или стратегии нескольких обгонов нет.

## Коррекция заноса

`observe` берёт точки левого переднего и левого заднего колеса, индексы 0 и 2. Скорость каждой точки равна `linear_velocity + angular_velocity.cross(world_offset)`. Боковая составляющая измеряется вдоль направления поперёк шины с учётом текущего поворота колеса. Это измерения двух точек, не усреднение всех колёс оси.

Коррекция допускается при минимум трёх опирающихся колёсах и горизонтальной скорости не меньше 2 м/с. Для боковых скоростей `f` и `r` вычисляется:

```text
excess = abs(r) - abs(f) - 0.35, если sign(f) == sign(r)
excess = abs(f - r) - 0.35, иначе
reference = f, если abs(f) > 0.05, иначе r
c = sign(reference) * min(0.3, excess / max(speed, 2))
wheel_target = clamp(current_normalized_wheel_angle + c, -1, 1)
steer = sign(wheel_target) * sqrt(abs(wheel_target))
```

При неположительном excess или слишком малом reference поправки нет. Ограничение 0.3 относится к пространству нормализованного угла колёс. Обратный знаковый корень нужен, чтобы последующая PC квадратичная граница не уменьшила поправку повторно.

Положительный газ умножается на `1 − min(0.65, 1.5 * abs(c))`. При текущем ограничении `abs(c) ≤ 0.3` максимальное снижение составляет 45 процентов. После этой коррекции финальная команда руля уже не обязана быть кратной 1/16. Величины коррекции — адаптация порта, без скрытого переноса Xbox таблиц управляемости.

## Автомат состояний и восстановление

Сначала обновляются таймеры проблем и проверяется необходимость `reposition`; затем состояния исправления направления и полёта; обычные скорость и руль вычисляются, если ранний выход не произошёл.

| Условие | Порог | Действие |
| --- | --- | --- |
| `forward.dot(route_tangent) < −0.15` | 0.35 с | Из `race` в `reverse_correct` |
| Контакт кузова и скорость меньше 3 mph | 0.25 с | Из `race` в `reverse_correct` |
| Нет ни одного опирающегося колеса | В `race` | Перейти в `airborne`; газ 1, руль 0 |
| Возобновился контакт колеса | В `airborne` | Вернуться в `race` |
| Исправление задним ходом | 0.9 с | Газ −0.7 и руль против сохранённого знака; затем `forward_correct` |
| Исправление вперёд | До 1.4 с или dot больше 0.55 | Газ 0.65 и руль по сохранённому знаку; затем `race` |
| Не вышел из области вокруг anchor на 0.75 м | 2.5 с | Запрос `recovery` |
| Боковая дистанция до маршрута больше 12 м | 1 с | Запрос `recovery` |
| `upright_dot < 0.25` | 1.2 с | Запрос `recovery` |

Таймеры выше — внутреннее время решений по 1/30 с. Знак коррекции хранится и обновляется на каждом решении по ошибке направления или стороне относительно маршрута, в том числе во время исправления задним ходом и вперёд. Порог anchor измеряет физическое горизонтальное перемещение, поэтому вращение колёс на месте не считается прогрессом.

Сцена дополнительно восстанавливает машину ниже нижней границы трассы на 15 м. `_recover_opponent` не начисляет новый прогресс: выбирает район последней принятой TAZ зоны, ресинхронизирует сессию и бонусы, сбрасывает водительские таймеры и интерполяцию.

`_vacant_recovery_pose` пробует 13 позиций: исходную и смещения назад по 0.75 м с повторной привязкой к маршруту. Требуется поверхность с `normal.dot(UP) ≥ 0.5` и дистанция до других машин больше суммы радиусов. Радиус определяется по оболочке кузова с запасом 0.05 м. Если всё занято, перестановка откладывается.

## Политика применения оружия

`consider_ai` использует общий слот инвентаря и общую `weapons.target_for`. Статус эффектов сливается с контекстом TAZ зоны, ширины FAN и wall flags. При положительном `should_fire` вызывается общий `request_fire`; действительный выстрел выполняется системой бонусов. После успешного AI применения действует отдельный cooldown 0.35 с.

Общие проверки: предмет есть; он удерживается минимум 0.4 с; состояние `race`; нет `recovery` и `wrong_way`; минимум три колеса опираются; `upright_dot ≥ 0.7`; `power_remaining ≤ 0`. Для поиска ближайшей машины и допустимой цели используется та же или соседняя TAZ зона, циклически в обе стороны. Дистанции оружейной политики пространственные, не расстояния по маршруту.

| Предмет | Условие применения после общих проверок |
| --- | --- |
| Shockwave | Ближайшая машина в передней полуплоскости ближе 20 м |
| Firework и Firework Pack | Общая выбранная цель находится впереди; дистанция строго между 1.5 и 20 м |
| Water Bomb | Ближайшая передняя машина строго между 1 и 20 м |
| Electro Pulse | Машина впереди ближе 2.5 м или позади ближе 3 м; электропульс ещё не активен |
| Oil | При собственной скорости больше 1 м/с машина позади в коридоре до 6 м, с боковым смещением меньше 1 м и вертикальным меньше 0.5 м; либо условие узкой дороги |
| Chrome Ball и Clone | Машина позади ближе 3 м; либо условие узкой дороги |
| Turbo | Скорость больше 1 м/с, газ больше 0.6, `abs(steer) < 0.12`, dot направления на цель не меньше 0.9 |
| Global | Применить при выполнении общих проверок |
| Putty Bomb | Не нажимать повторно; общая PC выдача активирует бомбу при получении |

Условие узкой дороги: скорость строго больше 10 mph, ширина не больше 4 м, а внутри этого диапазона либо ширина не больше 2 м, либо есть левый или правый wall flag.

Основной донорский обработчик — `CAI_ProcessWeapons`, `FrontEndSrc/ai_car.cpp:3054` и `Src/ai_car.cpp:3097`; просмотренные тела совпадают. Внутри донора 200 единиц соответствуют метру. Активные расстояния 20, 1.5, 1, 2.5 и 3 м и выравнивание Turbo 0.9 объясняют основу политики. Её явные отличия:

- Проверки удержания, опоры, вертикальности и cooldown добавлены портом.
- Коридор масла расширен до 6 м; условие близкой задней машины соединяется с условием узкой дороги через OR, тогда как у донора есть `else`.
- Ракета проверяет расстояние до реально выбранной цели; донор проверяет наличие цели вместе с дистанцией ближайшего переднего автомобиля.
- Донор блокирует атаки на `PLAYER_LOCAL` при `Players[0].car.LastHitTimer ≤ 10 с`; это исключение для игрока в текущую политику не перенесено.
- Индексы и снимки состояния заменяют донорские указатели; исполнение и эффекты остаются в общей PC системе порта.

Ракетная цель выбирается общим `target_for`: вертикальная компонента смещения умножается на 5, затем действуют направленный диапазон и score, повышающий вес подходящего направления. Это отдельная операция от выбора момента огня. Сбор предметов общий для всех машин, но вероятности выдачи не одинаковы: текущий `pc_pickup_state.get_weight_table` использует для CPU фиксированную PC строку, для игрока — интерполяцию по месту в гонке.

## Границы текущего поведения

Водитель не обучается, не использует нейросеть или A star, не назначает индивидуальные характеры соперникам. Различия движения возникают из настроек машин и наблюдений. Он не меняет физические силы ради удержания темпа игрока и не реализует catch up. Настоящие эффекты предметов могут менять возможности машины через общую симуляцию.

Он не выбирает `Next[1]`, не прокладывает путь к pickup и не использует `RacingLineSpeed` как восстановленную скорость. Обгон реагирует на близость, без проверки относительной скорости и бокового коридора. Датчики окружения не покрывают всю траекторию кузова. Автомобиль может сорваться с маршрута после удара; восстановление остаётся отдельной адаптацией сцены.

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

Исторические разделы `AI_SOURCE.md` про первые инкременты содержат фразу об ещё не реализованном оружии. К текущему порту она не относится: цепочка `consider_ai → should_fire → request_fire` подключена. Старый отчёт не должен переопределять текущую реализацию.

## Проверка описанной реализации

4 октября 2026 года существующие проверки были повторно выполнены без изменения игровой логики:

| Проверка | Что она проверяет | Результат |
| --- | --- | --- |
| `audit_late_pc_ai.py --check` | 14 выбранных копий функций, часы и PC CPU граница | Код возврата 0 |
| `ai_driver_smoke.gd` | Маршрут, команды и состояния водителя | 0 failures |
| `ai_traffic_smoke.gd` | Обгон, наблюдения и препятствия | 0 failures |
| `weapon_ai_smoke.gd` | Условия применения предметов и границы | 0 failures |
| `ai_weapon_speed_smoke.gd` | Активный максимум скорости и маршрутные ограничения | 0 failures |
| `ai_late_physics_smoke.gd` | Формулы, PC машины и реальные поездки по nhood1 | 42 703 checks, 0 failures |

Последний сценарий использует доступные нативные `RVRuntime`, `RVVehicleSimulation` и подготовленные PC ресурсы. На прямой игрок с полным газом и водитель AI дают совпадающие скорости и позиции в пределах проверки для RC Bandit, Dust Mite, Phat Slug и Col. Moss. За 20 секунд каждый водитель делает 607 решений на 2500 физических шагов. В отдельных поездках по `nhood1` все четыре обходятся без восстановления и режимов коррекции. Полный многомашинный трёхкруговой заезд в рамках подготовки этих документов не повторялся.

Пример запуска из корня проекта:

```powershell
& 'C:\Users\gboyd\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'tools/audit_late_pc_ai.py' --check
& '.\.tools\godot\Godot_v4.7.2-stable_win64_console.exe' --headless --path '.\port\godot' --script 'res://tests/ai_late_physics_smoke.gd'
```

Остальные сценарии запускаются заменой имени после `res://tests/`. Результаты этого прогона сохранены в `.build/ai-documentation-checks/`. Тесты показывают соответствие перечисленным сценариям, а не полноту восстановления retail PC AI.

## Исходники для дальнейшей работы

- [pc_ai_driver.gd](../port/godot/scripts/pc_ai_driver.gd): 230–250 расписание, 253–395 решение и состояния, 455–500 трафик, 503–516 выбор линии, 519–546 занос, 549–570 проекция, 577–588 выбор точки, 591–631 планировщик скорости, 638–645 обычный предел, 648–653 маршрутный руль.
- [pc_ai_sensors.gd](../port/godot/scripts/pc_ai_sensors.gd): 6–35 наблюдения, 38–55 управление и сцепление, 58–82 лучи.
- [race_scene.gd](../port/godot/scripts/race_scene.gd): 351–400 состав соперников, 512–579 подключение решений и наблюдений, 588–687 общий шаг, 861–892 восстановление, 941–971 поиск свободного места.
- [pc_weapon_ai.gd](../port/godot/scripts/pc_weapon_ai.gd): 38–107 политика, 125–230 близость, цель, коридор, ширина и зоны.
- [FrontEndSrc ai_car.cpp](../original/Xbox/FrontEndSrc/ai_car.cpp) и [Src ai_car.cpp](../original/Xbox/Src/ai_car.cpp): разрешённый AI донор; адреса активной цепочки и оружия указаны выше.
- [FIX_18_7_AI.md](FIX_18_7_AI.md), [audit_late_pc_ai.py](../tools/audit_late_pc_ai.py) и [late_pc_ai_manifest.json](baseline/late_pc_ai_manifest.json): история адаптации и точная граница текущего аудита.
