Завершить инженерную проверку этапа 2

This commit is contained in:
2026-08-18 13:54:48 +03:00
parent 3ad959dde0
commit 80822b74b1
17 changed files with 384 additions and 69 deletions
+2
View File
@@ -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-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-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-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."}}
+4 -3
View File
@@ -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-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-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-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-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-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-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-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} {"_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-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-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-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-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-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} {"_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-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-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-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-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-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} {"_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}
+6 -2
View File
@@ -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 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 `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 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 scenario for an expired timer on the far side of the pit. Stage 2 additionally
phone connected, run the complete matrix with: 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 ```sh
./gradlew connectedDebugAndroidTest ./gradlew connectedDebugAndroidTest
+2 -2
View File
@@ -10,8 +10,8 @@ android {
applicationId 'com.projectsacrifice.game' applicationId 'com.projectsacrifice.game'
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode 4 versionCode 5
versionName '0.3.0-stage2-playability' versionName '0.4.0-stage2-complete'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
@@ -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<MainActivity> 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];
}
}
@@ -126,6 +126,63 @@ public final class LateLevelPlaythroughTest {
} }
} }
@Test
public void sawDeliveredBodyKeepsLevelTenLiftPoweredAfterSettling() {
try (ActivityScenario<MainActivity> 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 static final class GameDriver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation; private final Instrumentation instrumentation;
private final GameView gameView; private final GameView gameView;
@@ -156,10 +156,27 @@ final class LateLevelScenarios {
.step("Clear the lift body again", 2_000L, .step("Clear the lift body again", 2_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) 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(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) snapshot -> !snapshot.inputActive())
.step("Jump over the frozen block", 2_500L, .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(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f)
.step("Power the final relay", 3_000L, LevelPlaytestBot.Input.right(), .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, .step("Clear the retained lift body", 2_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 860f) 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(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() >= 720f) snapshot -> !snapshot.inputActive())
.step("Clear the frozen block", 2_500L, .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(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_240f) 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, .step("Approach the powered relay", 2_500L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerX() >= 1_320f) snapshot -> snapshot.playerX() >= 1_320f)
@@ -201,15 +240,23 @@ final class LateLevelScenarios {
.step("Clear the powered relay", 2_000L, .step("Clear the powered relay", 2_000L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_650f) 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(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerAlive() snapshot -> snapshot.playerAlive()
&& snapshot.playerY() >= 770f
&& !Float.isNaN(snapshot.firstMovingPlatformY()) && !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, .step("Take off toward the moving lift", 1_000L,
LevelPlaytestBot.Input.rightJump(), LevelPlaytestBot.Input.rightJump(),
snapshot -> snapshot.playerAlive() && snapshot.playerY() <= 670f) snapshot -> snapshot.playerAlive()
&& (snapshot.playerOnMovingPlatform()
|| snapshot.playerY() <= 670f))
.step("Board the moving lift", 2_000L, .step("Board the moving lift", 2_000L,
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
GameView.AutomationSnapshot::playerOnMovingPlatform) GameView.AutomationSnapshot::playerOnMovingPlatform)
@@ -221,7 +268,7 @@ final class LateLevelScenarios {
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
snapshot -> snapshot.playerOnMovingPlatform() snapshot -> snapshot.playerOnMovingPlatform()
&& snapshot.playerX() >= 1_920f) && 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(), LevelPlaytestBot.Input.none(),
snapshot -> snapshot.playerY() <= 310f) snapshot -> snapshot.playerY() <= 310f)
.step("Move to the raised lift edge", 2_000L, .step("Move to the raised lift edge", 2_000L,
@@ -236,4 +283,5 @@ final class LateLevelScenarios {
GameView.AutomationSnapshot::completed) GameView.AutomationSnapshot::completed)
.build(); .build();
} }
} }
@@ -79,7 +79,8 @@ final class LevelEightScenario {
snapshot = await(driver, deadline, "Wait for the moving platform", snapshot = await(driver, deadline, "Wait for the moving platform",
LevelPlaytestBot.Input.none(), 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; if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true); driver.setInput(false, true, true);
@@ -87,7 +88,7 @@ final class LevelEightScenario {
driver.setInput(false, true, false); driver.setInput(false, true, false);
snapshot = await(driver, deadline, "Land on the moving platform", snapshot = await(driver, deadline, "Land on the moving platform",
LevelPlaytestBot.Input.right(), LevelPlaytestBot.Input.right(),
current -> current.elapsedMilliseconds() >= timerStartedAt + 3_200L); GameView.AutomationSnapshot::playerOnMovingPlatform);
if (failed(snapshot, deadline, driver)) return snapshot; if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(false, true, true); driver.setInput(false, true, true);
@@ -107,18 +108,15 @@ final class LevelEightScenario {
private GameView.AutomationSnapshot awaitFarFloorLandingOnOriginalTimer( private GameView.AutomationSnapshot awaitFarFloorLandingOnOriginalTimer(
LevelPlaytestBot.Driver driver, long deadline LevelPlaytestBot.Driver driver, long deadline
) { ) {
boolean moveLeft = true; GameView.AutomationSnapshot snapshot = await(driver, deadline,
GameView.AutomationSnapshot snapshot = driver.snapshot(); "Land on the far floor", LevelPlaytestBot.Input.right(),
while (driver.currentTimeMillis() < deadline && snapshot.playerAlive() current -> current.playerX() >= 1_510f
&& !(snapshot.playerX() >= 1_510f && snapshot.playerX() < 1_580f && current.playerY() >= 779f && current.allDoorsOpen());
&& snapshot.playerY() >= 779f && snapshot.allDoorsOpen())) { if (failed(snapshot, deadline, driver)) return snapshot;
driver.setInput(moveLeft, !moveLeft, false); return await(driver, deadline, "Stabilize on the far floor",
moveLeft = !moveLeft; LevelPlaytestBot.Input.left(),
driver.waitForNextDecision(DECISION_MILLIS); current -> current.playerX() < 1_580f
snapshot = driver.snapshot(); && current.playerY() >= 779f && current.allDoorsOpen());
}
driver.setInput(false, false, false);
return snapshot;
} }
private Result finishFromFarSide(LevelPlaytestBot.Driver driver, long deadline, private Result finishFromFarSide(LevelPlaytestBot.Driver driver, long deadline,
@@ -53,8 +53,12 @@ public final class GameView extends View implements Runnable {
private final float playerY; private final float playerY;
private final int deaths; private final int deaths;
private final int corpseCount; private final int corpseCount;
private final float firstMovingPlatformX;
private final float firstMovingPlatformY; private final float firstMovingPlatformY;
private final boolean playerOnMovingPlatform; private final boolean playerOnMovingPlatform;
private final boolean firstMovingPlatformPowered;
private final float firstCorpseX;
private final boolean playerOnGround;
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber, 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) { float playerX, float playerY, int deaths, int corpseCount) {
this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber, this(completed, playerAlive, allDoorsOpen, visiblyPaused, inputActive, levelNumber,
elapsedMilliseconds, playerX, playerY, deaths, corpseCount, elapsedMilliseconds, playerX, playerY, deaths, corpseCount,
Float.NaN, false); Float.NaN, Float.NaN, false, false, Float.NaN, false);
} }
AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen, AutomationSnapshot(boolean completed, boolean playerAlive, boolean allDoorsOpen,
boolean visiblyPaused, boolean inputActive, int levelNumber, boolean visiblyPaused, boolean inputActive, int levelNumber,
long elapsedMilliseconds, long elapsedMilliseconds,
float playerX, float playerY, int deaths, int corpseCount, 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.completed = completed;
this.playerAlive = playerAlive; this.playerAlive = playerAlive;
this.allDoorsOpen = allDoorsOpen; this.allDoorsOpen = allDoorsOpen;
@@ -81,8 +88,12 @@ public final class GameView extends View implements Runnable {
this.playerY = playerY; this.playerY = playerY;
this.deaths = deaths; this.deaths = deaths;
this.corpseCount = corpseCount; this.corpseCount = corpseCount;
this.firstMovingPlatformX = firstMovingPlatformX;
this.firstMovingPlatformY = firstMovingPlatformY; this.firstMovingPlatformY = firstMovingPlatformY;
this.playerOnMovingPlatform = playerOnMovingPlatform; this.playerOnMovingPlatform = playerOnMovingPlatform;
this.firstMovingPlatformPowered = firstMovingPlatformPowered;
this.firstCorpseX = firstCorpseX;
this.playerOnGround = playerOnGround;
} }
public boolean completed() { return completed; } public boolean completed() { return completed; }
@@ -96,8 +107,12 @@ public final class GameView extends View implements Runnable {
public float playerY() { return playerY; } public float playerY() { return playerY; }
public int deaths() { return deaths; } public int deaths() { return deaths; }
public int corpseCount() { return corpseCount; } public int corpseCount() { return corpseCount; }
public float firstMovingPlatformX() { return firstMovingPlatformX; }
public float firstMovingPlatformY() { return firstMovingPlatformY; } public float firstMovingPlatformY() { return firstMovingPlatformY; }
public boolean playerOnMovingPlatform() { return playerOnMovingPlatform; } 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 { private static final class Player {
@@ -235,6 +250,8 @@ public final class GameView extends View implements Runnable {
respawnTimer = 0f; respawnTimer = 0f;
cameraX = 0f; cameraX = 0f;
userPaused = false; userPaused = false;
pointers.clear();
deriveControls();
corpses.clear(); corpses.clear();
platformStates.clear(); platformStates.clear();
toggleSignals.clear(); toggleSignals.clear();
@@ -465,11 +482,29 @@ public final class GameView extends View implements Runnable {
resolveCorpseHorizontal(corpse); resolveCorpseHorizontal(corpse);
corpse.y += corpse.vy * dt; corpse.y += corpse.vy * dt;
resolveCorpseVertical(corpse); resolveCorpseVertical(corpse);
if (corpse.grounded && settleCorpseOnPressurePlate(corpse)) continue;
if (corpse.grounded) corpse.vx *= (float) Math.pow(0.05, dt); if (corpse.grounded) corpse.vx *= (float) Math.pow(0.05, dt);
if (Math.abs(corpse.vx) < 4f) corpse.vx = 0f; 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) { private void resolveCorpseHorizontal(Corpse corpse) {
RectF current = corpse.rect(); RectF current = corpse.rect();
for (RectF solid : solidRects(false)) { for (RectF solid : solidRects(false)) {
@@ -1131,6 +1166,10 @@ public final class GameView extends View implements Runnable {
startLevel(oneBasedLevelNumber - 1); startLevel(oneBasedLevelNumber - 1);
} }
public void restartLevelForAutomation() {
restartLevel();
}
public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) { public void setAutomationInput(boolean moveLeft, boolean moveRight, boolean jump) {
boolean jumpWasPressed = jumpPressed; boolean jumpWasPressed = jumpPressed;
leftPressed = moveLeft; leftPressed = moveLeft;
@@ -1144,10 +1183,14 @@ public final class GameView extends View implements Runnable {
if (level != null) { if (level != null) {
for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door); for (LevelDefinition.Door door : level.doors) allOpen &= doorOpen(door);
} }
float firstMovingPlatformX = Float.NaN;
float firstMovingPlatformY = Float.NaN; float firstMovingPlatformY = Float.NaN;
boolean firstMovingPlatformPowered = false;
for (PlatformState state : platformStates) { for (PlatformState state : platformStates) {
if (state.definition.isMoving()) { if (state.definition.isMoving()) {
firstMovingPlatformX = state.x;
firstMovingPlatformY = state.y; firstMovingPlatformY = state.y;
firstMovingPlatformPowered = signalActive(state.definition.controlId);
break; break;
} }
} }
@@ -1167,8 +1210,12 @@ public final class GameView extends View implements Runnable {
player.y, player.y,
deaths, deaths,
corpses.size(), corpses.size(),
firstMovingPlatformX,
firstMovingPlatformY, firstMovingPlatformY,
playerOnMovingPlatform playerOnMovingPlatform,
firstMovingPlatformPowered,
corpses.isEmpty() ? Float.NaN : corpses.get(0).x,
player.onGround
); );
} }
} }
@@ -109,12 +109,12 @@ public final class LevelCatalog {
8, "Borrowed Time", "Jump for the far blue relay if the timed gate closes.", 8, "Borrowed Time", "Jump for the far blue relay if the timed gate closes.",
2450, 130, 760, goal(2220, 600), 0, 38, 2450, 130, 760, goal(2220, 600), 0, 38,
platforms( platforms(
floor(0, 970), floor(1510, 940), ledge(1980, 710, 470), floor(0, 1040), floor(1510, 940), ledge(1980, 710, 470),
moving(990, 770, 240, 34, 380, 0, 4.0f, "clock") 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( 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) sw(TIMED, 1600, 650, 90, 70, "clock", 3.5f, 1)
), ),
doors(door(1770, 560, 82, 320, "clock")) doors(door(1770, 560, 82, 320, "clock"))
@@ -141,11 +141,11 @@ public final class LevelCatalog {
3100, 130, 760, goal(2880, 330), 3, 120, 3100, 130, 760, goal(2880, 330), 3, 120,
platforms( platforms(
floor(0, 1940), floor(2140, 960), ledge(2420, 440, 680), 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( hazards(
hazard(SAW, 520, 760, 96, 96, null, 700, -240), 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(ELECTRIC, 1510, 780, 110, 100, "power", 0, 0),
hazard(PIT, 1940, 880, 200, 200) hazard(PIT, 1940, 880, 200, 200)
), ),
@@ -144,6 +144,7 @@ public final class LevelPlaytestBot {
Step step = scenario.steps.get(stepIndex); Step step = scenario.steps.get(stepIndex);
lastStep = step.name; lastStep = step.name;
if (step.completion.reached(last)) { if (step.completion.reached(last)) {
driver.setInput(false, false, false);
stepIndex++; stepIndex++;
stepStartedAt = driver.currentTimeMillis(); stepStartedAt = driver.currentTimeMillis();
continue; continue;
@@ -100,6 +100,24 @@ public final class LevelCatalogTest {
freezer.box.x, freezer.box.y, freezer.box.width, freezer.box.height)); 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 @Test
public void masteryLiftHasLaunchOverlapAndStillSpansARealPit() { public void masteryLiftHasLaunchOverlapAndStillSpansARealPit() {
LevelDefinition level = LevelCatalog.get(9); LevelDefinition level = LevelCatalog.get(9);
@@ -118,17 +136,40 @@ public final class LevelCatalogTest {
float launchOverlap = Math.min(launchFloor.box.right(), lift.box.right()) float launchOverlap = Math.min(launchFloor.box.right(), lift.box.right())
- Math.max(launchFloor.box.x, lift.box.x); - Math.max(launchFloor.box.x, lift.box.x);
assertTrue("The floor must overlap the lift by at least 100px for a stable launch", assertTrue("The floor must overlap the lift by at least 140px for a delayed launch",
launchOverlap >= 100f); launchOverlap >= 140f);
assertEquals("The pit must begin exactly where the launch floor ends", assertEquals("The pit must begin exactly where the launch floor ends",
launchFloor.box.right(), pit.box.x, 0f); launchFloor.box.right(), pit.box.x, 0f);
assertEquals("The pit must continue to the landing floor", assertEquals("The pit must continue to the landing floor",
landingFloor.box.x, pit.box.right(), 0f); landingFloor.box.x, pit.box.right(), 0f);
assertTrue("The final pit must remain at least 200px wide", pit.box.width >= 200f); 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", assertTrue("The upper gallery must still require the vertical lift",
lift.travelY <= -400f && level.goal.y < launchFloor.box.y - 300f); 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 @Test
public void everyDoorAndControlledPlatformHasADeclaredSignalSource() { public void everyDoorAndControlledPlatformHasADeclaredSignalSource() {
for (LevelDefinition level : LevelCatalog.all()) { for (LevelDefinition level : LevelCatalog.all()) {
@@ -23,6 +23,8 @@ public final class LevelPlaytestBotTest {
assertTrue(result.diagnostic(), result.completed()); assertTrue(result.diagnostic(), result.completed());
assertEquals(7, driver.startedLevel); assertEquals(7, driver.startedLevel);
assertEquals("Reach the goal", result.lastStep()); assertEquals("Reach the goal", result.lastStep());
assertTrue("Completed steps must release held controls before the next action",
driver.inputReleases >= 2);
} }
@Test @Test
@@ -98,6 +100,7 @@ public final class LevelPlaytestBotTest {
private int deaths; private int deaths;
private boolean completed; private boolean completed;
private final boolean progresses; private final boolean progresses;
private int inputReleases;
ScriptedDriver() { ScriptedDriver() {
this(true); this(true);
@@ -119,6 +122,7 @@ public final class LevelPlaytestBotTest {
@Override @Override
public void setInput(boolean moveLeft, boolean moveRight, boolean jump) { public void setInput(boolean moveLeft, boolean moveRight, boolean jump) {
if (!moveLeft && !moveRight && !jump) inputReleases++;
this.moveLeft = moveLeft; this.moveLeft = moveLeft;
this.moveRight = moveRight; this.moveRight = moveRight;
} }
+2 -2
View File
@@ -22,9 +22,9 @@ master tracks origin/master
- предметные задачи с acceptance criteria, приоритетами и зависимостями; - предметные задачи с acceptance criteria, приоритетами и зависимостями;
- уже подтверждённые кодом и QA элементы закрыты с причиной. - уже подтверждённые кодом и 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 завершённым.
## Ежедневные команды ## Ежедневные команды
+9 -14
View File
@@ -6,7 +6,7 @@
## 1. Текущее состояние ## 1. Текущее состояние
Проект находится на стадии проверенного vertical slice: Инженерная часть этапа 2 завершена и проект готов к параллельному старту этапа 3:
- реализованы 10 последовательных уровней; - реализованы 10 последовательных уровней;
- работают движение, прыжок и мобильное multitouch-управление; - работают движение, прыжок и мобильное multitouch-управление;
@@ -16,12 +16,14 @@
- intended route всех уровней 1–10 защищён Android acceptance-ботами: каждый маршрут выполняется по три раза через production-ввод без прямого перемещения персонажа; - intended route всех уровней 1–10 защищён Android acceptance-ботами: каждый маршрут выполняется по три раза через production-ввод без прямого перемещения персонажа;
- blocker-softlock уровня 8 после истечения таймера устранён recovery relay и покрыт отдельным трёхкратным сценарием; - blocker-softlock уровня 8 после истечения таймера устранён recovery relay и покрыт отдельным трёхкратным сценарием;
- исправлено перекрытие `FREEZER` уровня 6, которое повторно убивало игрока на frozen stepping stone; - исправлено перекрытие `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-подписи корректны. - финальный 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. Принципы разработки ## 2. Принципы разработки
@@ -173,15 +175,8 @@ Performance-оговорка: критерий этапа 1 «полный пр
8. холодный запуск и проверка crash-буфера; 8. холодный запуск и проверка crash-буфера;
9. ручной smoke-тест уровня 1 и одного сложного комбинированного уровня. 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; Следующая реализационная итерация — этап 3 (управление, интерфейс и доступность). Параллельно внешний QA должен выполнить `goon-game-human-playtest`: по две ручные сессии каждого уровня без подсказок разработчика. До получения этого протокола milestone Vertical Slice нельзя называть принятым внешними тестировщиками, хотя разработку этапа 3 можно начинать.
- провести по две ручные сессии каждого уровня и зафиксировать попытки, restart и непонятные места;
- скорректировать допуски прыжков и подсказки только по наблюдаемым проблемам;
- проверить чистый restart из ключевых промежуточных состояний;
- закрыть или документировать гипотезы альтернативных решений уровней 2 и 10;
- не добавлять уровень 11 и новые механики до завершения критериев этапа 2.
Результатом итерации должны стать обновлённый исходный проект, проверенный APK, integrated bot report и протокол ручных игровых сессий.
+10 -10
View File
@@ -199,15 +199,15 @@ Door закрывает путь к goal
**Статус:** intended route и восстановление после истечения таймера доказаны production-input ботом, по три раза подряд. **Статус:** intended route и восстановление после истечения таймера доказаны production-input ботом, по три раза подряд.
**Ожидаемые смерти:** `0` (`targetDeaths=0`). **Ожидаемые смерти:** `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 ### Solution graph 8
```text ```text
PIT разрывает floor, а Door закрывает дальнейший путь PIT разрывает floor, а Door закрывает дальнейший путь
→ войти в TIMED и запустить signal "clock" на 6.0 s → войти в TIMED и запустить signal "clock" на 8.0 s
→ "clock" одновременно запускает horizontal moving platform и открывает Door → "clock" одновременно запускает horizontal moving platform и открывает Door
→ без задержки добежать до края floor x=970 → без задержки добежать до края floor x=1040
→ прыгнуть на moving platform и пересечь PIT → прыгнуть на moving platform и пересечь PIT
→ сойти на правый floor x≥1510 → сойти на правый floor x≥1510
→ пройти Door x=1770..1852 до истечения "clock" → пройти Door x=1770..1852 до истечения "clock"
@@ -222,9 +222,9 @@ PIT разрывает floor, а Door закрывает дальнейший п
→ пройти Door и достичь goal без смерти/restart → пройти 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. **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` без ретраев. **Статус:** mastery-маршрут доказан production-input ботом: 3/3 последовательных прохождения и ограниченный стресс финального перехода `96 ms` без ретраев.
**Ожидаемые и доказанные смерти:** `3` (`targetDeaths=3`). **Ожидаемые и доказанные смерти:** `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 ### Solution graph 10
@@ -267,23 +267,23 @@ Final Door использует AND из "weight" и "power"
Goal расположен на верхнем уступе за PIT и закрытой высокой Door Goal расположен на верхнем уступе за PIT и закрытой высокой Door
→ первый доброволец входит в SAW → первый доброволец входит в SAW
→ impulse (700,-240) доставляет тело на PRESSURE x=860..1020 → impulse (700,-240) доставляет тело на PRESSURE x=860..1020
→ тело удерживает signal "lift" и запускает вертикальную moving platform → тело оседает и фиксируется на PRESSURE, удерживает signal "lift" и запускает vertical moving platform
→ следующий доброволец погибает в FREEZER → следующий доброволец погибает в FREEZER
→ frozen corpse становится стабильной ступенью для повторного прохода этой зоны → frozen corpse становится стабильной ступенью для повторного прохода этой зоны
→ следующий доброволец погибает в ELECTRIC → следующий доброволец погибает в ELECTRIC
→ conductive corpse удерживает relay signal "power" и открывает Door → conductive corpse удерживает relay signal "power" и открывает Door
→ четвёртый доброволец respawn-ится и пересекает сохранённые тела/опасности → четвёртый доброволец respawn-ится и пересекает сохранённые тела/опасности
→ садится на активную platform у x=1840..2090 → садится на активную platform у x=1800..2090
→ поднимается над PIT и в верхней фазе прыгает вправо через открытую Door → поднимается над PIT и в верхней фазе прыгает вправо через открытую Door
→ приземляется на ledge x=2420..3100 → приземляется на ledge x=2420..3100
→ достигает goal и получает COMPLETE с тремя смертями → достигает 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`. **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 остаётся стабильным.
## Проверки, следующие из графов ## Проверки, следующие из графов
+35
View File
@@ -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`.