squad-proto/docs/09-acceptance.md
z.kirill 7880f275bd Прототип боевого ядра: отряд, хоррор-слой, ближний бой
Тактический отряд с автоматическим огнём в изометрии, собственный
софтверный растеризатор (raylib только окно, ввод и финальный блит).

Ядро (спек, раздел 6):
- LaneClear: проверка линии огня по своим, стенам и дальности;
- решатель огневых позиций с гистерезисом и коммитом;
- резервирование линий огня и расступание из чужих секторов;
- пули-снаряды, дружественный урон физически возможен.

Хоррор-слой (render-only): свет и туман войны с памятью карты,
светящиеся трассеры, кровь, виньетка, зерно, напряжение.

Ближний бой: сектор удара, три фазы, связка из трёх ударов,
своя дисциплина «не бить сквозь своего».

Проверка: squad_proto.exe --accept прогоняет критерии приёмки
раздела 11 плюс блок M7 по ближнему бою — все PASS,
FRIENDLY_HITS = 0 за 3 минуты боя.

Сборка: cmake -S . -B build && cmake --build build --config Release
raylib 6.0 подтягивается через FetchContent.

Документация — CLAUDE.md как оглавление, содержание в docs/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 13:00:49 +03:00

108 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 09 — Критерии приёмки
Раздел 11 спека. Воспроизвести: `build/bin/squad_proto.exe --accept`.
Сценарии — [src/debug/harness.cpp](../src/debug/harness.cpp), `RunAccept()`.
## Автоматическая проверка
Критерии 1, 6, 7, 8 описывают **правила стрельбы** и меряются на отряде из пяти
стрелков — так, как их формулировал спек. Ближний бой проверяется отдельным
блоком M7 и, главное, трёхминутным прогоном 11.2, где состав отряда обычный,
боевой (2 клинка + 3 ствола).
| # | Критерий | Статус | Цифры последнего прогона |
|---|---|---|---|
| 1 | ≥4 из 5 агентов имеют линию за ≤1.5 с после остановки | **PASS** | settle = 0.32 с, линий 5/5 |
| 2 | `FRIENDLY_HITS` = 0 за 3 минуты боя во всех режимах | **PASS** | 0 попаданий, смешанный отряд |
| 4 | агент с чужой линии огня уходит за ~0.3 с | **PASS** | максимум 0.35 с на арене (0.98 с по всем сценариям) |
| 5 | нет дёрганья: `postPos` не чаще раза в 0.6 с | **PASS** | 36 коммитов, 0 нарушений, минимальный интервал 0.75 с |
| 6 | в проходе 2 тайла — колонна, огонь не идёт сквозь своих | **PASS** | до 5 агентов в створе, поперёк 3.09 / вдоль 6.08 тайла, вперёд передний/хвост 77/9, сквозь союзника 0 выстрелов из 247 |
| 7 | ручной режим никогда не стреляет сквозь своих | **PASS** | 0 попаданий по своим за 30 с, назначенная цель 96 % времени |
| 8 | `FIRE UPTIME` в стоячем бою на арене ≥ 80 % | **PASS** | 100 % |
| 9 | стабильные 60 fps | **PASS** | симуляция 2.5 мкс + рендер 4.1 мс при бюджете 16.6 мс, fps 60 |
Ближний бой ([11-melee.md](11-melee.md)), сценарий 30 с у кучки мишеней:
| # | Критерий | Статус | Цифры |
|---|---|---|---|
| M7.1 | клинок доходит до цели и наносит урон | **PASS** | 55 взмахов, 52 попадания, 16 убийств, 14 оборвано своими; связка `slash 21 / backhand 19 / spin 15` |
| M7.2 | ближний бой не задевает своих | **PASS** | `FRIENDLY_HITS = 0` при 55 взмахах |
| M7.3 | у каждого класса свой круг вокруг диска | **PASS** | стрелки не дальше 3.49 от диска, клинки — 5.61 (лимиты 3.70 / 6.80) |
| M7.4 | лезвие не достаёт за пределы своей дуги | **PASS** | превышение сектора 0.0° (лимит 12°) |
M7.4 появился после починки геометрии удара: угловой допуск `asin(r/dist)`
вырождался на контактной дистанции в 90°, и боец рубил за спину — те самые
«удары на 360 градусов». Теперь проверка идёт по отрезку лезвия, а критерий
замеряет превышение разрешённого сектора и не даёт багу вернуться
([11-melee.md](11-melee.md)).
M7.2 держится не тюнингом, а конструкцией: урон применяется только к мишеням, а
влезший под лезвие союзник обрывает взмах ([08-decisions.md](08-decisions.md), A16).
Прогон 300 с (`--headless 300`), смешанный отряд: `FRIENDLY_HITS = 0`,
1471 выстрел, 142 взмаха / 146 попаданий лезвием, **63 убийства**.
Тот же маршрут чистым отрядом стрелков (`--melee 0`): 2100 выстрелов,
45 убийств. Клинки заметно поднимают урон отряда, не ломая главного правила.
`FIRE UPTIME` на стоянках маршрута: 82.9 / 93.2 / 90.7 / 86.0 % там, где есть
мишени, и 23.3 % на стоянке, где стрелять не по чему.
Тест 11.6 переписан ещё раз, и на этот раз по существу — с подсчёта «кто стрелял»
на проверку инварианта.
Причина была в другом месте, чем казалось. Сценарий тащил курсор непрерывно,
курсор обгонял отряд, натяжение поводка упиралось в упор, включался спринт — а в
спринте отряд по замыслу не стреляет вовсе. Тест показывал «0 выстрелов вперёд» и
падал, проверяя пустой коридор. После поднятия `sprintAt` до 0.85 удержание
направления спринт больше не включает, и отряд идёт через проход **с боем**
как и должен.
Область замера тоже была неверной: проход глубиной три тайла, а бойцов пятеро,
и колонна физически длиннее самого прохода — внутрь него целиком она не
помещается никогда. Меряется теперь створ прохода с подходами по обе стороны,
на всей проводке, а не только на стоянке.
Заодно выброшен критерий «передний стреляет чаще заднего». В проходе шириной два
тайла у заднего есть законные диагональные линии мимо переднего, и требовать их
отсутствия — значит требовать от отряда быть глупее, чем он есть. «Задние не
стреляют» — это следствие правила «не сквозь своего», а не отдельное правило.
Поэтому проверяется сам инвариант: берётся отрезок **реально сделанного**
выстрела и независимой геометрией проверяется, не задел ли он тело союзника.
Результат: 0 из 247 выстрелов прошли сквозь своего, колонна 6.08 тайла вдоль
против 3.09 поперёк, и передний всё равно ведёт огонь с перевесом 77 против 9 —
теперь это наблюдение, а не условие прохождения.
## Проверяется глазами / по коду
| # | Критерий | Как подтверждено |
|---|---|---|
| 3 | в движении стреляют только те, у кого чистая линия | по построению: спуск требует `hasLane`; в оверлее `F2` видно, что часть агентов идёт с жёлтой линией и молчит |
| 10 | вся сцена нарисована своим растеризатором | в `render/` нет ни одного raylib-примитива; raylib вызывается только в `ScreenBlitter` (`UpdateTexture` + `DrawTexturePro`) и в `debug/overlay.cpp`, который dev-only и снимается по `F1` |
Скриншот боевого кадра ([docs/combat.png](combat.png)): отряд в пятне света,
у каждого своя зелёная линия, ни одна не проходит через своего, трассеры в полёте,
дальние мишени видны только тлеющими глазами. `FRIENDLY_HITS 0`.
## Оговорки, которые стоит знать
- Критерий 1 и метрика `SETTLE TIME` осмысленны только когда в радиусе есть живые
мишени. В общем маршруте попадаются значения до 5.7 с — это остановки там, где
стрелять не по чему, а не медленное расхождение.
- Критерий 8 меряется в стоячем бою рядом с кучкой. При остановке на краю
дальности честные значения ниже: часть агентов физически не достаёт цель.
После сокращения дальности до 7.2 худшая стоянка маршрута даёт ~30 % — это
остановка там, где кучка уже вне радиуса, а не деградация решателя.
- `FRIENDLY_HITS = 0` достигается связкой из четырёх вещей: резерв линий, включая
перекрытые своим; резерв шире порога блокировки; учёт времени полёта на спуске;
пуля не летит дальше проверенного отрезка. Отключение любой из них возвращает
попадания по своим — цифры в [08-decisions.md](08-decisions.md).
- `FIRE UPTIME` усредняется только по стрелкам, `MELEE reach` — только по клинкам.
Смешивать доли времени двух разных занятий в одно среднее бессмысленно.
- «Макс. время в чужой линии» на смешанном отряде доходит до 3.9 с против 1.3 с
на чистых стрелках. Это боец с клинком, который стоит вплотную к мишени и тем
самым перекрывает стрелку линию: расступиться он не может, не бросив свою
работу. Правило при этом не нарушается — стрелок просто не стреляет и
перерешивает позицию. Критерий 11.4 меряется на открытой арене чистым отрядом
стрелков, где такой ситуации нет по построению.