Зонд отзывчивости управления --respond (M39)
Требование к строю задано сравнением — вести пятерых должно быть сопоставимо с ведением одного, — и меряться должно так же. Зонд считает три числа в пустом поле и без боя: старт (от нажатия до половины скорости хода), разворот и хвост строя, для одиночки и для пятёрки. Сейчас: один 0.13 / 0.60 / 2.0, пятеро 0.12 / 0.58 / 2.9. Пятеро против одного — старт x0.87, разворот x0.97 при пределе x1.50. Сверх сравнения стоит абсолютный потолок (0.30 с и 1.20 с): без него зонд пропустил бы вязкость, одинаковую для одного и для пятерых — отношение осталось бы единицей, а управление стало бы киселём. Проверено на сломанной настройке: --set steerAccel=3 роняет обе проверки. Первая версия зонда меряла мусор: отряд гнали по карте приёмки, где он через семь тайлов упирался в стену, а «поехал» считалось по ВСЕМ бойцам сразу, поэтому разворот не засчитывался никогда. Меряем в пустом поле и по центру масс. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b29358ad12
commit
9e23c6f980
@ -52,7 +52,9 @@
|
|||||||
- Самопроверка: `squad_proto.exe --accept` печатает PASS/FAIL по всем измеримым критериям.
|
- Самопроверка: `squad_proto.exe --accept` печатает PASS/FAIL по всем измеримым критериям.
|
||||||
Содержимое проверяется отдельно: `tile2d_editor.exe --check` (комнаты и покрытие глубин),
|
Содержимое проверяется отдельно: `tile2d_editor.exe --check` (комнаты и покрытие глубин),
|
||||||
`squad_proto.exe --descend 8` (спуск, эвакуация, круговой прогон профиля),
|
`squad_proto.exe --descend 8` (спуск, эвакуация, круговой прогон профиля),
|
||||||
`squad_proto.exe --ui-check` (пункты меню: каждый куда-то ведёт).
|
`squad_proto.exe --ui-check` (пункты меню: каждый куда-то ведёт),
|
||||||
|
`squad_proto.exe --respond` (отклик строя против одиночки),
|
||||||
|
`squad_proto.exe --no-profile --play 6 5` (отряд сам проходит этажи).
|
||||||
- **У кнопки тоже есть код возврата.** Пункт UPGRADES четыре вехи закрывал игру
|
- **У кнопки тоже есть код возврата.** Пункт UPGRADES четыре вехи закрывал игру
|
||||||
вместо открытия экрана: в обработчике стоял `default: quit`, а новый пункт
|
вместо открытия экрана: в обработчике стоял `default: quit`, а новый пункт
|
||||||
добавили четвёртым. Все прогоны меряли симуляцию, меню не проверял никто —
|
добавили четвёртым. Все прогоны меряли симуляцию, меню не проверял никто —
|
||||||
|
|||||||
@ -112,6 +112,8 @@ cmake -S . -B build -G "Visual Studio 17 2022" -A x64
|
|||||||
| `--accept` | прогоняет сценарии критериев приёмки (спек 11) и печатает PASS/FAIL |
|
| `--accept` | прогоняет сценарии критериев приёмки (спек 11) и печатает PASS/FAIL |
|
||||||
| `--ui-check` | пункты меню без окна: каждый ведёт куда-то, выходит только QUIT |
|
| `--ui-check` | пункты меню без окна: каждый ведёт куда-то, выходит только QUIT |
|
||||||
| `--wall N` | до какой глубины доходит отряд из 1/3/5 бойцов (нужен `--rooms`) |
|
| `--wall N` | до какой глубины доходит отряд из 1/3/5 бойцов (нужен `--rooms`) |
|
||||||
|
| `--play N [размер]` | автопилот: отряд сам идёт к спуску N этажей — единственный прогон, меряющий ПРОХОЖДЕНИЕ |
|
||||||
|
| `--respond` | отзывчивость управления: старт, разворот и хвост строя у одиночки против пятёрки |
|
||||||
| `--headless <сек>` | прогон по маршруту без окна + метрики каждые 10 с |
|
| `--headless <сек>` | прогон по маршруту без окна + метрики каждые 10 с |
|
||||||
| `--probe` | к `--headless`: подробности по каждому агенту на стоянках |
|
| `--probe` | к `--headless`: подробности по каждому агенту на стоянках |
|
||||||
| `--no-solver` | отключить решатель огневых позиций (A/B сравнение) |
|
| `--no-solver` | отключить решатель огневых позиций (A/B сравнение) |
|
||||||
@ -145,6 +147,8 @@ cmake -S . -B build -G "Visual Studio 17 2022" -A x64
|
|||||||
```bash
|
```bash
|
||||||
squad_proto.exe --accept
|
squad_proto.exe --accept
|
||||||
squad_proto.exe --ui-check
|
squad_proto.exe --ui-check
|
||||||
|
squad_proto.exe --respond # отклик строя против одиночки
|
||||||
|
squad_proto.exe --no-profile --play 6 5 # пятёрка проходит шесть этажей
|
||||||
squad_proto.exe --headless 300
|
squad_proto.exe --headless 300
|
||||||
squad_proto.exe --headless 300 --no-solver --no-laneavoid # каким прототип был бы без ядра
|
squad_proto.exe --headless 300 --no-solver --no-laneavoid # каким прототип был бы без ядра
|
||||||
squad_proto.exe --headless 300 --melee 0 # чистый отряд стрелков
|
squad_proto.exe --headless 300 --melee 0 # чистый отряд стрелков
|
||||||
|
|||||||
@ -186,6 +186,20 @@ muzzle = from + normalize(target - from) * muzzleOffset // 0.3 юнита
|
|||||||
цели и только у того, кто сейчас идёт в обход. Средний шаг симуляции — 6.1 мкс
|
цели и только у того, кто сейчас идёт в обход. Средний шаг симуляции — 6.1 мкс
|
||||||
при бюджете кадра 16666 мкс.
|
при бюджете кадра 16666 мкс.
|
||||||
|
|
||||||
|
**Отзывчивость** меряется отдельно (`--respond`), и требование к ней задано
|
||||||
|
сравнением: вести пятерых должно быть сопоставимо с ведением одного.
|
||||||
|
|
||||||
|
| Отряд | старт | разворот | хвост |
|
||||||
|
|---|---|---|---|
|
||||||
|
| один | 0.13 с | 0.60 с | 2.0 |
|
||||||
|
| пятеро | 0.12 с | 0.58 с | 2.9 |
|
||||||
|
|
||||||
|
Пятеро против одного: старт x0.87, разворот x0.97 при пределе x1.50. Сверх
|
||||||
|
сравнения стоит абсолютный потолок (0.30 с и 1.20 с): без него зонд пропустил бы
|
||||||
|
вязкость, одинаковую для одного и для пятерых — отношение осталось бы единицей,
|
||||||
|
а управление стало бы киселём. Проверено: `--set steerAccel=3` роняет обе
|
||||||
|
проверки.
|
||||||
|
|
||||||
**Замеры** (`--play 6 5`, восемь сидов):
|
**Замеры** (`--play 6 5`, восемь сидов):
|
||||||
|
|
||||||
| Сборка | Этажей пройдено |
|
| Сборка | Этажей пройдено |
|
||||||
|
|||||||
@ -2504,6 +2504,158 @@ int RunDescend(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t
|
|||||||
return bad == 0 ? 0 : 1;
|
return bad == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// --respond: ОТЗЫВЧИВОСТЬ управления.
|
||||||
|
//
|
||||||
|
// Требование к строю сформулировано сравнением: вести пятерых должно быть
|
||||||
|
// сопоставимо по отклику с ведением одного. Это не «ощущается лучше», это
|
||||||
|
// измеримо — сколько секунд проходит от нажатия до того, как отряд ПОЕХАЛ.
|
||||||
|
//
|
||||||
|
// Меряются три числа, и каждое отвечает на свой вопрос:
|
||||||
|
//
|
||||||
|
// СТАРТ — от нажатия до половины скорости хода. Сколько отряд «думает».
|
||||||
|
// РАЗВОРОТ — то же самое после смены направления на противоположное. Здесь
|
||||||
|
// видно инерцию: строй разворачивается тяжелее, чем трогается.
|
||||||
|
// ХВОСТ — насколько отстал самый дальний боец на установившемся ходу.
|
||||||
|
// Отряд, у которого хвост в пять тайлов, отзывчивым не ощущается,
|
||||||
|
// даже если центр масс тронулся мгновенно.
|
||||||
|
//
|
||||||
|
// Замер в ПУСТОЙ комнате и без боя: отклик управления не должен зависеть от
|
||||||
|
// того, кто сейчас стреляет.
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
struct RespondResult
|
||||||
|
{
|
||||||
|
float start = 0.0f;
|
||||||
|
float turn = 0.0f;
|
||||||
|
float tail = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
RespondResult MeasureRespond(int squadSize)
|
||||||
|
{
|
||||||
|
Profile profile;
|
||||||
|
profile.ResetLoadouts();
|
||||||
|
profile.squadSize = std::max(1, std::min(NUM_AGENTS, squadSize));
|
||||||
|
|
||||||
|
Game game;
|
||||||
|
game.profile = &profile; // размер отряда берётся ОТСЮДА (Game::Init)
|
||||||
|
game.Init();
|
||||||
|
|
||||||
|
// Чистое поле: отклик управления не должен зависеть ни от стен, ни от того,
|
||||||
|
// кто сейчас стреляет. Иначе замер поймает не строй, а расстановку мишеней.
|
||||||
|
for (int y = 1; y < MAP_H - 1; ++y)
|
||||||
|
for (int x = 1; x < MAP_W - 1; ++x) game.map.Set(x, y, Tile::EMPTY);
|
||||||
|
game.targets.clear();
|
||||||
|
|
||||||
|
game.PlaceSquad({float(MAP_W / 2) + 0.5f, float(MAP_H / 2) + 0.5f}, false);
|
||||||
|
|
||||||
|
const float half = 0.5f * g_tune.moveSpeed;
|
||||||
|
|
||||||
|
auto drive = [&](Vec2 worldDir, float seconds, float* timeToHalf) {
|
||||||
|
const int steps = int(seconds / FIXED_DT);
|
||||||
|
float elapsed = 0.0f;
|
||||||
|
Vec2 prev = game.squad.Centroid();
|
||||||
|
if (timeToHalf) *timeToHalf = -1.0f;
|
||||||
|
for (int i = 0; i < steps; ++i)
|
||||||
|
{
|
||||||
|
game.in = InputState{};
|
||||||
|
game.in.moveScreen = v2d::WorldDirToScreen(worldDir);
|
||||||
|
game.Step(FIXED_DT);
|
||||||
|
elapsed += FIXED_DT;
|
||||||
|
|
||||||
|
// «Отряд поехал» — это про ЦЕНТР МАСС, а не про переднего бойца:
|
||||||
|
// строй, у которого один улетел, а четверо стоят, отзывчивым не
|
||||||
|
// является, и центр это показывает сам собой.
|
||||||
|
const Vec2 now = game.squad.Centroid();
|
||||||
|
const float speed = Dot((now - prev) * (1.0f / FIXED_DT), worldDir);
|
||||||
|
prev = now;
|
||||||
|
if (timeToHalf && *timeToHalf < 0.0f && speed >= half) *timeToHalf = elapsed;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RespondResult r;
|
||||||
|
drive({1.0f, 0.0f}, 3.0f, &r.start);
|
||||||
|
drive({-1.0f, 0.0f}, 3.0f, &r.turn);
|
||||||
|
|
||||||
|
// Хвост на установившемся ходу: самый дальний от якоря.
|
||||||
|
float worst = 0.0f;
|
||||||
|
for (int k = 0; k < NUM_AGENTS; ++k)
|
||||||
|
{
|
||||||
|
const Agent& a = game.squad.agents[k];
|
||||||
|
if (!a.alive) continue;
|
||||||
|
worst = std::max(worst, Distance(a.pos, game.squad.anchor));
|
||||||
|
}
|
||||||
|
r.tail = worst;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int RunRespond()
|
||||||
|
{
|
||||||
|
std::printf("отзывчивость управления: от нажатия до половины скорости хода\n\n");
|
||||||
|
std::printf("%-8s %10s %10s %8s\n", "ОТРЯД", "СТАРТ", "РАЗВОРОТ", "ХВОСТ");
|
||||||
|
|
||||||
|
const RespondResult solo = MeasureRespond(1);
|
||||||
|
const RespondResult five = MeasureRespond(5);
|
||||||
|
|
||||||
|
auto line = [](const char* name, const RespondResult& r) {
|
||||||
|
std::printf("%-8s %9.2fс %9.2fс %7.1f\n", name, double(r.start), double(r.turn),
|
||||||
|
double(r.tail));
|
||||||
|
};
|
||||||
|
line("один", solo);
|
||||||
|
line("пятеро", five);
|
||||||
|
|
||||||
|
int bad = 0;
|
||||||
|
|
||||||
|
// Ни один из замеров не должен быть «не случилось»: -1 означает, что отряд
|
||||||
|
// за три секунды так и не разогнался до половины скорости.
|
||||||
|
if (solo.start < 0.0f || five.start < 0.0f || solo.turn < 0.0f || five.turn < 0.0f)
|
||||||
|
{
|
||||||
|
std::printf("\n [FAIL] отряд не разогнался до половины скорости за три секунды\n");
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Порог сравнения, а не абсолютный: требование к строю задано именно
|
||||||
|
// сравнением с одиночкой. Полуторный запас — на то, что пятерым надо
|
||||||
|
// ещё и разойтись по слотам, и это честная цена строя.
|
||||||
|
const float startRatio = five.start / std::max(0.01f, solo.start);
|
||||||
|
const float turnRatio = five.turn / std::max(0.01f, solo.turn);
|
||||||
|
std::printf("\nпятеро против одного: старт x%.2f, разворот x%.2f (предел x1.50)\n",
|
||||||
|
double(startRatio), double(turnRatio));
|
||||||
|
if (startRatio > 1.5f || turnRatio > 1.5f)
|
||||||
|
{
|
||||||
|
std::printf(" [FAIL] строй отзывается заметно хуже одиночки\n");
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Абсолютный потолок — поверх сравнения. Без него зонд пропустил бы
|
||||||
|
// вязкость, одинаковую для одного и для пятерых: отношение осталось бы
|
||||||
|
// единицей, а управление стало бы киселём. Числа с запасом вдвое к
|
||||||
|
// нынешним (0.12 и 0.58): это порог «стало плохо», а не цель.
|
||||||
|
if (five.start > 0.30f || five.turn > 1.20f)
|
||||||
|
{
|
||||||
|
std::printf(" [FAIL] управление вязкое само по себе: старт %.2f с (предел 0.30), "
|
||||||
|
"разворот %.2f с (предел 1.20)\n", double(five.start), double(five.turn));
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хвост: строй тянется за курсором, но не растягивается в цепочку.
|
||||||
|
if (five.tail > 4.0f)
|
||||||
|
{
|
||||||
|
std::printf(" [FAIL] хвост строя %.1f тайла — отряд идёт цепочкой, а не строем\n",
|
||||||
|
double(five.tail));
|
||||||
|
++bad;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::printf("\nитог: поломок %d\n", bad);
|
||||||
|
return bad == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// --ui-check: меню без окна.
|
// --ui-check: меню без окна.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -38,6 +38,11 @@ int RunAccept();
|
|||||||
// четыре вехи молча закрывал игру (ui/menu.h).
|
// четыре вехи молча закрывал игру (ui/menu.h).
|
||||||
int RunUiCheck();
|
int RunUiCheck();
|
||||||
|
|
||||||
|
// --respond: отзывчивость управления строем. Требование задано СРАВНЕНИЕМ —
|
||||||
|
// вести пятерых должно быть сопоставимо с ведением одного, — и меряется так же:
|
||||||
|
// время от нажатия до половины скорости у одиночки и у пятёрки.
|
||||||
|
int RunRespond();
|
||||||
|
|
||||||
// --wall N: до какой глубины доходит отряд разной силы. Отвечает на вопрос,
|
// --wall N: до какой глубины доходит отряд разной силы. Отвечает на вопрос,
|
||||||
// который появился вместе с чекпоинтами через 50 этажей: растёт ли достижимая
|
// который появился вместе с чекпоинтами через 50 этажей: растёт ли достижимая
|
||||||
// глубина от вылазки к вылазке (docs/16-descent.md).
|
// глубина от вылазки к вылазке (docs/16-descent.md).
|
||||||
|
|||||||
@ -124,6 +124,7 @@ int main(int argc, char** argv)
|
|||||||
const char* shotFile = "shot.png";
|
const char* shotFile = "shot.png";
|
||||||
bool accept = false;
|
bool accept = false;
|
||||||
bool uiCheck = false;
|
bool uiCheck = false;
|
||||||
|
bool respond = false;
|
||||||
bool clean = false; // --clean: стартовать без оверлея (для скриншотов)
|
bool clean = false; // --clean: стартовать без оверлея (для скриншотов)
|
||||||
bool sfTiles = false; // --sf-tiles: пол и стены из библиотеки SpriteForge
|
bool sfTiles = false; // --sf-tiles: пол и стены из библиотеки SpriteForge
|
||||||
bool sfEnemies = false; // --sf-enemies: анимированные твари оттуда же
|
bool sfEnemies = false; // --sf-enemies: анимированные твари оттуда же
|
||||||
@ -225,6 +226,7 @@ int main(int argc, char** argv)
|
|||||||
}
|
}
|
||||||
else if (std::strcmp(argv[i], "--accept") == 0) accept = true;
|
else if (std::strcmp(argv[i], "--accept") == 0) accept = true;
|
||||||
else if (std::strcmp(argv[i], "--ui-check") == 0) uiCheck = true;
|
else if (std::strcmp(argv[i], "--ui-check") == 0) uiCheck = true;
|
||||||
|
else if (std::strcmp(argv[i], "--respond") == 0) respond = true;
|
||||||
else if (std::strcmp(argv[i], "--probe") == 0) harness.probe = true;
|
else if (std::strcmp(argv[i], "--probe") == 0) harness.probe = true;
|
||||||
else if (std::strcmp(argv[i], "--no-solver") == 0) harness.solver = false;
|
else if (std::strcmp(argv[i], "--no-solver") == 0) harness.solver = false;
|
||||||
else if (std::strcmp(argv[i], "--no-laneavoid") == 0) harness.laneAvoid = false;
|
else if (std::strcmp(argv[i], "--no-laneavoid") == 0) harness.laneAvoid = false;
|
||||||
@ -309,6 +311,7 @@ int main(int argc, char** argv)
|
|||||||
if (sfReport) return RunSpriteReport();
|
if (sfReport) return RunSpriteReport();
|
||||||
if (accept) return RunAccept();
|
if (accept) return RunAccept();
|
||||||
if (uiCheck) return RunUiCheck();
|
if (uiCheck) return RunUiCheck();
|
||||||
|
if (respond) return RunRespond();
|
||||||
if (harness.seconds > 0.0f) return RunHeadless(harness);
|
if (harness.seconds > 0.0f) return RunHeadless(harness);
|
||||||
if (playFloors > 0)
|
if (playFloors > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user