From 53187a7cdd89f48e8c1af66f0bf62fb3faabb443 Mon Sep 17 00:00:00 2001 From: "z.kirill" Date: Sun, 16 Aug 2026 02:51:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=83=D0=BD=D0=BA=D1=82=20UPGRADES=20?= =?UTF-8?q?=D0=B2=D1=8B=D1=85=D0=BE=D0=B4=D0=B8=D0=BB=20=D0=B8=D0=B7=20?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D1=8B;=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20(M26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Жалоба была «крашится при открытии апгрейдов». Не крашилось: пункт закрывал игру. В обработчике главного меню стоял `default: ui.quit`, а экран улучшений добавили четвёртым пунктом — обработчик о нём просто не узнал. Снаружи мгновенное закрытие окна неотличимо от падения. Починка на две строки, но интереснее причина, по которой это прожило четыре вехи: в проекте каждая механика меряется прогоном с кодом возврата, а меню не проверял никто. Все инструменты смотрят на СИМУЛЯЦИЮ. Поэтому заведён --ui-check: проходит по всем пунктам меню и требует одного — нажатие ВЕДЁТ КУДА-ТО, а из игры выходит ровно один пункт, QUIT. Плюс проверяет, что кнопки экрана улучшений действительно покупают. Окно при этом не открывается: UiActivate ничего из raylib не трогает. Проверка сперва запущена на СЛОМАННОЙ сборке и дала код 1 с диагнозом «пункт UPGRADES не делает ничего», и только после этого объявлена рабочей. Проверка, которую не видели падающей, ещё ничего не проверяет. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 7 +++- docs/01-build-and-run.md | 2 + docs/07-milestones.md | 2 + src/debug/harness.cpp | 89 ++++++++++++++++++++++++++++++++++++++++ src/debug/harness.h | 5 +++ src/main.cpp | 3 ++ src/ui/menu.cpp | 45 ++++++++++++++------ src/ui/menu.h | 19 +++++++++ 8 files changed, 159 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2edab17..bf080ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,12 @@ переменной `TILE2D_PROJECT`. - Самопроверка: `squad_proto.exe --accept` печатает PASS/FAIL по всем измеримым критериям. Содержимое проверяется отдельно: `tile2d_editor.exe --check` (комнаты и покрытие глубин), - `squad_proto.exe --descend 8` (спуск, эвакуация, круговой прогон профиля). + `squad_proto.exe --descend 8` (спуск, эвакуация, круговой прогон профиля), + `squad_proto.exe --ui-check` (пункты меню: каждый куда-то ведёт). +- **У кнопки тоже есть код возврата.** Пункт UPGRADES четыре вехи закрывал игру + вместо открытия экрана: в обработчике стоял `default: quit`, а новый пункт + добавили четвёртым. Все прогоны меряли симуляцию, меню не проверял никто — + теперь проверяет `--ui-check`, и новый экран обязан появиться в нём. - **Унести можно только то, что на ВЫЖИВШИХ.** Павший теряет своё, вайп теряет всё, достигнутая глубина не теряется никогда ([17-extraction.md](docs/17-extraction.md)). - **`NUM_AGENTS` — это вместимость, а не размер отряда.** Сколько слотов занято, diff --git a/docs/01-build-and-run.md b/docs/01-build-and-run.md index e8a8805..b681a94 100644 --- a/docs/01-build-and-run.md +++ b/docs/01-build-and-run.md @@ -110,6 +110,7 @@ cmake -S . -B build -G "Visual Studio 17 2022" -A x64 | Аргумент | Что делает | |---|---| | `--accept` | прогоняет сценарии критериев приёмки (спек 11) и печатает PASS/FAIL | +| `--ui-check` | пункты меню без окна: каждый ведёт куда-то, выходит только QUIT | | `--headless <сек>` | прогон по маршруту без окна + метрики каждые 10 с | | `--probe` | к `--headless`: подробности по каждому агенту на стоянках | | `--no-solver` | отключить решатель огневых позиций (A/B сравнение) | @@ -142,6 +143,7 @@ cmake -S . -B build -G "Visual Studio 17 2022" -A x64 ```bash squad_proto.exe --accept +squad_proto.exe --ui-check squad_proto.exe --headless 300 squad_proto.exe --headless 300 --no-solver --no-laneavoid # каким прототип был бы без ядра squad_proto.exe --headless 300 --melee 0 # чистый отряд стрелков diff --git a/docs/07-milestones.md b/docs/07-milestones.md index 880b89e..c6a7662 100644 --- a/docs/07-milestones.md +++ b/docs/07-milestones.md @@ -40,6 +40,8 @@ | **M25** | редкости и аффиксы: экземпляр предмета вместо вида (`ItemInstance`), четыре редкости, десять тегов, бюджет и тир по глубине; выкатанное ложится в те же четыре числа бойца и в стихии оружия; инструменты `--tool drops` и `--tool affix` | замеры поймали ТРИ расхождения дизайн-документа с самим собой: формула редкости противоречила своей же таблице (2.7% против 22% на первом этаже), бюджет пускал восемь высших аффиксов вместо обещанных шести, а предельная вещь собиралась к семидесятому этажу вместо сотого — числа пересчитаны, документ исправлен; проверка «доезжает до бойца» показала урон 12→18, здоровье 100→140, броню 0.10→0.25, конверсию KIN 1.00→0.60 при сумме долей ровно 1.000; `--accept` PASS без изменений ([22-loot.md](22-loot.md)) | +| **M26** | `--ui-check`: пункты меню проверяются кодом возврата, как всё остальное | нашлось по жалобе «игра крашится при открытии улучшений»: пункт UPGRADES не крашил, а ВЫХОДИЛ из игры — в обработчике стоял `default: quit`, а экран улучшений добавили четвёртым пунктом, и обработчик о нём не узнал; проверка сперва воспроизведена на сломанной сборке (код 1, «пункт UPGRADES не делает ничего») и только потом объявлена рабочей ([01-build-and-run.md](01-build-and-run.md)) | + ## Что находилось по ходу и чинилось Прогоны без окна оказались полезнее, чем разглядывание кадра: три дефекта, diff --git a/src/debug/harness.cpp b/src/debug/harness.cpp index 8b5eaa9..8fc84f4 100644 --- a/src/debug/harness.cpp +++ b/src/debug/harness.cpp @@ -5,8 +5,10 @@ #include #include #include +#include #include "game.h" +#include "ui/menu.h" // Перевод экранных осей ввода в мировые — единственное место, // где симуляционный слой касается изометрии (см. docs/02-architecture.md). @@ -1557,3 +1559,90 @@ int RunDescend(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t std::printf("\nитог: этажей %d, опыта %.0f, ошибок %d\n", floorsDone, double(xpEarned), bad); return bad == 0 ? 0 : 1; } + +// ----------------------------------------------------------------------------- +// --ui-check: меню без окна. +// +// Проверяет ровно одно утверждение, зато для каждого пункта: НАЖАТИЕ ВЕДЁТ +// КУДА-ТО. Пункт, который не меняет ни экран, ни состояние игры, — это +// неработающая кнопка, и выглядит она в точности как работающая. +// +// Проверка появилась после конкретной поломки: пункт UPGRADES четыре вехи +// закрывал игру. В обработчике стоял `default: quit`, экран улучшений добавили +// четвёртым пунктом, и обработчик о нём не узнал. Для игрока это выглядело как +// падение при открытии улучшений — то есть как ошибка совсем другого рода. +// +// Раньше поймать это было нечем: все прогоны в проекте меряют СИМУЛЯЦИЮ, а +// меню не проверял никто. Отсюда и правило: у поверхности, по которой игрок +// щёлкает, тоже должен быть код возврата. +// +// Окно не открывается: UiActivate ничего из raylib не трогает, он только +// переключает экраны и зовёт методы игры. +// ----------------------------------------------------------------------------- +int RunUiCheck() +{ + Profile profile; + profile.ResetLoadouts(); + profile.bankedXp = 100000.0f; // чтобы покупки на экранах реально проходили + + Game game; + game.profile = &profile; + game.Init(); + + int bad = 0; + const int count = UiMenuItemCount(); + + std::printf("%-4s %-12s %-10s %-10s %s\n", "N", "ПУНКТ", "ЭКРАН ДО", "ПОСЛЕ", "ИТОГ"); + + for (int i = 0; i < count; ++i) + { + Ui ui; + ui.started = true; // «продолжить» имеет смысл только в начатом забеге + const Screen before = ui.screen; + + UiActivate(ui, game, UiPart::MENU_ITEM, i); + + const char* name = UiMenuItemName(i); + const bool isQuit = std::strcmp(name, "QUIT") == 0; + const bool moved = ui.screen != before; + const bool ok = isQuit ? (ui.quit && !moved) : (moved && !ui.quit); + + std::printf("%-4d %-12s %-10d %-10d %s\n", i, name, int(before), int(ui.screen), + ok ? "ok" : "ПОЛОМКА"); + + if (!ok) + { + if (ui.quit && !isQuit) + std::printf(" [FAIL] пункт `%s` выходит из игры\n", name); + else if (!moved) + std::printf(" [FAIL] пункт `%s` не делает ничего\n", name); + else + std::printf(" [FAIL] пункт `%s` ведёт себя не как QUIT\n", name); + ++bad; + } + } + + // Экран улучшений — тот самый, из-за которого проверка и появилась. + // Мало открыть его: покупка обязана работать с той же кнопки. + { + Ui ui; + const int sizeBefore = profile.squadSize; + UiActivate(ui, game, UiPart::UPGRADE_ITEM, int(Upgrade::COUNT)); // строка найма + const bool hired = profile.squadSize > sizeBefore; + + const int lvlBefore = profile.UpgradeLevel(Upgrade::AMMO); + UiActivate(ui, game, UiPart::UPGRADE_ITEM, int(Upgrade::AMMO)); + const bool bought = profile.UpgradeLevel(Upgrade::AMMO) > lvlBefore; + + std::printf("\nулучшения: найм %s, покупка %s\n", hired ? "работает" : "НЕ работает", + bought ? "работает" : "НЕ работает"); + if (!hired || !bought) + { + std::printf(" [FAIL] кнопки экрана улучшений не покупают\n"); + ++bad; + } + } + + std::printf("\nитог: пунктов %d, поломок %d\n", count, bad); + return bad == 0 ? 0 : 1; +} diff --git a/src/debug/harness.h b/src/debug/harness.h index e5ba42f..b8b43f0 100644 --- a/src/debug/harness.h +++ b/src/debug/harness.h @@ -32,3 +32,8 @@ struct HarnessOptions int RunHeadless(const HarnessOptions& opt); int RunAccept(); + +// --ui-check: пункты меню без окна. Проверяет, что каждый пункт КУДА-ТО ведёт и +// что из игры выходит ровно один — QUIT. Появилась после того, как UPGRADES +// четыре вехи молча закрывал игру (ui/menu.h). +int RunUiCheck(); diff --git a/src/main.cpp b/src/main.cpp index d6c50c9..9c224e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,6 +123,7 @@ int main(int argc, char** argv) int shotFrame = -1; const char* shotFile = "shot.png"; bool accept = false; + bool uiCheck = false; bool clean = false; // --clean: стартовать без оверлея (для скриншотов) bool sfTiles = false; // --sf-tiles: пол и стены из библиотеки SpriteForge bool sfEnemies = false; // --sf-enemies: анимированные твари оттуда же @@ -205,6 +206,7 @@ int main(int argc, char** argv) if (i + 1 < argc && argv[i + 1][0] != '-') projectArg = argv[++i]; } 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], "--probe") == 0) harness.probe = true; else if (std::strcmp(argv[i], "--no-solver") == 0) harness.solver = false; else if (std::strcmp(argv[i], "--no-laneavoid") == 0) harness.laneAvoid = false; @@ -288,6 +290,7 @@ int main(int argc, char** argv) if (sfReport) return RunSpriteReport(); if (accept) return RunAccept(); + if (uiCheck) return RunUiCheck(); if (harness.seconds > 0.0f) return RunHeadless(harness); if (descendFloors > 0) { diff --git a/src/ui/menu.cpp b/src/ui/menu.cpp index e39d79a..854fdb5 100644 --- a/src/ui/menu.cpp +++ b/src/ui/menu.cpp @@ -681,11 +681,15 @@ void ApplyDrop(Ui& ui, Game& game, UiPart to, int toIndex) EquipWeapon(game.world, a, inv->weapon); } +} // namespace + // ----------------------------------------------------------------------------- // Действия кнопок. Один вход и для клика, и для ENTER: иначе мышь и клавиатура // начнут делать «почти одно и то же». +// +// Не в анонимном пространстве: этот же вход дёргает `--ui-check` (ui/menu.h). // ----------------------------------------------------------------------------- -void Activate(Ui& ui, Game& game, UiPart part, int index) +void UiActivate(Ui& ui, Game& game, UiPart part, int index) { switch (part) { @@ -709,7 +713,14 @@ void Activate(Ui& ui, Game& game, UiPart part, int index) ui.screen = Screen::DEPTH; break; case 2: ui.screen = Screen::SQUAD; break; - default: ui.quit = true; break; + case 3: ui.screen = Screen::UPGRADE; break; + // QUIT — последний пункт, и попасть сюда можно только им. + // Раньше здесь стоял default, и любой НЕ перечисленный пункт + // выходил из игры: экран улучшений появился четвёртым, а + // обработчик о нём не узнал — нажатие UPGRADES закрывало игру, + // и снаружи это выглядело как падение. + case 4: ui.quit = true; break; + default: break; } return; @@ -749,6 +760,9 @@ void Activate(Ui& ui, Game& game, UiPart part, int index) } } +namespace +{ + bool Pressed(int key) { return IsKeyPressed(key); } // Все списки листаются одинаково: стрелки и WS. @@ -762,6 +776,13 @@ int Step() } // namespace +int UiMenuItemCount() { return MENU_COUNT; } + +const char* UiMenuItemName(int index) +{ + return (index >= 0 && index < MENU_COUNT) ? MENU_ITEMS[index] : "?"; +} + // ----------------------------------------------------------------------------- // Ввод // ----------------------------------------------------------------------------- @@ -833,34 +854,34 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt) if (menuIndex == 0 && !started && d >= 0) menuIndex = 1; if (lmbDown && hot && hot->enabled) - Activate(*this, game, hot->part, hot->index); + UiActivate(*this, game, hot->part, hot->index); else if (Pressed(KEY_ENTER) || Pressed(KEY_SPACE) || Pressed(KEY_KP_ENTER)) - Activate(*this, game, UiPart::MENU_ITEM, menuIndex); + UiActivate(*this, game, UiPart::MENU_ITEM, menuIndex); return true; } case Screen::PAUSE: - if (lmbDown && hot) { Activate(*this, game, hot->part, hot->index); return true; } + if (lmbDown && hot) { UiActivate(*this, game, hot->part, hot->index); return true; } if (Pressed(KEY_ESCAPE)) { screen = Screen::GAME; return true; } if (Pressed(KEY_I) || Pressed(KEY_TAB)) { screen = Screen::SQUAD; return true; } if (Pressed(KEY_Q)) { screen = Screen::MENU; return true; } return true; case Screen::DEFEAT: - if (lmbDown && hot) { Activate(*this, game, hot->part, hot->index); return true; } + if (lmbDown && hot) { UiActivate(*this, game, hot->part, hot->index); return true; } if (Pressed(KEY_ENTER) || Pressed(KEY_SPACE)) - Activate(*this, game, UiPart::DEFEAT_ITEM, 0); + UiActivate(*this, game, UiPart::DEFEAT_ITEM, 0); if (Pressed(KEY_ESCAPE)) screen = Screen::MENU; return true; case Screen::SUMMARY: - if (lmbDown && hot) { Activate(*this, game, hot->part, hot->index); return true; } + if (lmbDown && hot) { UiActivate(*this, game, hot->part, hot->index); return true; } if (Pressed(KEY_ENTER) || Pressed(KEY_SPACE) || Pressed(KEY_ESCAPE)) - Activate(*this, game, UiPart::SUMMARY_ITEM, 0); + UiActivate(*this, game, UiPart::SUMMARY_ITEM, 0); return true; case Screen::UPGRADE: - if (lmbDown && hot) Activate(*this, game, hot->part, hot->index); + if (lmbDown && hot) UiActivate(*this, game, hot->part, hot->index); if (Pressed(KEY_ESCAPE) || Pressed(KEY_U)) screen = Screen::MENU; return true; @@ -874,11 +895,11 @@ bool Ui::Update(Game& game, const ScreenRect& fit, float dt) if (d) depthIndex = (depthIndex + d + shown) % shown; if (depthIndex < 0) depthIndex = 0; - if (lmbDown && hot) { Activate(*this, game, hot->part, hot->index); return true; } + if (lmbDown && hot) { UiActivate(*this, game, hot->part, hot->index); return true; } if (Pressed(KEY_ENTER) || Pressed(KEY_SPACE)) { const Widget& pick = w.item[depthIndex]; - Activate(*this, game, pick.part, pick.index); + UiActivate(*this, game, pick.part, pick.index); return true; } if (Pressed(KEY_ESCAPE)) screen = Screen::MENU; diff --git a/src/ui/menu.h b/src/ui/menu.h index 21f2271..d633398 100644 --- a/src/ui/menu.h +++ b/src/ui/menu.h @@ -107,3 +107,22 @@ struct Ui // Поверх готового кадра. fit — куда лёг фреймбуфер в окне. void Draw(const Game& game, const ScreenRect& fit) const; }; + +// ----------------------------------------------------------------------------- +// Нажать пункт так, как будто по нему кликнули. Наружу вынесено ради ОДНОЙ +// проверки — `--ui-check`, — и вынесено не для полноты API. +// +// Причина конкретная: пункт меню UPGRADES четыре вехи выходил из игры. В +// обработчике стоял `default: quit`, экран улучшений появился четвёртым +// пунктом, и обработчик о нём просто не узнал. Снаружи это выглядело как +// падение игры при открытии улучшений. +// +// Ни один прогон этого не ловил, потому что все проверки в проекте меряют +// СИМУЛЯЦИЮ, а меню не проверял никто. Теперь его щупают так же, как всё +// остальное: код возврата, а не глаз. +void UiActivate(Ui& ui, Game& game, UiPart part, int index); + +// Сколько пунктов в главном меню и как называется i-й. Нужны той же проверке: +// она обязана пройти по ВСЕМ пунктам, а не по тем, о которых знает. +int UiMenuItemCount(); +const char* UiMenuItemName(int index);