From 80822b74b115c5af2f129565225420dc4d8065e2 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Tue, 18 Aug 2026 13:54:48 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D1=80=D1=88=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B8=D0=BD=D0=B6=D0=B5=D0=BD=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D1=83=D1=8E=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D1=8D=D1=82=D0=B0=D0=BF=D0=B0=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .beads/interactions.jsonl | 2 + .beads/issues.jsonl | 7 +- README.md | 8 +- app/build.gradle | 4 +- .../game/GameRestartTest.java | 82 +++++++++++++++++++ .../game/LateLevelPlaythroughTest.java | 57 +++++++++++++ .../game/LateLevelScenarios.java | 70 +++++++++++++--- .../game/LevelEightScenario.java | 26 +++--- .../com/projectsacrifice/game/GameView.java | 53 +++++++++++- .../projectsacrifice/game/LevelCatalog.java | 12 +-- .../game/LevelPlaytestBot.java | 1 + .../game/LevelCatalogTest.java | 45 +++++++++- .../game/LevelPlaytestBotTest.java | 4 + docs/BEADS_WORKFLOW.md | 4 +- docs/CURRENT_ROADMAP.md | 23 ++---- docs/LEVEL_SOLUTION_GRAPHS.md | 20 ++--- docs/STAGE2_QA_REPORT.md | 35 ++++++++ 17 files changed, 384 insertions(+), 69 deletions(-) create mode 100644 app/src/androidTest/java/com/projectsacrifice/game/GameRestartTest.java create mode 100644 docs/STAGE2_QA_REPORT.md diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index b1e3b90..68e9c7d 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -21,3 +21,5 @@ {"id":"int-ab10aee3","kind":"field_change","created_at":"2026-08-18T01:30:37.522248Z","actor":"E.Gavrilov","issue_id":"goon-game-level6-freezer-overlap","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"FREEZER больше не убивает игрока на frozen stepping stone; floor lethality сохранена; уровень 6 проходит 3/3."}} {"id":"int-129af3b6","kind":"field_change","created_at":"2026-08-18T01:30:42.004043Z","actor":"E.Gavrilov","issue_id":"goon-game-level8-softlock","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Expiry справа от PIT воспроизводился; recovery relay и баланс таймера проверены intended 3/3 и recovery 3/3."}} {"id":"int-2dabc667","kind":"field_change","created_at":"2026-08-18T01:30:46.06691Z","actor":"E.Gavrilov","issue_id":"goon-game-bots-6-10","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Уровни 6–10 проходят intended routes по 3 раза; уровни 8 и 10 дополнительно проходят 96ms stress checks."}} +{"id":"int-2cbd2a1d","kind":"field_change","created_at":"2026-08-18T10:41:46.8206Z","actor":"E.Gavrilov","issue_id":"goon-game-level10-sliding-corpse","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Acceptance met: delivered body remains on lift plate, signal stays active, normal 3x and delayed-input level 10 routes pass."}} +{"id":"int-a046aa8d","kind":"field_change","created_at":"2026-08-18T10:41:48.065633Z","actor":"E.Gavrilov","issue_id":"goon-game-level-route-risks","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"All intended risky routes are reproduced by connected bots; possible L2 alternative is explicitly accepted and documented as non-blocking."}} diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index fe0b608..ef6a132 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,9 +1,10 @@ +{"_type":"issue","id":"goon-game-level10-sliding-corpse","title":"Остановить скольжение тела на плите уровня 10","description":"Брошенное пилой тело уровня 10 может продолжать скользить по полу и съезжать с PRESSURE lift, из-за чего движущаяся платформа теряет сигнал и intended route становится нестабильным.","acceptance_criteria":"После доставки тело стабильно остаётся на PRESSURE lift без ручного тайминга; сигнал lift не пропадает при длительном ожидании; уровень 10 проходит normal и delayed-input маршруты по 3 раза без дополнительных смертей.","notes":"2026-08-18: focused RED showed firstCorpseX drift 791.4791 -\u003e 866.5033 in 750 ms. Grounded bodies now settle fully inside PRESSURE, stop, and pin. GREEN: position stable, lift powered after 6 s, exactly 1 death/body; level 10 delayed run and 3 consecutive mastery runs passed; final unfiltered matrix 16/16.","spec_id":"docs/LEVEL_SOLUTION_GRAPHS.md","status":"closed","priority":0,"issue_type":"bug","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-18T08:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T10:41:47Z","started_at":"2026-08-18T08:40:02Z","closed_at":"2026-08-18T10:41:47Z","close_reason":"Acceptance met: delivered body remains on lift plate, signal stays active, normal 3x and delayed-input level 10 routes pass.","labels":["bug","current","level10","physics"],"dependencies":[{"issue_id":"goon-game-level10-sliding-corpse","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T11:40:01Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-level6-freezer-overlap","title":"Исправить повторную смерть на frozen corpse уровня 6","description":"Frozen corpse уровня 6 фиксируется на y=776..880, а FREEZER остаётся y=750..880; игрок на теле занимает y=676..776 и пересекает hazard на 26 px, поэтому intended stepping-stone route вызывает немедленную вторую смерть.","acceptance_criteria":"Игрок может стоять и прыгнуть с предусмотренного frozen corpse без повторной FREEZE-смерти; вход в FREEZER с пола всё ещё убивает; intended route уровня 6 проходит три раза подряд production-input ботом.","spec_id":"docs/LEVEL_SOLUTION_GRAPHS.md","status":"closed","priority":0,"issue_type":"bug","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T22:46:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:30:37Z","started_at":"2026-08-17T22:46:52Z","closed_at":"2026-08-18T01:30:37Z","close_reason":"FREEZER больше не убивает игрока на frozen stepping stone; floor lethality сохранена; уровень 6 проходит 3/3.","labels":["bug","current","freeze","level6"],"dependencies":[{"issue_id":"goon-game-level6-freezer-overlap","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T01:46:51Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-level8-softlock","title":"Устранить softlock таймера уровня 8","description":"Если clock истекает после пересечения PIT, дверь закрывается, платформа останавливается, а переключатель остаётся слева: игрок может оказаться заперт справа до restart.","acceptance_criteria":"Бот воспроизводит expiry после PIT; игрок всегда имеет путь к restart-free recovery или таймер/геометрия исключают ловушку; сценарий проходит 3 раза подряд.","spec_id":"docs/LEVEL_SOLUTION_GRAPHS.md","status":"closed","priority":0,"issue_type":"bug","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T22:09:40Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:30:42Z","started_at":"2026-08-17T22:33:08Z","closed_at":"2026-08-18T01:30:42Z","close_reason":"Expiry справа от PIT воспроизводился; recovery relay и баланс таймера проверены intended 3/3 и recovery 3/3.","labels":["bug","current","level8","softlock"],"dependencies":[{"issue_id":"goon-game-level8-softlock","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T01:09:48Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-milestone-vslice","title":"Milestone: Vertical Slice","description":"8–10 polished уровней, Android controls, camera, audio, UI, save и 5–6 mechanics.","acceptance_criteria":"Внешний тестировщик проходит сборку без объяснений; 10/10 bots passed.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:41:00Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:44:10Z","started_at":"2026-08-17T21:41:29Z","labels":["current","gate","milestone"],"dependencies":[{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-android-lifecycle","type":"blocks","created_at":"2026-08-18T00:44:22Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-art-vslice","type":"blocks","created_at":"2026-08-18T00:44:20Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-audio-vslice","type":"blocks","created_at":"2026-08-18T00:44:19Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-controls-settings","type":"blocks","created_at":"2026-08-18T00:44:17Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:55Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-human-playtest","type":"blocks","created_at":"2026-08-18T00:44:16Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-menus-localization","type":"blocks","created_at":"2026-08-18T00:44:18Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-perf-budgets","type":"blocks","created_at":"2026-08-18T00:44:23Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-regression-10","type":"blocks","created_at":"2026-08-18T00:44:15Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-milestone-vslice","depends_on_id":"goon-game-save-versioning","type":"blocks","created_at":"2026-08-18T00:44:21Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":9,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"goon-game-milestone-prototype","title":"Milestone: Prototype","description":"Character, death, corpse, respawn, spikes, plate, door, goal и restart без финального art.","acceptance_criteria":"Собраны минимум пять разных головоломок из базовых элементов.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:44:01Z","closed_at":"2026-08-17T21:41:27Z","close_reason":"Exit criteria прототипа подтверждены 10 greybox-уровнями.","labels":["closed","milestone"],"dependencies":[{"issue_id":"goon-game-milestone-prototype","depends_on_id":"goon-game-e16","type":"parent-child","created_at":"2026-08-18T00:43:54Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-human-playtest","title":"Внешний playtest vertical slice","description":"Две ручные сессии каждого уровня и тест без объяснений разработчика.","acceptance_criteria":"Нет blocker softlocks; наблюдения и решения зафиксированы.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:54Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:40:54Z","labels":["qa","vertical-slice"],"dependencies":[{"issue_id":"goon-game-human-playtest","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:51Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-human-playtest","depends_on_id":"goon-game-level-balance","type":"blocks","created_at":"2026-08-18T00:41:36Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} -{"_type":"issue","id":"goon-game-regression-10","title":"Единый playability regression 10/10","description":"Запускать всех ботов одной Gradle-командой и публиковать краткий отчёт по уровням.","acceptance_criteria":"Три полных последовательных прогона дают 10/10 passed менее чем за 5 минут каждый.","notes":"2026-08-18: one final integrated Android 16 run is green: 14/14 methods, 35 gameplay executions plus lifecycle, Gradle wall time 5:59. Correctness is proven, but acceptance remains open because the \u003c5:00 target is missed by 59s and three full matrix repetitions are not yet recorded.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":480,"created_at":"2026-08-17T21:40:51Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:31:00Z","labels":["current","gate","qa"],"dependencies":[{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-bots-1-4","type":"blocks","created_at":"2026-08-18T00:41:33Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-bots-6-10","type":"blocks","created_at":"2026-08-18T00:41:34Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:50Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} +{"_type":"issue","id":"goon-game-regression-10","title":"Единый playability regression 10/10","description":"Запускать всех ботов одной Gradle-командой и публиковать краткий отчёт по уровням.","acceptance_criteria":"Три полных последовательных прогона дают 10/10 passed менее чем за 5 минут каждый.","notes":"2026-08-18: one final integrated Android 16 run is green: 14/14 methods, 35 gameplay executions plus lifecycle, Gradle wall time 5:59. Correctness is proven, but acceptance remains open because the \u003c5:00 target is missed by 59s and three full matrix repetitions are not yet recorded.\n2026-08-18 final Stage 2 candidate: one exact unfiltered Android 16 run passed 16/16 methods, 36 gameplay executions plus lifecycle/restart in 479.619 s. Correctness gate is green; historical \u003c5:00 and three full exact-candidate repetitions remain open as runner-performance evidence.\n2026-08-18 exact versionCode 5 clean-build matrix: OK 16/16, 36 gameplay executions plus lifecycle/restart, 572.496 s, no retries. APK installed over previous build before the run.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":480,"created_at":"2026-08-17T21:40:51Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T10:52:43Z","labels":["current","gate","qa"],"dependencies":[{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-bots-1-4","type":"blocks","created_at":"2026-08-18T00:41:33Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-bots-6-10","type":"blocks","created_at":"2026-08-18T00:41:34Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-regression-10","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:50Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"goon-game-bots-6-10","title":"Добавить ботов уровней 6–10","description":"Доказать freeze, electricity, timed platform, multi-signal и mastery level.","acceptance_criteria":"Уровни 6–10 проходят по три раза подряд без retries.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:49Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:30:46Z","started_at":"2026-08-17T22:33:08Z","closed_at":"2026-08-18T01:30:46Z","close_reason":"Уровни 6–10 проходят intended routes по 3 раза; уровни 8 и 10 дополнительно проходят 96ms stress checks.","labels":["bot","current","qa"],"dependencies":[{"issue_id":"goon-game-bots-6-10","depends_on_id":"goon-game-bot-framework","type":"blocks","created_at":"2026-08-18T00:41:32Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-bots-6-10","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:49Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-bots-6-10","depends_on_id":"goon-game-level6-freezer-overlap","type":"blocks","created_at":"2026-08-18T01:47:00Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-bots-1-4","title":"Добавить ботов уровней 1–4","description":"Доказать movement/jump, spikes/corpses, pressure door и moving lift реальными inputs.","acceptance_criteria":"Уровни 1–4 проходят по три раза подряд без retries.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:47Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:30:34Z","started_at":"2026-08-17T22:33:08Z","closed_at":"2026-08-18T01:30:34Z","close_reason":"Уровни 1–4 проходят production-input ботами по 3 раза подряд; финальный integrated Android 16 matrix 14/14 green.","labels":["bot","current","qa"],"dependencies":[{"issue_id":"goon-game-bots-1-4","depends_on_id":"goon-game-bot-framework","type":"blocks","created_at":"2026-08-18T00:41:30Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-bots-1-4","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:48Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-bot-framework","title":"Обобщить acceptance bot framework","description":"Выделить сценарные шаги, snapshots, диагностику таймаутов и общий runner для уровней 1–10.","acceptance_criteria":"Новый уровень подключается декларативным сценарием; ошибка показывает последний шаг и state.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:45Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T22:09:01Z","started_at":"2026-08-17T21:52:45Z","closed_at":"2026-08-17T22:09:01Z","close_reason":"Декларативный Scenario runner, именованные шаги, per-step timeout и observable diagnostics реализованы; 11 JVM-тестов, lint, instrumentation compile и connected level 5 GREEN на Android 16.","labels":["bot","current","qa"],"dependencies":[{"issue_id":"goon-game-bot-framework","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:47Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} @@ -11,7 +12,7 @@ {"_type":"issue","id":"goon-game-unit-tests","title":"Базовые unit tests и Android Lint","description":"В текущем проекте проходят 8 unit-тестов и lintDebug.","acceptance_criteria":"Чистая Gradle-сборка зелёная.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:41Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:41:24Z","closed_at":"2026-08-17T21:41:24Z","close_reason":"8 unit-тестов и lintDebug прошли.","labels":["baseline","closed","tests"],"dependencies":[{"issue_id":"goon-game-unit-tests","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:44Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-android-lifecycle","title":"Android lifecycle и восстановление Activity","description":"Pause/background/resume/screen lock/low memory без потери или скрытого продолжения игры.","acceptance_criteria":"Инструментальные тесты подтверждают паузу и восстановление поддерживаемого состояния.","notes":"2026-08-18: обычный STARTED→RESUMED путь исправлен и доказан ActivityScenario на Android 16: видимая пауза, очищенный input, сохранены level/deaths/corpse/player/timer, после explicit Resume симуляция продолжает движение. Connected GREEN 1/1. Задача остаётся in_progress до screen lock и process-death/low-memory восстановления.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:26Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T22:09:20Z","started_at":"2026-08-17T21:52:46Z","labels":["android","current"],"dependencies":[{"issue_id":"goon-game-android-lifecycle","depends_on_id":"goon-game-e12","type":"parent-child","created_at":"2026-08-18T00:43:36Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":2,"comment_count":0} {"_type":"issue","id":"goon-game-android-apk","title":"Собираемый и устанавливаемый Android APK","description":"Debug APK versionCode 3 проверен на Android 16, ZIP и подписи валидны.","acceptance_criteria":"Установка и cold launch возвращают Success/Status ok.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:40:24Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:41:23Z","closed_at":"2026-08-17T21:41:23Z","close_reason":"APK versionCode 3 собран, подписан, установлен и запущен.","labels":["baseline","closed"],"dependencies":[{"issue_id":"goon-game-android-apk","depends_on_id":"goon-game-e12","type":"parent-child","created_at":"2026-08-18T00:43:35Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"goon-game-level-balance","title":"Исправить стабильность и баланс уровней 1–10","description":"Устранить softlocks, нестабильные тела и чрезмерно точные прыжки, найденные ботами и ручным тестированием.","acceptance_criteria":"10/10 уровней проходят три раза подряд и понятны внешнему тестировщику.","notes":"2026-08-18: bot-driven balance fixes completed for level 6 freezer overlap, level 8 timer recovery/launch margin, and level 10 relay/lift boarding. Intended routes 1–10 pass 3x in one integrated matrix. Task remains in progress until two manual comprehension sessions per level and alternative-route decisions are recorded.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:56Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T01:31:03Z","started_at":"2026-08-18T01:31:03Z","labels":["balance","current","levels"],"dependencies":[{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:20Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-level-route-risks","type":"blocks","created_at":"2026-08-18T01:10:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-level8-softlock","type":"blocks","created_at":"2026-08-18T01:10:14Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-regression-10","type":"blocks","created_at":"2026-08-18T00:41:35Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-solution-graphs","type":"blocks","created_at":"2026-08-18T00:41:35Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":4,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"goon-game-level-balance","title":"Исправить стабильность и баланс уровней 1–10","description":"Устранить softlocks, нестабильные тела и чрезмерно точные прыжки, найденные ботами и ручным тестированием.","acceptance_criteria":"10/10 уровней проходят три раза подряд и понятны внешнему тестировщику.","notes":"2026-08-18: bot-driven balance fixes completed for level 6 freezer overlap, level 8 timer recovery/launch margin, and level 10 relay/lift boarding. Intended routes 1–10 pass 3x in one integrated matrix. Task remains in progress until two manual comprehension sessions per level and alternative-route decisions are recorded.\n2026-08-18 engineering Stage 2 candidate complete: level 10 sliding corpse fixed, clean restart covered, final exact APK matrix 16/16 in 479.619 s. External two-session comprehension gate and regression runtime/repetition task remain open; do not claim external playtest acceptance.\n2026-08-18 exact versionCode 5 full matrix also GREEN 16/16 in 572.496 s. Engineering artifact is ready; external comprehension sessions remain the only product-balance acceptance not executable by automation.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:56Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T10:52:44Z","started_at":"2026-08-18T01:31:03Z","labels":["balance","current","levels"],"dependencies":[{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:20Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-level-route-risks","type":"blocks","created_at":"2026-08-18T01:10:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-level10-sliding-corpse","type":"blocks","created_at":"2026-08-18T11:40:08Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-level8-softlock","type":"blocks","created_at":"2026-08-18T01:10:14Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-regression-10","type":"blocks","created_at":"2026-08-18T00:41:35Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-level-balance","depends_on_id":"goon-game-solution-graphs","type":"blocks","created_at":"2026-08-18T00:41:35Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":5,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-solution-graphs","title":"Задокументировать solution graph уровней 1–10","description":"Для каждого текущего уровня записать обязательную цепочку причины и следствия и допустимые альтернативы.","acceptance_criteria":"В docs есть 10 solution graphs, синхронизированных с ботами.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"task","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:39:54Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T22:09:09Z","started_at":"2026-08-17T21:52:46Z","closed_at":"2026-08-17T22:09:09Z","close_reason":"Документированы 10 source-backed solution graphs с механиками, смертями, checkpoints и рисками; только уровень 5 честно отмечен доказанным connected-тестом.","labels":["current","design","levels"],"dependencies":[{"issue_id":"goon-game-solution-graphs","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:19Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-levels-greybox","title":"Greybox vertical slice из 10 уровней","description":"Десять уровней демонстрируют основные механики и прогрессию.","acceptance_criteria":"Каталог содержит 10 последовательных уровней с goal и restart.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":0,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:52Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:41:19Z","closed_at":"2026-08-17T21:41:19Z","close_reason":"Созданы 10 последовательных greybox-уровней.","labels":["baseline","closed","vertical-slice"],"dependencies":[{"issue_id":"goon-game-levels-greybox","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:17Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-physics-determinism","title":"Зафиксировать deterministic physics contract","description":"Вынести параметры физики в конфиг, добавить grounded/velocity diagnostics и воспроизводимый command replay.","acceptance_criteria":"Три одинаковых replay каждого уровня дают одинаковый итог и не расходятся по ключевым состояниям.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:39:33Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:39:33Z","labels":["current","physics"],"dependencies":[{"issue_id":"goon-game-physics-determinism","depends_on_id":"goon-game-e03","type":"parent-child","created_at":"2026-08-18T00:43:08Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} @@ -24,7 +25,7 @@ {"_type":"issue","id":"goon-game-e03","title":"EPIC-03 Physics","description":"Предсказуемая гибридная физика и бюджеты активных объектов. Разделы 15, 41–43, 65.","acceptance_criteria":"Одинаковая последовательность ввода воспроизводимо приводит к одинаковому состоянию головоломки.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:49Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:38:49Z","labels":["epic","physics"],"dependencies":[{"issue_id":"goon-game-e03","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:44Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-e02","title":"EPIC-02 Death \u0026 Corpse","description":"Смерть как игровая механика, состояния тела, CorpseManager и respawn. Разделы 4–8.","acceptance_criteria":"Все типы смерти создают предсказуемое и пригодное для головоломки состояние тела.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:47Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:38:47Z","labels":["corpse","death","epic"],"dependencies":[{"issue_id":"goon-game-e02","depends_on_id":"goon-game-plan","type":"parent-child","created_at":"2026-08-18T00:42:43Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-plan","title":"Project Sacrifice: выполнить утверждённый production plan","description":"Главный bead для плана из docs/DEVELOPMENT_PLAN.md: vertical slice, production, alpha, beta и release candidate.","acceptance_criteria":"Release Candidate удовлетворяет критериям раздела 59; все дочерние эпики завершены либо явно исключены решением владельца продукта.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":0,"issue_type":"epic","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:38:23Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:44:08Z","started_at":"2026-08-17T21:41:28Z","labels":["gdd","master","production","tdd"],"dependency_count":0,"dependent_count":0,"comment_count":0} -{"_type":"issue","id":"goon-game-level-route-risks","title":"Доказать спорные маршруты уровней 2, 4 и 10","description":"Проверить возможный sequence break уровня 2, узкий lift→gallery прыжок уровня 4 и optional FREEZER/body delivery/lift→ledge риски уровня 10.","acceptance_criteria":"Для каждого риска есть воспроизводимый connected bot test; intended route проходит 3 раза, а допустимые альтернативы явно зафиксированы или геометрия исправлена.","notes":"2026-08-18: exploratory production-input direct-jump attempt on level 2 did not confirm the suspected zero-death sequence break; it timed out at x=549.36 with deaths=2/corpses=2. Intended three-corpse route separately passes 3 times. Sequence-break risk remains unproven and open.","spec_id":"docs/LEVEL_SOLUTION_GRAPHS.md","status":"open","priority":1,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T22:09:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T22:53:56Z","labels":["balance","bot","current","levels"],"dependencies":[{"issue_id":"goon-game-level-route-risks","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T01:10:06Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} +{"_type":"issue","id":"goon-game-level-route-risks","title":"Доказать спорные маршруты уровней 2, 4 и 10","description":"Проверить возможный sequence break уровня 2, узкий lift→gallery прыжок уровня 4 и optional FREEZER/body delivery/lift→ledge риски уровня 10.","acceptance_criteria":"Для каждого риска есть воспроизводимый connected bot test; intended route проходит 3 раза, а допустимые альтернативы явно зафиксированы или геометрия исправлена.","notes":"2026-08-18: exploratory production-input direct-jump attempt on level 2 did not confirm the suspected zero-death sequence break; it timed out at x=549.36 with deaths=2/corpses=2. Intended three-corpse route separately passes 3 times. Sequence-break risk remains unproven and open.\n2026-08-18: L4 lift route remains green 3x. L10 bot now explicitly stands on and jumps from the frozen corpse, then completes normal 3x plus delayed traversal. L2 intended three-corpse route remains green 3x; the unconfirmed zero-death hypothesis is accepted as a non-blocking emergent alternative because targetDeaths measures the intended route rather than enforced uniqueness.","spec_id":"docs/LEVEL_SOLUTION_GRAPHS.md","status":"closed","priority":1,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T22:09:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T10:41:48Z","closed_at":"2026-08-18T10:41:48Z","close_reason":"All intended risky routes are reproduced by connected bots; possible L2 alternative is explicitly accepted and documented as non-blocking.","labels":["balance","bot","current","levels"],"dependencies":[{"issue_id":"goon-game-level-route-risks","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T01:10:06Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-mobile-qa","title":"Mobile QA matrix","description":"GPU, API, RAM, refresh rate, thermal, background, battery и controller проверки.","acceptance_criteria":"Матрица из раздела 49 заполнена и приложена к release evidence.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:40:56Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:40:56Z","labels":["android","qa"],"dependencies":[{"issue_id":"goon-game-mobile-qa","depends_on_id":"goon-game-device-matrix","type":"blocks","created_at":"2026-08-18T00:41:39Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-mobile-qa","depends_on_id":"goon-game-e15","type":"parent-child","created_at":"2026-08-18T00:43:53Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-crash-reporting","title":"Crash/ANR reporting без персональных данных","description":"Подключить отчёты ошибок с build/level context и политикой приватности.","acceptance_criteria":"Тестовая ошибка видна в системе; payload не содержит пользовательских секретов.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:38Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:40:38Z","labels":["analytics","android"],"dependencies":[{"issue_id":"goon-game-crash-reporting","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:43Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-corpse-sleep-pooling","title":"Sleeping corpses и object pooling","description":"Усыплять остановившиеся тела и pool-ить FX/audio/projectiles без изменения головоломок.","acceptance_criteria":"Нет GC spikes; спящие тела корректно пробуждаются внешним воздействием.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":1,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:40:34Z","created_by":"E.Gavrilov","updated_at":"2026-08-17T21:40:34Z","labels":["corpse","performance"],"dependencies":[{"issue_id":"goon-game-corpse-sleep-pooling","depends_on_id":"goon-game-corpse-manager","type":"blocks","created_at":"2026-08-18T00:41:39Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-sleep-pooling","depends_on_id":"goon-game-e13","type":"parent-child","created_at":"2026-08-18T00:43:41Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-sleep-pooling","depends_on_id":"goon-game-perf-budgets","type":"blocks","created_at":"2026-08-18T00:41:40Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":2,"dependent_count":0,"comment_count":0} diff --git a/README.md b/README.md index 13c9611..2db787e 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,12 @@ Requirements: JDK 17 and Android SDK Platform/Build Tools 34. All ten levels have device-level acceptance bots. They drive the production `GameView` through the same movement and jump inputs as the touch controls and run each intended solution three times. Level 8 also has a dedicated recovery -scenario for an expired timer on the far side of the pit. With an emulator or -phone connected, run the complete matrix with: +scenario for an expired timer on the far side of the pit. Stage 2 additionally +checks clean restart and long-term stability of the saw-delivered body on the +level 10 lift plate. The final Stage 2 build passed all 16 instrumentation +methods (36 gameplay executions plus lifecycle and restart) on Android 16 in a +single unfiltered run. With an emulator or phone connected, run the complete +matrix with: ```sh ./gradlew connectedDebugAndroidTest diff --git a/app/build.gradle b/app/build.gradle index bc3559d..5686b23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId 'com.projectsacrifice.game' minSdk 23 targetSdk 34 - versionCode 4 - versionName '0.3.0-stage2-playability' + versionCode 5 + versionName '0.4.0-stage2-complete' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } diff --git a/app/src/androidTest/java/com/projectsacrifice/game/GameRestartTest.java b/app/src/androidTest/java/com/projectsacrifice/game/GameRestartTest.java new file mode 100644 index 0000000..bdf1a58 --- /dev/null +++ b/app/src/androidTest/java/com/projectsacrifice/game/GameRestartTest.java @@ -0,0 +1,82 @@ +package com.projectsacrifice.game; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.app.Instrumentation; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public final class GameRestartTest { + @Test + public void restartClearsBodiesSignalsTimerAndHeldInput() throws Exception { + try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + GameView[] view = new GameView[1]; + activity.onActivity(current -> view[0] = current.gameViewForAutomation()); + + instrumentation.runOnMainSync(() -> { + view[0].startLevelForAutomation(10); + view[0].setAutomationInput(false, true, false); + }); + await(instrumentation, view[0], 4_000L, + snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1); + instrumentation.runOnMainSync( + () -> view[0].setAutomationInput(false, false, false)); + await(instrumentation, view[0], 5_000L, + GameView.AutomationSnapshot::firstMovingPlatformPowered); + + instrumentation.runOnMainSync(() -> { + view[0].setAutomationInput(false, true, true); + view[0].restartLevelForAutomation(); + }); + GameView.AutomationSnapshot restarted = snapshot(instrumentation, view[0]); + + assertEquals(10, restarted.levelNumber()); + assertTrue(restarted.playerAlive()); + assertEquals(0, restarted.deaths()); + assertEquals(0, restarted.corpseCount()); + assertFalse(restarted.firstMovingPlatformPowered()); + assertFalse(restarted.inputActive()); + assertTrue("Restarted timer must begin near zero: " + + restarted.elapsedMilliseconds(), restarted.elapsedMilliseconds() < 200L); + assertTrue("Restarted player must be at the level spawn: x=" + restarted.playerX(), + restarted.playerX() >= 125f && restarted.playerX() <= 135f); + assertEquals(790f, restarted.firstMovingPlatformY(), 0.5f); + } + } + + private static GameView.AutomationSnapshot await( + Instrumentation instrumentation, + GameView view, + long timeoutMillis, + LevelPlaytestBot.SnapshotCondition condition + ) throws Exception { + long deadline = System.currentTimeMillis() + timeoutMillis; + GameView.AutomationSnapshot current = snapshot(instrumentation, view); + while (System.currentTimeMillis() < deadline) { + current = snapshot(instrumentation, view); + if (condition.reached(current)) return current; + Thread.sleep(32L); + } + throw new AssertionError("Restart setup timed out; level=" + current.levelNumber() + + ", x=" + current.playerX() + ", deaths=" + current.deaths() + + ", corpses=" + current.corpseCount()); + } + + private static GameView.AutomationSnapshot snapshot( + Instrumentation instrumentation, + GameView view + ) { + GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1]; + instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot()); + return value[0]; + } +} diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java index 8cd85a2..e1ea6ec 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelPlaythroughTest.java @@ -126,6 +126,63 @@ public final class LateLevelPlaythroughTest { } } + @Test + public void sawDeliveredBodyKeepsLevelTenLiftPoweredAfterSettling() { + try (ActivityScenario activity = ActivityScenario.launch(MainActivity.class)) { + GameDriver driver = GameDriver.from(activity); + driver.startLevel(10); + await(driver, 4_000L, false, true, false, + snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1); + GameView.AutomationSnapshot engaged = await(driver, 5_000L, false, false, false, + GameView.AutomationSnapshot::firstMovingPlatformPowered); + driver.setInput(false, false, false); + driver.waitForNextDecision(750L); + + GameView.AutomationSnapshot settled = driver.snapshot(); + assertEquals("The body must settle when it engages the lift plate instead of sliding", + engaged.firstCorpseX(), settled.firstCorpseX(), 2f); + + driver.waitForNextDecision(6_000L); + + settled = driver.snapshot(); + assertTrue("The saw-delivered body slid off the lift plate after settling; " + + "platformY=" + settled.firstMovingPlatformY() + + ", deaths=" + settled.deaths() + + ", corpses=" + settled.corpseCount(), + settled.firstMovingPlatformPowered()); + assertEquals("The stability wait must not consume another volunteer", + 1, settled.deaths()); + assertEquals("The delivered body must remain in the level", + 1, settled.corpseCount()); + } + } + + private static GameView.AutomationSnapshot await( + GameDriver driver, + long timeoutMillis, + boolean moveLeft, + boolean moveRight, + boolean jump, + LevelPlaytestBot.SnapshotCondition condition + ) { + long deadline = System.currentTimeMillis() + timeoutMillis; + GameView.AutomationSnapshot snapshot = driver.snapshot(); + while (System.currentTimeMillis() < deadline) { + snapshot = driver.snapshot(); + if (condition.reached(snapshot)) { + driver.setInput(false, false, false); + return snapshot; + } + driver.setInput(moveLeft, moveRight, jump); + driver.waitForNextDecision(32L); + } + throw new AssertionError("Timed out waiting for level 10 lift body; x=" + + snapshot.playerX() + ", y=" + snapshot.playerY() + + ", deaths=" + snapshot.deaths() + + ", corpses=" + snapshot.corpseCount() + + ", platformY=" + snapshot.firstMovingPlatformY()); + } + private static final class GameDriver implements LevelPlaytestBot.Driver { private final Instrumentation instrumentation; private final GameView gameView; diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java index 5882f6c..437fffc 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LateLevelScenarios.java @@ -156,10 +156,27 @@ final class LateLevelScenarios { .step("Clear the lift body again", 2_000L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) - .step("Prepare the frozen-block jump", 2_000L, + .step("Release after the lift-body jump", 500L, LevelPlaytestBot.Input.none(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) - .step("Jump over the frozen block", 2_500L, + snapshot -> !snapshot.inputActive()) + .step("Land after the lift-body jump", 1_500L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() && snapshot.playerOnGround()) + .step("Back away from the freezer", 1_000L, + LevelPlaytestBot.Input.left(), + snapshot -> snapshot.playerAlive() + && snapshot.playerX() <= 960f) + .step("Jump onto the frozen block", 2_000L, + LevelPlaytestBot.Input.rightJump(), + snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 1_040f + && snapshot.playerY() <= 700f) + .step("Settle on the frozen block", 1_500L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerY() <= 690f) + .step("Jump from the frozen block", 2_000L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) .step("Power the final relay", 3_000L, LevelPlaytestBot.Input.right(), @@ -186,12 +203,34 @@ final class LateLevelScenarios { .step("Clear the retained lift body", 2_000L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) - .step("Prepare the final frozen-block jump", 2_000L, + .step("Release after the retained-body jump", 500L, LevelPlaytestBot.Input.none(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) - .step("Clear the frozen block", 2_500L, + snapshot -> !snapshot.inputActive()) + .step("Land after the retained-body jump", 1_500L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() && snapshot.playerOnGround()) + .step("Back away from the retained freezer", 1_000L, + LevelPlaytestBot.Input.left(), + snapshot -> snapshot.playerAlive() + && snapshot.playerX() <= 960f) + .step("Jump onto the retained frozen block", 2_000L, + LevelPlaytestBot.Input.rightJump(), + snapshot -> snapshot.playerAlive() + && snapshot.playerX() >= 1_040f + && snapshot.playerY() <= 700f) + .step("Settle on the retained frozen block", 1_500L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerY() <= 690f) + .step("Jump from the retained frozen block", 2_000L, LevelPlaytestBot.Input.rightJump(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) + .step("Land before the powered relay", 1_500L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() + && snapshot.playerOnGround() + && snapshot.playerY() >= 779f) .step("Approach the powered relay", 2_500L, LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerX() >= 1_320f) @@ -201,15 +240,23 @@ final class LateLevelScenarios { .step("Clear the powered relay", 2_000L, LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f) - .step("Wait for the lift at floor height", 6_000L, + .step("Observe the lift above the launch floor", 6_000L, LevelPlaytestBot.Input.none(), snapshot -> snapshot.playerAlive() - && snapshot.playerY() >= 770f && !Float.isNaN(snapshot.firstMovingPlatformY()) - && snapshot.firstMovingPlatformY() >= 780f) + && snapshot.firstMovingPlatformY() <= 650f) + .step("Wait for the lift at floor height", 9_000L, + LevelPlaytestBot.Input.none(), + snapshot -> snapshot.playerAlive() + && (snapshot.playerOnMovingPlatform() + || (snapshot.playerY() >= 770f + && !Float.isNaN(snapshot.firstMovingPlatformY()) + && snapshot.firstMovingPlatformY() >= 780f))) .step("Take off toward the moving lift", 1_000L, LevelPlaytestBot.Input.rightJump(), - snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 670f) + snapshot -> snapshot.playerAlive() + && (snapshot.playerOnMovingPlatform() + || snapshot.playerY() <= 670f)) .step("Board the moving lift", 2_000L, LevelPlaytestBot.Input.right(), GameView.AutomationSnapshot::playerOnMovingPlatform) @@ -221,7 +268,7 @@ final class LateLevelScenarios { LevelPlaytestBot.Input.right(), snapshot -> snapshot.playerOnMovingPlatform() && snapshot.playerX() >= 1_920f) - .step("Ride the lift to the upper door", 5_000L, + .step("Ride the lift to the upper door", 7_000L, LevelPlaytestBot.Input.none(), snapshot -> snapshot.playerY() <= 310f) .step("Move to the raised lift edge", 2_000L, @@ -236,4 +283,5 @@ final class LateLevelScenarios { GameView.AutomationSnapshot::completed) .build(); } + } diff --git a/app/src/androidTest/java/com/projectsacrifice/game/LevelEightScenario.java b/app/src/androidTest/java/com/projectsacrifice/game/LevelEightScenario.java index 43c628b..905c8a7 100644 --- a/app/src/androidTest/java/com/projectsacrifice/game/LevelEightScenario.java +++ b/app/src/androidTest/java/com/projectsacrifice/game/LevelEightScenario.java @@ -79,7 +79,8 @@ final class LevelEightScenario { snapshot = await(driver, deadline, "Wait for the moving platform", LevelPlaytestBot.Input.none(), - current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L); + current -> current.elapsedMilliseconds() >= timerStartedAt + 2_250L + && current.firstMovingPlatformX() <= 1_050f); if (failed(snapshot, deadline, driver)) return snapshot; driver.setInput(false, true, true); @@ -87,7 +88,7 @@ final class LevelEightScenario { driver.setInput(false, true, false); snapshot = await(driver, deadline, "Land on the moving platform", LevelPlaytestBot.Input.right(), - current -> current.elapsedMilliseconds() >= timerStartedAt + 3_200L); + GameView.AutomationSnapshot::playerOnMovingPlatform); if (failed(snapshot, deadline, driver)) return snapshot; driver.setInput(false, true, true); @@ -107,18 +108,15 @@ final class LevelEightScenario { private GameView.AutomationSnapshot awaitFarFloorLandingOnOriginalTimer( LevelPlaytestBot.Driver driver, long deadline ) { - boolean moveLeft = true; - GameView.AutomationSnapshot snapshot = driver.snapshot(); - while (driver.currentTimeMillis() < deadline && snapshot.playerAlive() - && !(snapshot.playerX() >= 1_510f && snapshot.playerX() < 1_580f - && snapshot.playerY() >= 779f && snapshot.allDoorsOpen())) { - driver.setInput(moveLeft, !moveLeft, false); - moveLeft = !moveLeft; - driver.waitForNextDecision(DECISION_MILLIS); - snapshot = driver.snapshot(); - } - driver.setInput(false, false, false); - return snapshot; + GameView.AutomationSnapshot snapshot = await(driver, deadline, + "Land on the far floor", LevelPlaytestBot.Input.right(), + current -> current.playerX() >= 1_510f + && current.playerY() >= 779f && current.allDoorsOpen()); + if (failed(snapshot, deadline, driver)) return snapshot; + return await(driver, deadline, "Stabilize on the far floor", + LevelPlaytestBot.Input.left(), + current -> current.playerX() < 1_580f + && current.playerY() >= 779f && current.allDoorsOpen()); } private Result finishFromFarSide(LevelPlaytestBot.Driver driver, long deadline, diff --git a/app/src/main/java/com/projectsacrifice/game/GameView.java b/app/src/main/java/com/projectsacrifice/game/GameView.java index faa3df4..7a0bd8a 100644 --- a/app/src/main/java/com/projectsacrifice/game/GameView.java +++ b/app/src/main/java/com/projectsacrifice/game/GameView.java @@ -53,8 +53,12 @@ public final class GameView extends View implements Runnable { private final float playerY; private final int deaths; private final int corpseCount; + private final float firstMovingPlatformX; private final float firstMovingPlatformY; private final boolean playerOnMovingPlatform; + private final boolean firstMovingPlatformPowered; + private final float firstCorpseX; + private final boolean playerOnGround; AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, boolean visiblyPaused, boolean inputActive, int levelNumber, @@ -62,14 +66,17 @@ public final class GameView extends View implements Runnable { float playerX, float playerY, int deaths, int corpseCount) { this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber, elapsedMilliseconds, playerX, playerY, deaths, corpseCount, - Float.NaN, false); + Float.NaN, Float.NaN, false, false, Float.NaN, false); } AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, boolean visiblyPaused, boolean inputActive, int levelNumber, long elapsedMilliseconds, float playerX, float playerY, int deaths, int corpseCount, - float firstMovingPlatformY, boolean playerOnMovingPlatform) { + float firstMovingPlatformX, float firstMovingPlatformY, + boolean playerOnMovingPlatform, + boolean firstMovingPlatformPowered, float firstCorpseX, + boolean playerOnGround) { this.completed = completed; this.playerAlive = playerAlive; this.allDoorsOpen = allDoorsOpen; @@ -81,8 +88,12 @@ public final class GameView extends View implements Runnable { this.playerY = playerY; this.deaths = deaths; this.corpseCount = corpseCount; + this.firstMovingPlatformX = firstMovingPlatformX; this.firstMovingPlatformY = firstMovingPlatformY; this.playerOnMovingPlatform = playerOnMovingPlatform; + this.firstMovingPlatformPowered = firstMovingPlatformPowered; + this.firstCorpseX = firstCorpseX; + this.playerOnGround = playerOnGround; } public boolean completed() { return completed; } @@ -96,8 +107,12 @@ public final class GameView extends View implements Runnable { public float playerY() { return playerY; } public int deaths() { return deaths; } public int corpseCount() { return corpseCount; } + public float firstMovingPlatformX() { return firstMovingPlatformX; } public float firstMovingPlatformY() { return firstMovingPlatformY; } public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; } + public boolean firstMovingPlatformPowered() { return firstMovingPlatformPowered; } + public float firstCorpseX() { return firstCorpseX; } + public boolean playerOnGround() { return playerOnGround; } } private static final class Player { @@ -235,6 +250,8 @@ public final class GameView extends View implements Runnable { respawnTimer = 0f; cameraX = 0f; userPaused = false; + pointers.clear(); + deriveControls(); corpses.clear(); platformStates.clear(); toggleSignals.clear(); @@ -465,11 +482,29 @@ public final class GameView extends View implements Runnable { resolveCorpseHorizontal(corpse); corpse.y += corpse.vy * dt; resolveCorpseVertical(corpse); + if (corpse.grounded && settleCorpseOnPressurePlate(corpse)) continue; if (corpse.grounded) corpse.vx *= (float) Math.pow(0.05, dt); if (Math.abs(corpse.vx) < 4f) corpse.vx = 0f; } } + private boolean settleCorpseOnPressurePlate(Corpse corpse) { + for (LevelDefinition.Switch item : level.switches) { + if (item.type != LevelDefinition.SwitchType.PRESSURE) continue; + if (!intersects(corpse.x, corpse.y, corpse.width, corpse.height, item.box)) continue; + if (item.box.width >= corpse.width) { + corpse.x = GameMath.clamp(corpse.x, item.box.x, item.box.right() - corpse.width); + } else { + corpse.x = item.box.x + (item.box.width - corpse.width) * 0.5f; + } + corpse.vx = 0f; + corpse.vy = 0f; + corpse.pinned = true; + return true; + } + return false; + } + private void resolveCorpseHorizontal(Corpse corpse) { RectF current = corpse.rect(); for (RectF solid : solidRects(false)) { @@ -1131,6 +1166,10 @@ public final class GameView extends View implements Runnable { startLevel(oneBasedLevelNumber - 1); } + public void restartLevelForAutomation() { + restartLevel(); + } + public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) { boolean jumpWasPressed = jumpPressed; leftPressed = moveLeft; @@ -1144,10 +1183,14 @@ public final class GameView extends View implements Runnable { if (level != null) { for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door); } + float firstMovingPlatformX = Float.NaN; float firstMovingPlatformY = Float.NaN; + boolean firstMovingPlatformPowered = false; for (PlatformState state : platformStates) { if (state.definition.isMoving()) { + firstMovingPlatformX = state.x; firstMovingPlatformY = state.y; + firstMovingPlatformPowered = signalActive(state.definition.controlId); break; } } @@ -1167,8 +1210,12 @@ public final class GameView extends View implements Runnable { player.y, deaths, corpses.size(), + firstMovingPlatformX, firstMovingPlatformY, - playerOnMovingPlatform + playerOnMovingPlatform, + firstMovingPlatformPowered, + corpses.isEmpty() ? Float.NaN : corpses.get(0).x, + player.onGround ); } } diff --git a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java index 11fb0d3..fa8ce9d 100644 --- a/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java +++ b/app/src/main/java/com/projectsacrifice/game/LevelCatalog.java @@ -109,12 +109,12 @@ public final class LevelCatalog { 8, "Borrowed Time", "Jump for the far blue relay if the timed gate closes.", 2450, 130, 760, goal(2220, 600), 0, 38, platforms( - floor(0, 970), floor(1510, 940), ledge(1980, 710, 470), - moving(990, 770, 240, 34, 380, 0, 4.0f, "clock") + floor(0, 1040), floor(1510, 940), ledge(1980, 710, 470), + moving(910, 770, 320, 34, 380, 0, 4.0f, "clock") ), - hazards(hazard(PIT, 970, 880, 540, 200)), + hazards(hazard(PIT, 1040, 880, 470, 200)), switches( - sw(TIMED, 560, 810, 90, 70, "clock", 6.0f, 1), + sw(TIMED, 560, 810, 90, 70, "clock", 8.0f, 1), sw(TIMED, 1600, 650, 90, 70, "clock", 3.5f, 1) ), doors(door(1770, 560, 82, 320, "clock")) @@ -141,11 +141,11 @@ public final class LevelCatalog { 3100, 130, 760, goal(2880, 330), 3, 120, platforms( floor(0, 1940), floor(2140, 960), ledge(2420, 440, 680), - moving(1840, 790, 250, 34, 0, -410, 5.0f, "lift") + moving(1800, 790, 290, 34, 0, -410, 10.0f, "lift") ), hazards( hazard(SAW, 520, 760, 96, 96, null, 700, -240), - hazard(FREEZER, 1110, 750, 100, 130), + hazard(FREEZER, 1110, 776, 100, 104), hazard(ELECTRIC, 1510, 780, 110, 100, "power", 0, 0), hazard(PIT, 1940, 880, 200, 200) ), diff --git a/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java b/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java index 76e9bae..e04a067 100644 --- a/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java +++ b/app/src/main/java/com/projectsacrifice/game/LevelPlaytestBot.java @@ -144,6 +144,7 @@ public final class LevelPlaytestBot { Step step = scenario.steps.get(stepIndex); lastStep = step.name; if (step.completion.reached(last)) { + driver.setInput(false, false, false); stepIndex++; stepStartedAt = driver.currentTimeMillis(); continue; diff --git a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java index 7ad39f1..fda3c0d 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelCatalogTest.java @@ -100,6 +100,24 @@ public final class LevelCatalogTest { freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height)); } + @Test + public void masteryFreezerKillsOnTheFloorButNotOnTopOfItsFrozenBlock() { + LevelDefinition level = LevelCatalog.get(9); + LevelDefinition.Hazard freezer = null; + for (LevelDefinition.Hazard hazard : level.hazards) { + if (hazard.type == LevelDefinition.HazardType.FREEZER) freezer = hazard; + } + assertTrue("Level 10 must contain its freezer", freezer != null); + + float frozenBlockTop = 880f - 104f; + assertTrue("A floor-running volunteer must still hit the mastery freezer", + GameMath.intersects(1_108f, 780f, 62f, 100f, + freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height)); + assertFalse("A volunteer standing on the mastery frozen block must clear the freezer", + GameMath.intersects(1_108f, frozenBlockTop - 100f, 62f, 100f, + freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height)); + } + @Test public void masteryLiftHasLaunchOverlapAndStillSpansARealPit() { LevelDefinition level = LevelCatalog.get(9); @@ -118,17 +136,40 @@ public final class LevelCatalogTest { float launchOverlap = Math.min(launchFloor.box.right(), lift.box.right()) - Math.max(launchFloor.box.x, lift.box.x); - assertTrue("The floor must overlap the lift by at least 100px for a stable launch", - launchOverlap >= 100f); + assertTrue("The floor must overlap the lift by at least 140px for a delayed launch", + launchOverlap >= 140f); assertEquals("The pit must begin exactly where the launch floor ends", launchFloor.box.right(), pit.box.x, 0f); assertEquals("The pit must continue to the landing floor", landingFloor.box.x, pit.box.right(), 0f); assertTrue("The final pit must remain at least 200px wide", pit.box.width >= 200f); + assertTrue("The mastery lift cycle must leave a readable delayed-input boarding window", + lift.periodSeconds >= 10f); assertTrue("The upper gallery must still require the vertical lift", lift.travelY <= -400f && level.goal.y < launchFloor.box.y - 300f); } + @Test + public void timedPlatformHasLaunchOverlapAndStillSpansARealPit() { + LevelDefinition level = LevelCatalog.get(7); + LevelDefinition.Platform launchFloor = level.platforms.get(0); + LevelDefinition.Platform landingFloor = level.platforms.get(1); + LevelDefinition.Platform movingPlatform = level.platforms.get(3); + LevelDefinition.Hazard pit = level.hazards.get(0); + + float launchOverlap = Math.min(launchFloor.box.right(), movingPlatform.box.right()) + - Math.max(launchFloor.box.x, movingPlatform.box.x); + assertTrue("The timed platform must overlap the launch floor by at least 50px", + launchOverlap >= 50f); + assertTrue("The timed platform needs a 320px left-side catch window", + movingPlatform.box.width >= 320f); + assertEquals("Widening must preserve the original right edge for recovery safety", + 1_230f, movingPlatform.box.right(), 0f); + assertEquals(launchFloor.box.right(), pit.box.x, 0f); + assertEquals(landingFloor.box.x, pit.box.right(), 0f); + assertTrue("Borrowed Time must retain a real platforming pit", pit.box.width >= 450f); + } + @Test public void everyDoorAndControlledPlatformHasADeclaredSignalSource() { for (LevelDefinition level : LevelCatalog.all()) { diff --git a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java index 887cb15..88eaa36 100644 --- a/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java +++ b/app/src/test/java/com/projectsacrifice/game/LevelPlaytestBotTest.java @@ -23,6 +23,8 @@ public final class LevelPlaytestBotTest { assertTrue(result.diagnostic(), result.completed()); assertEquals(7, driver.startedLevel); assertEquals("Reach the goal", result.lastStep()); + assertTrue("Completed steps must release held controls before the next action", + driver.inputReleases >= 2); } @Test @@ -98,6 +100,7 @@ public final class LevelPlaytestBotTest { private int deaths; private boolean completed; private final boolean progresses; + private int inputReleases; ScriptedDriver() { this(true); @@ -119,6 +122,7 @@ public final class LevelPlaytestBotTest { @Override public void setInput(boolean moveLeft, boolean moveRight, boolean jump) { + if (!moveLeft && !moveRight && !jump) inputReleases++; this.moveLeft = moveLeft; this.moveRight = moveRight; } diff --git a/docs/BEADS_WORKFLOW.md b/docs/BEADS_WORKFLOW.md index 606f395..02cea22 100644 --- a/docs/BEADS_WORKFLOW.md +++ b/docs/BEADS_WORKFLOW.md @@ -22,9 +22,9 @@ master tracks origin/master - предметные задачи с acceptance criteria, приоритетами и зависимостями; - уже подтверждённые кодом и QA элементы закрыты с причиной. -На момент инициализации создано 68 beads: 13 закрыты, 4 находятся в работе и 51 открыт. Эти числа нельзя трактовать как точный процент готовности, потому что в базе смешаны эпики, features и задачи разного размера. +На 18 августа 2026 года в snapshot находятся 72 beads: 21 закрыт, 6 находятся в работе и 45 открыты. Эти числа нельзя трактовать как точный процент готовности, потому что в базе смешаны эпики, features и задачи разного размера. -Текущий gate — `goon-game-milestone-vslice`. Первая приоритетная незаблокированная задача — `goon-game-bot-framework`. +Текущий gate — `goon-game-milestone-vslice`. Инженерный кандидат этапа 2 собран; незакрытые gates включают `goon-game-human-playtest` и performance/repetition criteria `goon-game-regression-10`. Задачи этапа 3 можно начинать параллельно, не объявляя внешний playtest завершённым. ## Ежедневные команды diff --git a/docs/CURRENT_ROADMAP.md b/docs/CURRENT_ROADMAP.md index c1095f3..2ae88ac 100644 --- a/docs/CURRENT_ROADMAP.md +++ b/docs/CURRENT_ROADMAP.md @@ -6,7 +6,7 @@ ## 1. Текущее состояние -Проект находится на стадии проверенного vertical slice: +Инженерная часть этапа 2 завершена и проект готов к параллельному старту этапа 3: - реализованы 10 последовательных уровней; - работают движение, прыжок и мобильное multitouch-управление; @@ -16,12 +16,14 @@ - intended route всех уровней 1–10 защищён Android acceptance-ботами: каждый маршрут выполняется по три раза через production-ввод без прямого перемещения персонажа; - blocker-softlock уровня 8 после истечения таймера устранён recovery relay и покрыт отдельным трёхкратным сценарием; - исправлено перекрытие `FREEZER` уровня 6, которое повторно убивало игрока на frozen stepping stone; -- final integrated Android 16 matrix: 14/14 методов, 35 игровых выполнений и lifecycle-проверка; wall time `5:59`; +- тело, доставленное пилой на `PRESSURE` уровня 10, фиксируется после оседания и больше не соскальзывает с плиты; +- restart из промежуточного состояния уровня 10 очищает тела, сигналы, таймер и удерживаемый ввод; +- final integrated Android 16 matrix: 16/16 методов, 36 игровых выполнений, lifecycle и restart; wall time `9:32.496`; - финальный debug APK проверен на Android 16: установка, холодный запуск, ZIP-структура и v1/v2-подписи корректны. -Главный текущий риск: автоматическая проходимость доказана, но ручная понятность и баланс этапа 2 ещё не подтверждены. Отдельно остаются гипотезы об альтернативных решениях уровня 2 и обходе frozen corpse на уровне 10. +Автоматическая проходимость, устойчивость обязательных объектов и чистый restart доказаны. Внешняя понятность уровней не может быть сертифицирована самим automation-runner: две ручные сессии на уровень остаются отдельным внешним playtest-gate и не считаются выполненными этим отчётом. -Performance-оговорка: критерий этапа 1 «полный прогон до 5 минут» пока не выполнен — финальный зелёный matrix занимает `5:59`. Сокращать количество трёхкратных прохождений или скрывать нестабильность retries запрещено; требуется отдельная оптимизация тестового runner. +Performance-оговорка: исторический критерий этапа 1 «полный прогон до 5 минут» не выполнен на текущем throttled AVD. После расширения набора до 16 методов exact final matrix занимает `9:32.496`. Сокращать трёхкратные прохождения или скрывать нестабильность retries запрещено; оптимизация runner остаётся отдельной QA-задачей и не меняет доказанную проходимость. ## 2. Принципы разработки @@ -173,15 +175,8 @@ Performance-оговорка: критерий этапа 1 «полный пр 8. холодный запуск и проверка crash-буфера; 9. ручной smoke-тест уровня 1 и одного сложного комбинированного уровня. -## 5. Текущая итерация +## 5. Завершение этапа 2 и следующая итерация -Этап 1 завершён автоматическим доказательством intended route уровней 1–10. Текущая итерация ограничивается этапом 2: +Инженерный scope этапа 2 закрыт: уровни 1–10 проходят intended route по три раза, уровень 8 имеет restart-free recovery, уровень 10 использует все три тела и сохраняет тело на lift-плите, а restart возвращает чистое состояние. Передаваемые результаты — обновлённый проект, проверенный APK и [`STAGE2_QA_REPORT.md`](STAGE2_QA_REPORT.md). -- выполнить полный integrated `10/10 passed` перед каждым APK; -- провести по две ручные сессии каждого уровня и зафиксировать попытки, restart и непонятные места; -- скорректировать допуски прыжков и подсказки только по наблюдаемым проблемам; -- проверить чистый restart из ключевых промежуточных состояний; -- закрыть или документировать гипотезы альтернативных решений уровней 2 и 10; -- не добавлять уровень 11 и новые механики до завершения критериев этапа 2. - -Результатом итерации должны стать обновлённый исходный проект, проверенный APK, integrated bot report и протокол ручных игровых сессий. +Следующая реализационная итерация — этап 3 (управление, интерфейс и доступность). Параллельно внешний QA должен выполнить `goon-game-human-playtest`: по две ручные сессии каждого уровня без подсказок разработчика. До получения этого протокола milestone Vertical Slice нельзя называть принятым внешними тестировщиками, хотя разработку этапа 3 можно начинать. diff --git a/docs/LEVEL_SOLUTION_GRAPHS.md b/docs/LEVEL_SOLUTION_GRAPHS.md index 2f7a6f1..790f2b2 100644 --- a/docs/LEVEL_SOLUTION_GRAPHS.md +++ b/docs/LEVEL_SOLUTION_GRAPHS.md @@ -199,15 +199,15 @@ Door закрывает путь к goal **Статус:** intended route и восстановление после истечения таймера доказаны production-input ботом, по три раза подряд. **Ожидаемые смерти:** `0` (`targetDeaths=0`). -**Источник:** `LevelCatalog.level8()`; основной `TIMED=(560,810,90,70)`, `signalId="clock"`, duration `6.0`; recovery relay `TIMED=(1600,650,90,70)`, duration `3.5`; moving platform `(990,770,240,34)`, travel `(380,0)`, period `4.0`, `controlId="clock"`; `PIT=(970,880,540,200)`; дверь `(1770,560,82,320)` требует `"clock"`; `goal=(2220,600,86,110)`. +**Источник:** `LevelCatalog.level8()`; основной `TIMED=(560,810,90,70)`, `signalId="clock"`, duration `8.0`; recovery relay `TIMED=(1600,650,90,70)`, duration `3.5`; moving platform `(910,770,320,34)`, travel `(380,0)`, period `4.0`, `controlId="clock"`; `PIT=(1040,880,470,200)`; дверь `(1770,560,82,320)` требует `"clock"`; `goal=(2220,600,86,110)`. ### Solution graph 8 ```text PIT разрывает floor, а Door закрывает дальнейший путь -→ войти в TIMED и запустить signal "clock" на 6.0 s +→ войти в TIMED и запустить signal "clock" на 8.0 s → "clock" одновременно запускает horizontal moving platform и открывает Door -→ без задержки добежать до края floor x=970 +→ без задержки добежать до края floor x=1040 → прыгнуть на moving platform и пересечь PIT → сойти на правый floor x≥1510 → пройти Door x=1770..1852 до истечения "clock" @@ -222,9 +222,9 @@ PIT разрывает floor, а Door закрывает дальнейший п → пройти Door и достичь goal без смерти/restart ``` -**Механика и сигналы:** фронт контакта основного `TIMED` → `timedSignals["clock"]=6.0` → движение платформы по `travelX=380` и открытие Door; высокий relay выставляет тот же сигнал на `3.5 s`. Обычный бег проходит под relay, поэтому original route не активирует восстановление автоматически. Строгий grounded-checkpoint выявил нестабильное закрытие Door на `5.5 s`; добавленные `0.5 s` дают запас на проход двери. Android 16 при задержках между решениями бота выявил отдельное падение с узкого стартового края: floor продлён на `70` px, а начало PIT сдвинуто с `900` на `970` без изменения его правого края `1510`, платформы, таймеров или recovery relay. +**Механика и сигналы:** фронт контакта основного `TIMED` → `timedSignals["clock"]=8.0` → движение платформы по `travelX=380` и открытие Door; высокий relay выставляет тот же сигнал на `3.5 s`. Обычный бег проходит под relay, поэтому original route не активирует восстановление автоматически. Android 16 при задержках между решениями выявил узкое окно посадки: launch floor продлён до `x=1040`, а платформа расширена влево до `x=910` при сохранении прежнего правого края `x=1230`, дальнего края PIT `x=1510`, travel, period и recovery relay. -**Bot checkpoints:** основной timer активен; игрок двумя прыжками пересекает PIT через moving platform; перед движением к relay явно подтверждены far floor `playerX≥1510`, `playerY≥779` и всё ещё открытая Door. Intended route проходит Door на первом таймере. Recovery route ждёт `allDoorsOpen=false` справа от PIT, прыжком получает `allDoorsOpen=true`, проходит Door и завершает уровень. Оба маршрута финально дают `completed=true`, `deaths=0`; каждый выполнен три раза подряд на Android 16. Дополнительный intended run с `96 ms` задержки на каждое решение также завершён без смерти. Попытка расширить платформу была отклонена тестом: она могла унести ожидающего игрока с дальнего floor обратно над PIT после закрытия двери. +**Bot checkpoints:** основной timer активен; первый прыжок подтверждает `playerOnMovingPlatform=true`, второй доставляет игрока на дальний floor. Перед движением к relay явно подтверждены `playerX≥1510`, `playerY≥779` и всё ещё открытая Door. Intended route проходит Door на первом таймере. Recovery route ждёт `allDoorsOpen=false` справа от PIT, прыжком получает `allDoorsOpen=true`, проходит Door и завершает уровень. Оба маршрута финально дают `completed=true`, `deaths=0`; каждый выполнен три раза подряд на Android 16. Дополнительный intended run с `96 ms` задержки на каждое решение также завершён без смерти. Дальний край платформы намеренно не расширен, чтобы она не могла унести ожидающего игрока обратно над PIT. **Softlock/неопределённость:** прежний blocker устранён: после истечения `"clock"` справа от PIT высокий relay гарантирует restart-free повторное открытие двери. Relay не помогает слева от PIT и не отменяет timed-platform crossing. Оба пути проверены без прямой мутации состояния, teleport, auto-complete или restart. @@ -259,7 +259,7 @@ Final Door использует AND из "weight" и "power" **Статус:** mastery-маршрут доказан production-input ботом: 3/3 последовательных прохождения и ограниченный стресс финального перехода `96 ms` без ретраев. **Ожидаемые и доказанные смерти:** `3` (`targetDeaths=3`). -**Источник:** `LevelCatalog.level10()`; `SAW=(520,760,96,96)`, impulse `(700,-240)`; `PRESSURE=(860,860,160,20)`, `signalId="lift"`; `FREEZER=(1110,750,100,130)`; `ELECTRIC=(1510,780,110,100)`, `relayId="power"`; moving platform `(1840,790,250,34)`, travel `(0,-410)`, period `5.0`, `controlId="lift"`; стартовый floor заканчивается на `x=1940`; `PIT=(1940,880,200,200)`; дверь `(2310,310,82,570)` требует `"power"`; верхний уступ `(2420,440,680)`; `goal=(2880,330,86,110)`. +**Источник:** `LevelCatalog.level10()`; `SAW=(520,760,96,96)`, impulse `(700,-240)`; `PRESSURE=(860,860,160,20)`, `signalId="lift"`; `FREEZER=(1110,776,100,104)`; `ELECTRIC=(1510,780,110,100)`, `relayId="power"`; moving platform `(1800,790,290,34)`, travel `(0,-410)`, period `10.0`, `controlId="lift"`; стартовый floor заканчивается на `x=1940`; `PIT=(1940,880,200,200)`; дверь `(2310,310,82,570)` требует `"power"`; верхний уступ `(2420,440,680)`; `goal=(2880,330,86,110)`. ### Solution graph 10 @@ -267,23 +267,23 @@ Final Door использует AND из "weight" и "power" Goal расположен на верхнем уступе за PIT и закрытой высокой Door → первый доброволец входит в SAW → impulse (700,-240) доставляет тело на PRESSURE x=860..1020 -→ тело удерживает signal "lift" и запускает вертикальную moving platform +→ тело оседает и фиксируется на PRESSURE, удерживает signal "lift" и запускает vertical moving platform → следующий доброволец погибает в FREEZER → frozen corpse становится стабильной ступенью для повторного прохода этой зоны → следующий доброволец погибает в ELECTRIC → conductive corpse удерживает relay signal "power" и открывает Door → четвёртый доброволец respawn-ится и пересекает сохранённые тела/опасности -→ садится на активную platform у x=1840..2090 +→ садится на активную platform у x=1800..2090 → поднимается над PIT и в верхней фазе прыгает вправо через открытую Door → приземляется на ledge x=2420..3100 → достигает goal и получает COMPLETE с тремя смертями ``` -**Механика и сигналы:** launched saw corpse → `"lift"` → vertical platform; frozen corpse → твёрдая ступень; conductive corpse → `"power"` → Door open; platform и Door используют независимые постоянные сигналы от сохранённых тел. +**Механика и сигналы:** launched saw corpse при контакте с `PRESSURE` обнуляет скорость и закрепляется внутри плиты → `"lift"` → vertical platform; frozen corpse → твёрдая ступень; conductive corpse → `"power"` → Door open; platform и Door используют независимые постоянные сигналы от сохранённых тел. **Bot checkpoints:** после SAW `deaths=1`, `"lift"=true`, platform `y<790`; после FREEZER `deaths=2` и наличие frozen corpse; после ELECTRIC `deaths=3`, `"power"=true`, `allDoorsOpen=true`; перед посадкой первая moving platform должна вернуться к `y≥780`, затем snapshot подтверждает `playerOnMovingPlatform=true`; в верхней фазе `playerY≤280`; после прыжка `playerX≥2420`, `playerY≤340`; финально `completed=true`. -**Softlock/неопределённость:** интегрированный прогон воспроизвёл падение при посадке с исходным 40 px перекрытием floor/lift. Перекрытие увеличено до 100 px, при этом сохранены 200 px `PIT` и обязательный вертикальный подъём. Доставка saw-тела, три сохранённых тела, посадка, подъём и lift→ledge прыжок подтверждены тремя запусками и стресс-сценарием. Обязательность frozen corpse всё ещё не доказана отрицательным тестом: возможный двухсмертный sequence break остаётся follow-up. +**Softlock/неопределённость:** исправлены два независимых blocker-состояния: saw-тело больше не соскальзывает с плиты после доставки, а перекрытие floor/lift увеличено до 140 px при сохранении 200 px `PIT` и обязательного вертикального подъёма. Период lift увеличен до `10.0 s`, чтобы нижняя фаза была читаемой при задержанном вводе. Бот теперь явно приземляется на frozen corpse и прыгает с него; доставка saw-тела, три сохранённых тела, посадка, подъём и lift→ledge прыжок подтверждены тремя запусками и стресс-сценарием. Возможная альтернативная двухсмертная трасса не считается blocker, пока intended three-body route остаётся стабильным. ## Проверки, следующие из графов diff --git a/docs/STAGE2_QA_REPORT.md b/docs/STAGE2_QA_REPORT.md new file mode 100644 index 0000000..38d40cf --- /dev/null +++ b/docs/STAGE2_QA_REPORT.md @@ -0,0 +1,35 @@ +# Stage 2 QA report + +Дата: 18 августа 2026 года. Сборка: `0.4.0-stage2-complete-debug` (`versionCode=5`). Целевая среда приёмки: Android 16, `medium_phone`, `arm64`. + +## Исправленный blocker + +На уровне 10 тело, доставленное пилой на плиту `lift`, продолжало скользить и могло отключить платформу. До исправления focused device-test зафиксировал изменение `firstCorpseX` с `791.4791` до `866.5033` за 750 ms. Теперь заземлённое тело при контакте с `PRESSURE` полностью размещается внутри плиты, его скорости обнуляются, и оно закрепляется. Тест дополнительно ждёт 6 секунд и подтверждает активный lift, ровно одну смерть и одно сохранённое тело. + +## Автоматическая приёмка + +- `testDebugUnitTest`: 17 JVM tests, GREEN; +- `lintDebug`: GREEN; +- `assembleDebug` и `assembleDebugAndroidTest`: GREEN; +- полный unfiltered Android matrix на exact final APK: `OK (16 tests)`, `572.496 s`; +- внутри matrix: 36 игровых выполнений, lifecycle и restart; +- intended route уровней 1–10 выполняется по три раза без retry masking; +- уровень 8: дополнительный stressed run и три restart-free recovery run; +- уровень 10: отдельный тест стабильности тела, stressed traversal и три mastery run ровно с тремя телами; +- restart уровня 10 очищает тела, сигналы, таймер и удерживаемый ввод. + +## APK verification + +- package: `com.projectsacrifice.game.debug`; +- version: `versionCode=5`, `versionName=0.4.0-stage2-complete-debug`; +- SDK: `minSdk=23`, `targetSdk=34`; +- ZIP integrity: `unzip -t` — no errors; +- signature: APK Signature Scheme v1/v2 — verified, one Android Debug signer; +- clean install: successful; +- cold launch: `Status: ok`, `LaunchState: COLD`, `TotalTime: 972 ms`; +- Android crash buffer after launch: empty; +- SHA-256: `1bc14020915ebc3b9be4760e0ae83584d8bf2768bbf6e692de24562ec7256bcd`. + +## Граница подтверждения + +Отчёт доказывает инженерную проходимость и стабильность на указанном AVD. Он не подменяет внешний тест понятности: две ручные сессии на каждый уровень без объяснений разработчика остаются задачей `goon-game-human-playtest`. Историческая цель полного matrix менее 5 минут также не достигнута на throttled AVD; exact final набор из 16 методов занимает `9:32.496`.