Добавить вторую волну мира Foundations

This commit is contained in:
2026-08-19 11:25:52 +03:00
parent b31c2ea720
commit 4d8aa28f0a
19 changed files with 556 additions and 25 deletions
+1 -1
View File
@@ -67,7 +67,7 @@
{"_type":"issue","id":"goon-game-analytics-contract","title":"Схема gameplay analytics","description":"Версионировать level_start/restart/death/complete/abandon/challenge и heatmap payload.","acceptance_criteria":"События документированы, минимальны и проверены на тестовом backend.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:36Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:35Z","metadata":{"stages":7,"primary_stage":7},"labels":["analytics","primary-stage-7","production","stage-7"],"dependencies":[{"issue_id":"goon-game-analytics-contract","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-analytics-contract","title":"Схема gameplay analytics","description":"Версионировать level_start/restart/death/complete/abandon/challenge и heatmap payload.","acceptance_criteria":"События документированы, минимальны и проверены на тестовом backend.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"task","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":960,"created_at":"2026-08-17T21:40:36Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:35Z","metadata":{"stages":7,"primary_stage":7},"labels":["analytics","primary-stage-7","production","stage-7"],"dependencies":[{"issue_id":"goon-game-analytics-contract","depends_on_id":"goon-game-e14","type":"parent-child","created_at":"2026-08-18T00:43:42Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"goon-game-modular-characters","title":"Модульные персонажи и косметика","description":"Body/helmet/face/weapon/cape/accessory, случайные имена и 30+ косметических элементов.","acceptance_criteria":"Новые жертвы визуально различаются, комбинации не ломают анимации.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2880,"created_at":"2026-08-17T21:40:21Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["art","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-modular-characters","depends_on_id":"goon-game-e11","type":"parent-child","created_at":"2026-08-18T00:43:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-modular-characters","title":"Модульные персонажи и косметика","description":"Body/helmet/face/weapon/cape/accessory, случайные имена и 30+ косметических элементов.","acceptance_criteria":"Новые жертвы визуально различаются, комбинации не ломают анимации.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"open","priority":2,"issue_type":"feature","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2880,"created_at":"2026-08-17T21:40:21Z","created_by":"E.Gavrilov","updated_at":"2026-08-18T21:02:25Z","metadata":{"stages":6,"primary_stage":6},"labels":["art","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-modular-characters","depends_on_id":"goon-game-e11","type":"parent-child","created_at":"2026-08-18T00:43:33Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"goon-game-challenges","title":"Challenges, collectibles и статистика","description":"Реализовать casualty/time/collectible цели и отображение трёх звёзд.","acceptance_criteria":"Расчёт покрыт unit-тестами и сохраняется по LevelID.","notes":"2026-08-19: claimed after weighted-object slice 259fcd3 passed 108 app JVM + 2 content-tool JVM and exact Android 48/48. TDD scope: stable LevelID/WorldID progression keys with legacy numeric migration, casualty/time/collectible challenge evaluation, persistent best three-star record, data-driven optional collectible, level-select/result rendering, automation and one teaching route without weakening existing gates.\nFinal post-review gate: duplicate stable LevelID now fails catalog load before progress-key collision (targeted RED then GREEN). Clean 116 app JVM + 2 content-tool JVM; final cold host-GPU AVD exact Android 52/52 in 551.773s. Verified APK SHA-256 0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:13Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T06:24:46Z","started_at":"2026-08-19T04:41:26Z","closed_at":"2026-08-19T05:58:26Z","close_reason":"Accepted stable LevelID/WorldID challenge progression: progress v3/session v5 migrations, casualty/time/collectible stars, level 18 3/3 route, 115+2 JVM and exact Android 52/52 (553.686s), APK SHA-256 9c12cfbb15e313421d58f891d9d107af14c9e8e626228fa8fd0c74e56c293d14.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","progression","stage-6"],"dependencies":[{"issue_id":"goon-game-challenges","depends_on_id":"goon-game-e09","type":"parent-child","created_at":"2026-08-18T00:43:29Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-challenges","title":"Challenges, collectibles и статистика","description":"Реализовать casualty/time/collectible цели и отображение трёх звёзд.","acceptance_criteria":"Расчёт покрыт unit-тестами и сохраняется по LevelID.","notes":"2026-08-19: claimed after weighted-object slice 259fcd3 passed 108 app JVM + 2 content-tool JVM and exact Android 48/48. TDD scope: stable LevelID/WorldID progression keys with legacy numeric migration, casualty/time/collectible challenge evaluation, persistent best three-star record, data-driven optional collectible, level-select/result rendering, automation and one teaching route without weakening existing gates.\nFinal post-review gate: duplicate stable LevelID now fails catalog load before progress-key collision (targeted RED then GREEN). Clean 116 app JVM + 2 content-tool JVM; final cold host-GPU AVD exact Android 52/52 in 551.773s. Verified APK SHA-256 0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1440,"created_at":"2026-08-17T21:40:13Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T06:24:46Z","started_at":"2026-08-19T04:41:26Z","closed_at":"2026-08-19T05:58:26Z","close_reason":"Accepted stable LevelID/WorldID challenge progression: progress v3/session v5 migrations, casualty/time/collectible stars, level 18 3/3 route, 115+2 JVM and exact Android 52/52 (553.686s), APK SHA-256 9c12cfbb15e313421d58f891d9d107af14c9e8e626228fa8fd0c74e56c293d14.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","progression","stage-6"],"dependencies":[{"issue_id":"goon-game-challenges","depends_on_id":"goon-game-e09","type":"parent-child","created_at":"2026-08-18T00:43:29Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T07:50:26Z","started_at":"2026-08-19T06:26:20Z","metadata":{"stages":6,"primary_stage":6},"labels":["future","levels","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:41:41Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-content-worlds","title":"Спроектировать и произвести 4 мира / 55–70 уровней","description":"Content matrix четырёх миров, challenge-уровней и финальных испытаний по разделам 2 и 23.","acceptance_criteria":"Все уровни проходят content pipeline и Definition of Done.","notes":"2026-08-19: executable matrix commit 4869649 fixed release at 69 levels: workshop 1-17 plus four 13-level production worlds (52 rows). Foundations wave 1 now authors Levels 19-22; matrix prefix is 5/52 including Level 18. TDD/device REDs fixed only in new content: inaccessible L19 cache, skipped second L20 body, late L21 saw takeoff, early-moving L22 ferry; pagination acceptance updated from 2 to 3 pages. Four route methods passed 12/12 playthroughs, restart DoD passed 1/1. Clean gate: 117/117 app JVM, 7/7 level-format, validators 22/authored5, lint/app/test APK; final cold unfiltered Android 57/57 in 1807.264s, APK SHA 81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef. Next bounded slice is Foundations 23-26; issue remains in_progress until Level 69.\n2026-08-19: Foundations wave 2 authors Levels 23-26; matrix prefix is 9/52. Remote saw delivery, frozen-body step and conductive relay pass exact 1 death/1 corpse. Level 26 Second Hand passes original-timer and expired-timer recovery routes, both 3/3 with zero deaths; recovery explicitly observes the far Door closed before rearming the shared signal. Focused suite: 5/5 methods, 15 routes, 10.768s. Clean gate: 118/118 app JVM, 7/7 level-format, validators 26/authored9, lint/app/test APK; final cold unfiltered Android 62/62 in 1063.114s, APK SHA 67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa. Next bounded slice is Foundations 27-30; issue remains in_progress until Level 69.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"in_progress","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","created_at":"2026-08-17T21:39:58Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T08:24:33Z","started_at":"2026-08-19T06:26:20Z","metadata":{"stages":6,"primary_stage":6},"labels":["future","levels","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-e07","type":"parent-child","created_at":"2026-08-18T00:43:21Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-content-worlds","depends_on_id":"goon-game-milestone-vslice","type":"blocks","created_at":"2026-08-18T00:41:41Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"goon-game-weighted-objects","title":"Добавить общий IWeightedObject и физические объекты","description":"Добавить box, rock, ball, ice block и counterweight поверх единого контракта массы.","acceptance_criteria":"Pressure plate одинаково работает с персонажем, телом и объектами.","notes":"2026-08-19: claimed after corpse-state slice 19bb64e passed 102 JVM and exact Android 46/46. TDD scope: public IWeightedObject mass contract, JSON Box/Rock/Ball/IceBlock/Counterweight definitions, deterministic runtime/save/render/automation, pressure parity and one teaching level with solution bot.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T04:38:25Z","started_at":"2026-08-19T03:35:44Z","closed_at":"2026-08-19T04:38:25Z","close_reason":"Shared pressure-mass contract, five authored physical-object types, deterministic runtime/save v4/render/export, level 17 and production-input route accepted. Clean 108 app JVM + 2 format JVM; focused 8/8; exact unfiltered Android 16 matrix 48/48 in 2223.695s.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-weighted-objects","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:14Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-weighted-objects","title":"Добавить общий IWeightedObject и физические объекты","description":"Добавить box, rock, ball, ice block и counterweight поверх единого контракта массы.","acceptance_criteria":"Pressure plate одинаково работает с персонажем, телом и объектами.","notes":"2026-08-19: claimed after corpse-state slice 19bb64e passed 102 JVM and exact Android 46/46. TDD scope: public IWeightedObject mass contract, JSON Box/Rock/Ball/IceBlock/Counterweight definitions, deterministic runtime/save/render/automation, pressure parity and one teaching level with solution bot.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:43Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T04:38:25Z","started_at":"2026-08-19T03:35:44Z","closed_at":"2026-08-19T04:38:25Z","close_reason":"Shared pressure-mass contract, five authored physical-object types, deterministic runtime/save v4/render/export, level 17 and production-input route accepted. Clean 108 app JVM + 2 format JVM; focused 8/8; exact unfiltered Android 16 matrix 48/48 in 2223.695s.","metadata":{"stages":6,"primary_stage":6},"labels":["primary-stage-6","production","puzzle","stage-6"],"dependencies":[{"issue_id":"goon-game-weighted-objects","depends_on_id":"goon-game-e05","type":"parent-child","created_at":"2026-08-18T00:43:14Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"goon-game-hazards-full","title":"Дополнить production-каталог опасностей","description":"Добавить crusher, fire, projectile, explosion и data-driven настройки импульса/звука/визуала.","acceptance_criteria":"Каждая опасность имеет prefab/data definition, тесты и минимум один учебный уровень.","notes":"2026-08-19: claimed after the three Stage 6 foundation slices. Will proceed test-first per hazard with data definition, deterministic behavior, teaching content, solution graph and production-input route; crusher/fire precede the dependent crushed/burned corpse states.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2400,"created_at":"2026-08-17T21:39:37Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T02:28:37Z","started_at":"2026-08-19T01:07:11Z","closed_at":"2026-08-19T02:28:37Z","close_reason":"Crusher, fire, projectile and cyclic explosion are data-driven, validated and shipped with teaching levels 11-14, solution graphs and 3x production-input routes; clean 99 JVM/lint/APK and unfiltered Android 43/43 green.","metadata":{"stages":6,"primary_stage":6},"labels":["hazards","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-hazards-full","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:10Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0} {"_type":"issue","id":"goon-game-hazards-full","title":"Дополнить production-каталог опасностей","description":"Добавить crusher, fire, projectile, explosion и data-driven настройки импульса/звука/визуала.","acceptance_criteria":"Каждая опасность имеет prefab/data definition, тесты и минимум один учебный уровень.","notes":"2026-08-19: claimed after the three Stage 6 foundation slices. Will proceed test-first per hazard with data definition, deterministic behavior, teaching content, solution graph and production-input route; crusher/fire precede the dependent crushed/burned corpse states.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":2400,"created_at":"2026-08-17T21:39:37Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T02:28:37Z","started_at":"2026-08-19T01:07:11Z","closed_at":"2026-08-19T02:28:37Z","close_reason":"Crusher, fire, projectile and cyclic explosion are data-driven, validated and shipped with teaching levels 11-14, solution graphs and 3x production-input routes; clean 99 JVM/lint/APK and unfiltered Android 43/43 green.","metadata":{"stages":6,"primary_stage":6},"labels":["hazards","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-hazards-full","depends_on_id":"goon-game-e04","type":"parent-child","created_at":"2026-08-18T00:43:10Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"goon-game-corpse-states","title":"Расширить состояния тел","description":"Добавить crushed, burned и остальные утверждённые состояния поверх normal/pinned/frozen/conductive.","acceptance_criteria":"Каждое состояние используется минимум одной уникальной головоломкой и проходит DoD механики.","notes":"2026-08-19: claimed after production hazards dc2ebcb passed 99 JVM and Android 43/43. Will add explicit crushed/burned/shattered and derived corpse behavior test-first, then one teaching route/solution graph per new gameplay-relevant state and full regression.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T03:32:49Z","started_at":"2026-08-19T02:31:25Z","closed_at":"2026-08-19T03:32:49Z","close_reason":"GDD corpse states complete: existing normal/pinned/frozen/conductive routes plus new burned/crushed contracts, save v3 migration, levels 15-16 and bots. Clean 102 JVM; exact Android 46/46 in 1842.248s. IsDestroyed is a lifecycle field; Shattered is not an approved GDD state.","metadata":{"stages":6,"primary_stage":6},"labels":["corpse","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:05Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-hazards-full","type":"blocks","created_at":"2026-08-19T04:07:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"goon-game-corpse-states","title":"Расширить состояния тел","description":"Добавить crushed, burned и остальные утверждённые состояния поверх normal/pinned/frozen/conductive.","acceptance_criteria":"Каждое состояние используется минимум одной уникальной головоломкой и проходит DoD механики.","notes":"2026-08-19: claimed after production hazards dc2ebcb passed 99 JVM and Android 43/43. Will add explicit crushed/burned/shattered and derived corpse behavior test-first, then one teaching route/solution graph per new gameplay-relevant state and full regression.","spec_id":"docs/DEVELOPMENT_PLAN.md","status":"closed","priority":2,"issue_type":"feature","assignee":"E.Gavrilov","owner":"gavrilovdev@yandex-team.ru","estimated_minutes":1920,"created_at":"2026-08-17T21:39:29Z","created_by":"E.Gavrilov","updated_at":"2026-08-19T03:32:49Z","started_at":"2026-08-19T02:31:25Z","closed_at":"2026-08-19T03:32:49Z","close_reason":"GDD corpse states complete: existing normal/pinned/frozen/conductive routes plus new burned/crushed contracts, save v3 migration, levels 15-16 and bots. Clean 102 JVM; exact Android 46/46 in 1842.248s. IsDestroyed is a lifecycle field; Shattered is not an approved GDD state.","metadata":{"stages":6,"primary_stage":6},"labels":["corpse","primary-stage-6","production","stage-6"],"dependencies":[{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-e02","type":"parent-child","created_at":"2026-08-18T00:43:05Z","created_by":"E.Gavrilov","metadata":"{}"},{"issue_id":"goon-game-corpse-states","depends_on_id":"goon-game-hazards-full","type":"blocks","created_at":"2026-08-19T04:07:09Z","created_by":"E.Gavrilov","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
+9 -4
View File
@@ -129,6 +129,10 @@ The first bounded Foundations authoring wave adds levels 19–22: a two-jump
movement route, a two-body spike bridge, a retained pressure-door body and a movement route, a two-body spike bridge, a retained pressure-door body and a
toggle-controlled horizontal ferry. All four routes pass three consecutive runs, toggle-controlled horizontal ferry. All four routes pass three consecutive runs,
and restart clears the stateful bridge/ferry setups. and restart clears the stateful bridge/ferry setups.
The second wave adds levels 23–26: remote saw delivery, a frozen-body step, a
conductive-body relay and a timed ferry with an explicit far-side recovery relay.
Five route methods prove all four levels three times, including both the original
timer and expired-timer recovery routes for level 26.
Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`, Scope and final evidence are in `docs/STAGE6_CHARACTER_STATES.md`,
`docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest `docs/STAGE6_CORPSE_MANAGER.md`, and `docs/STAGE6_PUZZLE_SYSTEM.md`; the latest
hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed hazard evidence is in `docs/STAGE6_PRODUCTION_HAZARDS.md`, and the burned/crushed
@@ -138,10 +142,11 @@ contract is in `docs/STAGE6_CORPSE_STATES.md`. Weighted-object evidence is in
69 levels: 17 accepted workshop levels plus 52 production slots in Foundations, 69 levels: 17 accepted workshop levels plus 52 production slots in Foundations,
Physics, States and Mastery. Its contract and Level DoD pipeline are documented Physics, States and Mastery. Its contract and Level DoD pipeline are documented
in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in
`docs/STAGE6_FOUNDATIONS_WAVE1.md`. The latest exact gate is `117 app JVM + 7 `docs/STAGE6_FOUNDATIONS_WAVE1.md` and
content-tool JVM / 57 Android` on the twenty-two-level candidate (`57/57`, `docs/STAGE6_FOUNDATIONS_WAVE2.md`. The latest exact gate is `118 app JVM + 7
1807.264 seconds). The verified debug APK SHA-256 is content-tool JVM / 62 Android` on the twenty-six-level candidate (`62/62`,
`81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`. 1063.114 seconds). The verified debug APK SHA-256 is
`67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`.
The debug APK is written to `app/build/outputs/apk/debug/app-debug.apk`. It is signed with Android's standard debug key and can be installed with: The debug APK is written to `app/build/outputs/apk/debug/app-debug.apk`. It is signed with Android's standard debug key and can be installed with:
@@ -0,0 +1,88 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
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 FoundationsAdvancedPlaythroughTest {
@Test public void remoteVolunteerUsesOneDeliveredBodyThreeTimes() {
runThree(AdvancedFoundationsScenarios.levelTwentyThree(), 1, 1);
}
@Test public void frozenStepUsesOneFrozenBodyThreeTimes() {
runThree(AdvancedFoundationsScenarios.levelTwentyFour(), 1, 1);
}
@Test public void livingWireUsesOneConductiveBodyThreeTimes() {
runThree(AdvancedFoundationsScenarios.levelTwentyFive(), 1, 1);
}
@Test public void secondHandCompletesOnTheOriginalTimerThreeTimes() {
runThree(AdvancedFoundationsScenarios.levelTwentySixIntended(), 0, 0);
}
@Test public void secondHandRecoversAfterTheFarGateClosesThreeTimes() {
runThree(AdvancedFoundationsScenarios.levelTwentySixRecovery(), 0, 0);
}
private static void runThree(LevelPlaytestBot.Scenario scenario,
int expectedDeaths, int expectedCorpses) {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
Driver driver = Driver.from(activity);
for (int run = 1; run <= 3; run++) {
LevelPlaytestBot.Result result = new LevelPlaytestBot().run(driver, scenario, 45_000L);
assertTrue("run=" + run + "; " + result.diagnostic(), result.completed());
assertEquals("run=" + run, expectedDeaths, result.deaths());
assertEquals("run=" + run, expectedCorpses, result.corpseCount());
assertTrue("run=" + run + "; collectible", driver.snapshot().collectibleFound());
}
}
}
private static final class Driver implements LevelPlaytestBot.Driver {
private final Instrumentation instrumentation;
private final GameView view;
private Driver(Instrumentation instrumentation, GameView view) {
this.instrumentation = instrumentation;
this.view = view;
}
static Driver from(ActivityScenario<MainActivity> activity) {
GameView[] target = new GameView[1];
activity.onActivity(current -> target[0] = current.gameViewForAutomation());
return new Driver(InstrumentationRegistry.getInstrumentation(), target[0]);
}
@Override public void startLevel(int oneBasedLevelNumber) {
instrumentation.runOnMainSync(() -> {
view.enableDeterministicAutomationClock();
view.startLevelForAutomation(oneBasedLevelNumber);
});
}
@Override public void setInput(boolean left, boolean right, boolean jump) {
instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump));
}
@Override public GameView.AutomationSnapshot snapshot() {
GameView.AutomationSnapshot[] value = new GameView.AutomationSnapshot[1];
instrumentation.runOnMainSync(() -> value[0] = view.automationSnapshot());
return value[0];
}
@Override public void waitForNextDecision(long millis) {
instrumentation.runOnMainSync(() ->
view.advanceDeterministicAutomation(Math.max(16L, millis)));
}
}
}
@@ -53,6 +53,10 @@ public final class LevelPaginationTest {
assertEquals(2, view.levelPageIndexForAutomation()); assertEquals(2, view.levelPageIndexForAutomation());
assertTrue(visible.contains("Held Open")); assertTrue(visible.contains("Held Open"));
assertTrue(visible.contains("Clockwork Step")); assertTrue(visible.contains("Clockwork Step"));
assertTrue(visible.contains("Remote Volunteer"));
assertTrue(visible.contains("Frozen Step"));
assertTrue(visible.contains("Living Wire"));
assertTrue(visible.contains("Second Hand"));
assertFalse(visible.contains("Covered Path")); assertFalse(visible.contains("Covered Path"));
assertTrue(visible.contains("Page 3 of 3")); assertTrue(visible.contains("Page 3 of 3"));
}); });
@@ -0,0 +1,172 @@
package com.projectsacrifice.game;
/** Production-input routes for Foundations levels 23-26. */
public final class AdvancedFoundationsScenarios {
private AdvancedFoundationsScenarios() {}
public static LevelPlaytestBot.Scenario levelTwentyThree() {
return LevelPlaytestBot.Scenario.builder(23, "Level 23 - remote volunteer")
.step("Launch one volunteer onto the remote plate", 4_000L,
LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1))
.step("Observe the delivered body open the door", 2_500L,
LevelPlaytestBot.Input.none(),
atMost(1, GameView.AutomationSnapshot::allDoorsOpen))
.step("Wait for the crossing volunteer", 2_000L,
LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned))
.step("Stage before the launch saw", 2_500L,
LevelPlaytestBot.Input.right(),
atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(420f)))
.step("Take off before the launch saw", 1_500L,
LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
.step("Clear the launch saw", 2_000L, LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 740f))
.step("Approach the delivered body", 1_500L, LevelPlaytestBot.Input.right(),
atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(780f)))
.step("Jump over the body and collect the cache", 3_000L,
LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.collectibleFound()))
.step("Pass the remote gate", 3_000L, LevelPlaytestBot.Input.right(),
atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_650f)))
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
atMost(1, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwentyFour() {
return LevelPlaytestBot.Scenario.builder(24, "Level 24 - frozen step")
.step("Freeze one volunteer", 4_000L, LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1))
.step("Wait for the climbing volunteer", 2_000L,
LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned))
.step("Approach the frozen block", 2_500L, LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.playerX() >= 420f))
.step("Jump toward the frozen block", 1_500L,
LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerX() >= 670f))
.step("Settle on the frozen block", 2_000L, LevelPlaytestBot.Input.none(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerY() >= 668f && snapshot.playerY() <= 690f))
.step("Jump to the upper route", 3_000L, LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerX() >= 850f && snapshot.playerY() <= 500f))
.step("Cross the ledge and collect the cache", 3_000L,
LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.collectibleFound() && snapshot.playerX() >= 1_300f))
.step("Jump to the final ledge", 2_500L, LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 1_480f && snapshot.playerY() <= 520f))
.step("Reach the upper relic", 4_000L, LevelPlaytestBot.Input.right(),
atMost(1, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwentyFive() {
return LevelPlaytestBot.Scenario.builder(25, "Level 25 - living wire")
.step("Power the relay with one volunteer", 4_000L,
LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.deaths() == 1
&& snapshot.corpseCount() == 1))
.step("Observe the conductive body open the door", 2_000L,
LevelPlaytestBot.Input.none(),
atMost(1, GameView.AutomationSnapshot::allDoorsOpen))
.step("Wait for the crossing volunteer", 2_000L,
LevelPlaytestBot.Input.none(), atMost(1, AdvancedFoundationsScenarios::respawned))
.step("Stage before the live relay", 2_500L, LevelPlaytestBot.Input.right(),
atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
.step("Take off before the live relay", 1_500L,
LevelPlaytestBot.Input.rightJump(),
atMost(1, snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && snapshot.playerY() <= 680f))
.step("Clear the conductive body and collect the cache", 3_000L,
LevelPlaytestBot.Input.right(),
atMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.playerX() >= 1_080f && snapshot.collectibleFound()))
.step("Pass the powered door", 3_000L, LevelPlaytestBot.Input.right(),
atMost(1, LevelPlaytestBot.groundedAliveAtOrBeyond(1_470f)))
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
atMost(1, GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwentySixIntended() {
return timedRoute(false);
}
public static LevelPlaytestBot.Scenario levelTwentySixRecovery() {
return timedRoute(true);
}
private static LevelPlaytestBot.Scenario timedRoute(boolean recovery) {
LevelPlaytestBot.Scenario.Builder route = LevelPlaytestBot.Scenario.builder(26,
recovery ? "Level 26 - far-side recovery" : "Level 26 - original timer");
route.step("Start the second-hand timer", 3_000L, LevelPlaytestBot.Input.right(),
noDeaths(snapshot -> snapshot.allDoorsOpen()
&& snapshot.firstMovingPlatformPowered()))
.step("Approach the timed ferry", 2_000L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(800f)))
.step("Wait for the outbound boarding phase", 3_500L,
LevelPlaytestBot.Input.none(),
noDeaths(snapshot -> MovingPlatformLaunchPolicy.readyToBoard(snapshot, 1_000f)))
.step("Board the timed ferry", 2_500L, LevelPlaytestBot.Input.rightJump(),
noDeaths(snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerOnMovingPlatform()))
.step("Ride to the far launch phase", 4_000L, LevelPlaytestBot.Input.none(),
noDeaths(snapshot -> MovingPlatformLaunchPolicy.ready(snapshot, 1_250f)))
.step("Jump to the far floor", 3_000L, LevelPlaytestBot.Input.rightJump(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_600f)));
if (recovery) {
route.step("Move beneath the high recovery relay", 2_000L,
LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_650f)))
.step("Wait for the far gate to close", 10_000L,
LevelPlaytestBot.Input.none(),
noDeaths(snapshot -> !snapshot.allDoorsOpen()))
.step("Jump into the recovery relay", 2_500L,
LevelPlaytestBot.Input.rightJump(),
noDeaths(GameView.AutomationSnapshot::allDoorsOpen));
} else {
route.step("Pass the door on the original timer", 3_000L,
LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(2_030f)));
}
return route.step("Stage before the upper route", 2_000L,
LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(2_050f)))
.step("Jump onto the upper route", 3_000L,
LevelPlaytestBot.Input.rightJump(),
noDeaths(snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerX() >= 2_250f
&& snapshot.playerY() <= 610f))
.step("Collect the upper cache", 2_000L,
LevelPlaytestBot.Input.right(),
noDeaths(snapshot -> snapshot.playerAlive() && snapshot.collectibleFound()))
.step("Reach the upper relic", 4_000L, LevelPlaytestBot.Input.right(),
noDeaths(GameView.AutomationSnapshot::completed))
.build();
}
private static LevelPlaytestBot.SnapshotCondition atMost(
int deaths, LevelPlaytestBot.SnapshotCondition condition) {
return LevelPlaytestBot.whileDeathsAtMost(deaths, condition);
}
private static LevelPlaytestBot.SnapshotCondition noDeaths(
LevelPlaytestBot.SnapshotCondition condition) {
return atMost(0, condition);
}
private static boolean respawned(GameView.AutomationSnapshot snapshot) {
return snapshot.playerAlive() && snapshot.playerX() < 250f;
}
}
@@ -0,0 +1,58 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class FoundationsAdvancedContentTest {
@Test
public void secondFoundationsWaveMatchesMatrixAndPreservesRecoveryContracts() {
List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(26, levels.size());
LevelDefinition remote = levels.get(22);
assertIdentity(remote, 23, "foundations.remote-volunteer", "Remote Volunteer", 1, 50);
assertTrue(remote.containsMechanic(LevelDefinition.HazardType.SAW));
assertTrue(remote.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
assertFalse(remote.doors.isEmpty());
LevelDefinition frozen = levels.get(23);
assertIdentity(frozen, 24, "foundations.frozen-step", "Frozen Step", 1, 48);
assertTrue(frozen.containsMechanic(LevelDefinition.HazardType.FREEZER));
assertTrue(frozen.goal.y < 700f);
LevelDefinition live = levels.get(24);
assertIdentity(live, 25, "foundations.living-wire", "Living Wire", 1, 52);
assertTrue(live.containsMechanic(LevelDefinition.HazardType.ELECTRIC));
assertFalse(live.doors.isEmpty());
LevelDefinition timed = levels.get(25);
assertIdentity(timed, 26, "foundations.borrowed-time", "Second Hand", 0, 55);
assertTrue(timed.containsMechanic(LevelDefinition.SwitchType.TIMED));
assertTrue(timed.containsMechanic(LevelDefinition.HazardType.PIT));
assertTrue(timed.hasMovingPlatform());
assertFalse(timed.doors.isEmpty());
assertEquals(2, timed.switches.size());
Set<String> signals = new HashSet<>();
for (LevelDefinition.Switch item : timed.switches) signals.add(item.signalId);
assertEquals(Set.of(timed.platforms.get(3).controlId), signals);
}
private static void assertIdentity(LevelDefinition level, int number, String id, String title,
int targetDeaths, int targetSeconds) {
assertEquals(number, level.number);
assertEquals(id, level.levelId);
assertEquals("foundations", level.worldId);
assertEquals(title, level.title);
assertEquals(targetDeaths, level.targetDeaths);
assertEquals(targetSeconds, level.targetSeconds);
assertNotNull(level.collectible);
}
}
@@ -13,7 +13,7 @@ public final class FoundationsContentTest {
@Test @Test
public void firstFoundationsWaveMatchesTheApprovedMatrixAndTeachesOneIdeaAtATime() { public void firstFoundationsWaveMatchesTheApprovedMatrixAndTeachesOneIdeaAtATime() {
List<LevelDefinition> levels = LevelCatalog.all(); List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(22, levels.size()); assertTrue(levels.size() >= 22);
LevelDefinition firstReturn = levels.get(18); LevelDefinition firstReturn = levels.get(18);
assertIdentity(firstReturn, 19, "foundations.first-return", "First Return", 0, 28); assertIdentity(firstReturn, 19, "foundations.first-return", "First Return", 0, 28);
@@ -15,7 +15,7 @@ public final class LevelCatalogTest {
@Test @Test
public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() { public void productionCatalogContainsMechanicsAndChallengeTeachingLevels() {
List<LevelDefinition> levels = LevelCatalog.all(); List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(22, levels.size()); assertEquals(26, levels.size());
Set<String> titles = new HashSet<>(); Set<String> titles = new HashSet<>();
for (int i = 0; i < levels.size(); i++) { for (int i = 0; i < levels.size(); i++) {
LevelDefinition level = levels.get(i); LevelDefinition level = levels.get(i);
+8 -1
View File
@@ -217,7 +217,14 @@ Android matrix `52/52` (`551.773 s`). Evidence — `STAGE6_CHALLENGES.md`.
spikes, retained pressure Door и toggle ferry имеют solution graphs, collectibles, spikes, retained pressure Door и toggle ferry имеют solution graphs, collectibles,
restart proof и 12/12 production-input playthroughs. Cold unfiltered candidate restart proof и 12/12 production-input playthroughs. Cold unfiltered candidate
прошёл Android `57/57` за `1807.264 s`, app JVM `117/117`, content-tool JVM `7/7`. прошёл Android `57/57` за `1807.264 s`, app JVM `117/117`, content-tool JVM `7/7`.
Evidence — `STAGE6_FOUNDATIONS_WAVE1.md`; следующий slice — Levels 23–26. Evidence — `STAGE6_FOUNDATIONS_WAVE1.md`.
Второй bounded wave Levels 23–26 также принят: remote saw delivery, frozen step,
conductive relay и timed ferry с дальним recovery relay имеют solution graphs,
collectibles и 15/15 последовательных production-input playthroughs. Cold
unfiltered candidate прошёл Android `62/62` за `1063.114 s`, app JVM `118/118`,
content-tool JVM `7/7`; APK содержит все 26 level JSON. Evidence —
`STAGE6_FOUNDATIONS_WAVE2.md`; следующий slice — Levels 27–30, включая Foundations
challenge и finale.
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней ### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
+72
View File
@@ -535,6 +535,78 @@ LevelID="foundations.clockwork-step", WorldID="foundations"
160 px launch overlap до движения и 20 px far overlap в конечной фазе; bot ждёт 160 px launch overlap до движения и 20 px far overlap в конечной фазе; bot ждёт
реальную поддержку, а не единичное пересечение в воздухе. реальную поддержку, а не единичное пересечение в воздухе.
## Уровень 23 — `Remote Volunteer`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; доставленное тело удерживает Door.
```text
LevelID="foundations.remote-volunteer", WorldID="foundations"
→ первый доброволец входит в saw x=600..696 и получает impulse (550,-700)
→ тело попадает на raised pressure plate x=1030..1200 и открывает remote-gate
→ следующий доброволец стартует прыжок до saw и остаётся жив
→ перепрыгивает доставленное тело, забирает cache x=1180
→ проходит Door x=1540..1622 и достигает goal с deaths=1, corpses=1
```
**Контракт:** дистанционно доставленное тело является единственным источником
pressure-сигнала. Маршрут не маскирует повторную смерть и ограничен максимум одной
жертвой на каждом шаге.
## Уровень 24 — `Frozen Step`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; одно замороженное тело становится ступенью.
```text
LevelID="foundations.frozen-step", WorldID="foundations"
→ первый доброволец замерзает в FREEZER x=720..820
→ следующий прыгает и стабилизируется на frozen body на y≈680
→ отдельным jump edge поднимается на ledge x=850..1410, y=590
→ забирает cache x=1280 и перепрыгивает на ledge x=1500..2200
→ достигает верхнего goal с deaths=1, corpses=1
```
**Контракт:** без сохранённого frozen body высота первого уступа недостижима
доказанным маршрутом; бот отдельно наблюдает устойчивую опору на теле.
## Уровень 25 — `Living Wire`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; проводящее тело постоянно питает Door.
```text
LevelID="foundations.living-wire", WorldID="foundations"
→ первый доброволец погибает в ELECTRIC x=720..830
→ conductive corpse активирует relayId="living-power" и открывает Door
→ следующий стартует прыжок до live relay
→ перелетает тело, забирает cache x=1100 на безопасном полу
→ проходит Door x=1350..1434 и достигает goal с deaths=1, corpses=1
```
**Контракт:** Door открывается только живым relay-сигналом сохранённого conductive
body; каждый шаг fail-fast запрещает вторую смерть.
## Уровень 26 — `Second Hand`
**Статус:** доказаны два production-input маршрута, каждый 3/3.
**Ожидаемые и доказанные смерти:** `0` для original timer и recovery.
```text
LevelID="foundations.borrowed-time", WorldID="foundations"
→ TIMED switch x=760 запускает second-hand на 9 s и outbound ferry
→ игрок ждёт boarding phase, садится на platform x=940..1300
→ едет до platformX≥1250 и прыгает на far floor x=1580..2700
→ intended: проходит Door x=1920 на исходном таймере
→ recovery: ждёт закрытия Door и прыгает в высокий relay x=1700,y=650 на 4 s
→ оба маршрута поднимаются на ledge x=2230..2700, забирают cache x=2350
→ достигают верхнего goal без смерти
```
**Контракт:** recovery relay использует тот же `second-hand`, но расположен над
обычным беговым маршрутом. Отдельный тест сначала наблюдает реально закрытую Door,
затем её повторное открытие — softlock после истечения исходного таймера исключён.
## Проверки, следующие из графов ## Проверки, следующие из графов
1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8. 1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8.
+15 -14
View File
@@ -49,7 +49,7 @@ finale не вводят новую механику: они проверяют
Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение: Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение:
```text ```text
Validated production content matrix: worlds=4; planned=52; authored=5; releaseTotal=69 Validated production content matrix: worlds=4; planned=52; authored=9; releaseTotal=69
``` ```
## Content pipeline и Level DoD ## Content pipeline и Level DoD
@@ -64,29 +64,30 @@ JSON-уровень добавляется только ведущим преф
5. restart, blocker-softlock и collectible/challenge проверки; 5. restart, blocker-softlock и collectible/challenge проверки;
6. camera, sound, art, optimization и QA regression gates из Level DoD. 6. camera, sound, art, optimization и QA regression gates из Level DoD.
Следующий authoring wave — Foundations 19–30. Уровни добавляются небольшими Foundations 19–26 уже авторизованы через этот pipeline. Следующий bounded slice —
вертикальными срезами; незавершённый уровень не скрывается в общем счётчике Levels 27–30, включая challenge и finale мира; незавершённый уровень не
production-контента. скрывается в общем счётчике production-контента.
## TDD и gate первого среза ## TDD и текущий gate
RED сначала зафиксировал отсутствующий `ProductionContentMatrix` (15 compile RED сначала зафиксировал отсутствующий `ProductionContentMatrix` (15 compile
errors), затем отсутствующую CLI-команду `validate-matrix` (exit `2`). После GREEN errors), затем отсутствующую CLI-команду `validate-matrix` (exit `2`). После GREEN
добавлен отрицательный regression: изменение planned title Level 18 отклоняется добавлен отрицательный regression: изменение planned title Level 18 отклоняется
как drift реального JSON-префикса. как drift реального JSON-префикса.
Текущий gate после первого Foundations wave на 19 августа 2026 года: Текущий gate после второго Foundations wave на 19 августа 2026 года:
- `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`; - `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`;
- app JVM regression: `117/117`; - app JVM regression: `118/118`;
- `validateLevels`: `22` authored levels; - `validateLevels`: `26` authored levels;
- `validateContentMatrix`: `4` worlds, `52` planned, `5` authored production rows, - `validateContentMatrix`: `4` worlds, `52` planned, `9` authored production rows,
release total `69`; release total `69`;
- `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN; - `lintDebug`, `assembleDebug`, `assembleDebugAndroidTest`: GREEN;
- APK ZIP integrity: no errors; matrix и `level-18.json` присутствуют в package; - APK ZIP integrity: no errors; `26` level JSON присутствуют в package;
- cold unfiltered Android: `57/57`, `1807.264 s`; - cold unfiltered Android: `62/62`, failures/errors `0/0`, `1063.114 s`;
- debug APK SHA-256: - debug APK SHA-256:
`81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`. `67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`.
Матрица не подменяет device evidence: Levels 19–22 прошли отдельные routes, Матрица не подменяет device evidence: Levels 19–26 прошли отдельные routes и
restart DoD и полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md`. полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md` и
`STAGE6_FOUNDATIONS_WAVE2.md`.
+58
View File
@@ -0,0 +1,58 @@
# Stage 6: Foundations wave 2
## Принятый результат
Второй bounded authoring wave расширяет каталог с 22 до 26 уровней:
- 23 `Remote Volunteer` — saw доставляет одно тело на поднятую pressure plate;
- 24 `Frozen Step` — одно frozen body становится обязательной ступенью;
- 25 `Living Wire` — одно conductive body постоянно питает relay и Door;
- 26 `Second Hand` — timed ferry над PIT и высокий far-side recovery relay.
Имя Level 26 изменено с дублирующего `Borrowed Time` на уникальное `Second Hand`;
stable ID `foundations.borrowed-time` сохранён, поэтому identity и будущие saves
не зависят от редактируемого display title.
Все уровни имеют RU/EN copy, stable LevelID/WorldID, casualty/time targets,
collectible и solution graph. Пять Android methods выполняют по три
последовательных production-input route: по одному для Levels 23–25 и два для
Level 26 — исходный таймер и восстановление после его истечения.
## TDD и найденные ошибки
RED сначала зафиксировал каталог `22 != 26`, затем отсутствие
`AdvancedFoundationsScenarios`. Device-прогоны обнаружили конкретные ошибки
контента и choreography:
1. Level 23 останавливался перед raised platform (`x=838`); подход к обязательному
прыжку перенесён раньше без изменения saw/physics.
2. Level 25 проходил ниже слишком высокого cache; cache перенесён на доказанный
безопасный ground route.
3. Оба маршрута Level 26 падали в PIT около `x=1128`; TIMED switch перенесён к
launch zone, а ferry получила достаточный boarding overlap и state-driven
launch policy.
4. После пересечения PIT Level 26 проходил под cache (`x=2638`); финальный путь
разделён на наблюдаемую посадку на верхний ledge и отдельный сбор cache, а
ledge сдвинут так, чтобы требовать этот подъём.
Финальный focused suite прошёл `5/5` methods — `15` route executions за
`10.768 s`. Recovery route трижды ждёт `allDoorsOpen=false` на дальней стороне,
активирует высокий relay и только затем проходит вновь открытую Door. Retry,
teleport и прямая мутация completion отсутствуют.
## Level DoD evidence
- JSON validator: `26` levels, format/physics version `1`;
- content matrix: `4` worlds, `52` planned, `9` authored production rows,
release total `69`;
- focused routes: `5/5` methods, `15` playthroughs, `10.768 s`;
- app JVM: `118/118`; level-format JVM: `7/7`;
- clean `lintDebug`, app APK и AndroidTest APK: GREEN;
- final cold unfiltered Android 16: `62/62`, failures/errors `0/0`,
instrumentation time `1063.114 s` (`BUILD SUCCESSFUL` in `17m55s`);
- APK ZIP: no errors; `26` level JSON packaged;
- debug APK SHA-256:
`67510d404cfa0704500cbf36c7845e9ba9fa05e96630079ab02bc35113e946fa`.
Wave не меняет глобальную physics/runtime модель. Следующий bounded slice —
Foundations 27–30, включая challenge и finale этого мира.
@@ -7,7 +7,7 @@ number worldId levelId role title introducedMechanic mechanics targetDeaths targ
23 foundations foundations.remote-volunteer MAIN Remote Volunteer saw-launch movement,jump,death,saw-launch,pressure,door 1 50 true 23 foundations foundations.remote-volunteer MAIN Remote Volunteer saw-launch movement,jump,death,saw-launch,pressure,door 1 50 true
24 foundations foundations.frozen-step MAIN Frozen Step frozen-body movement,jump,death,frozen-body 1 48 true 24 foundations foundations.frozen-step MAIN Frozen Step frozen-body movement,jump,death,frozen-body 1 48 true
25 foundations foundations.living-wire MAIN Living Wire conductive-body movement,jump,death,conductive-body,door 1 52 true 25 foundations foundations.living-wire MAIN Living Wire conductive-body movement,jump,death,conductive-body,door 1 52 true
26 foundations foundations.borrowed-time MAIN Borrowed Time timed-switch movement,jump,timed-switch,moving-platform,pit 0 55 true 26 foundations foundations.borrowed-time MAIN Second Hand timed-switch movement,jump,timed-switch,moving-platform,pit 0 55 true
27 foundations foundations.two-keys MAIN Two Keys signal-and movement,jump,death,pressure,conductive-body,signal-and,door 2 65 true 27 foundations foundations.two-keys MAIN Two Keys signal-and movement,jump,death,pressure,conductive-body,signal-and,door 2 65 true
28 foundations foundations.clean-restart MAIN Clean Restart - movement,jump,death,saw-launch,frozen-body,conductive-body 3 75 true 28 foundations foundations.clean-restart MAIN Clean Restart - movement,jump,death,saw-launch,frozen-body,conductive-body 3 75 true
29 foundations foundations.minimum-cost CHALLENGE Minimum Cost - movement,jump,death,corpse-bridge,pressure,timed-switch 2 65 true 29 foundations foundations.minimum-cost CHALLENGE Minimum Cost - movement,jump,death,corpse-bridge,pressure,timed-switch 2 65 true
1 number worldId levelId role title introducedMechanic mechanics targetDeaths targetSeconds collectible
7 23 foundations foundations.remote-volunteer MAIN Remote Volunteer saw-launch movement,jump,death,saw-launch,pressure,door 1 50 true
8 24 foundations foundations.frozen-step MAIN Frozen Step frozen-body movement,jump,death,frozen-body 1 48 true
9 25 foundations foundations.living-wire MAIN Living Wire conductive-body movement,jump,death,conductive-body,door 1 52 true
10 26 foundations foundations.borrowed-time MAIN Borrowed Time Second Hand timed-switch movement,jump,timed-switch,moving-platform,pit 0 55 true
11 27 foundations foundations.two-keys MAIN Two Keys signal-and movement,jump,death,pressure,conductive-body,signal-and,door 2 65 true
12 28 foundations foundations.clean-restart MAIN Clean Restart - movement,jump,death,saw-launch,frozen-body,conductive-body 3 75 true
13 29 foundations foundations.minimum-cost CHALLENGE Minimum Cost - movement,jump,death,corpse-bridge,pressure,timed-switch 2 65 true
@@ -1 +1 @@
{"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json","level-15.json","level-16.json","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json"]} {"formatVersion":1,"physicsVersion":1,"levels":["level-01.json","level-02.json","level-03.json","level-04.json","level-05.json","level-06.json","level-07.json","level-08.json","level-09.json","level-10.json","level-11.json","level-12.json","level-13.json","level-14.json","level-15.json","level-16.json","level-17.json","level-18.json","level-19.json","level-20.json","level-21.json","level-22.json","level-23.json","level-24.json","level-25.json","level-26.json"]}
@@ -0,0 +1,16 @@
{
"formatVersion":1,"physicsVersion":1,"number":23,
"id":"foundations.remote-volunteer","worldId":"foundations",
"copy":{"en":{"title":"Remote Volunteer","hint":"Let the saw deliver one volunteer to the raised plate, then clear the retained body."},"ru":{"title":"Удалённый доброволец","hint":"Пусть пила доставит добровольца на поднятую плиту, затем перепрыгните оставшееся тело."}},
"world":{"width":2450,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2220,"y":770,"width":86,"height":110},
"collectible":{"x":1180,"y":650,"width":58,"height":58},
"targets":{"deaths":1,"seconds":50},
"platforms":[
{"box":{"x":0,"y":880,"width":2450,"height":200}},
{"box":{"x":900,"y":800,"width":400,"height":34}}
],
"hazards":[{"type":"SAW","box":{"x":600,"y":760,"width":96,"height":96},"impulseX":550,"impulseY":-700}],
"switches":[{"type":"PRESSURE","box":{"x":1030,"y":780,"width":170,"height":20},"signalId":"remote-gate","durationSeconds":0,"requiredMass":1}],
"doors":[{"box":{"x":1540,"y":560,"width":82,"height":320},"requiredSignals":["remote-gate"]}]
}
@@ -0,0 +1,16 @@
{
"formatVersion":1,"physicsVersion":1,"number":24,
"id":"foundations.frozen-step","worldId":"foundations",
"copy":{"en":{"title":"Frozen Step","hint":"Freeze one volunteer, stand on the block, and jump into the upper route."},"ru":{"title":"Замёрзшая ступень","hint":"Заморозьте добровольца, встаньте на блок и прыгните на верхний маршрут."}},
"world":{"width":2350,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2110,"y":500,"width":86,"height":110},
"collectible":{"x":1280,"y":500,"width":58,"height":58},
"targets":{"deaths":1,"seconds":48},
"platforms":[
{"box":{"x":0,"y":880,"width":1260,"height":200}},
{"box":{"x":850,"y":590,"width":560,"height":34}},
{"box":{"x":1500,"y":610,"width":700,"height":34}}
],
"hazards":[{"type":"FREEZER","box":{"x":720,"y":776,"width":100,"height":104}}],
"switches":[],"doors":[]
}
@@ -0,0 +1,13 @@
{
"formatVersion":1,"physicsVersion":1,"number":25,
"id":"foundations.living-wire","worldId":"foundations",
"copy":{"en":{"title":"Living Wire","hint":"One conductive body keeps the relay alive. Jump over it to use the open route."},"ru":{"title":"Живой проводник","hint":"Одно проводящее тело удерживает реле. Перепрыгните его и используйте открытый маршрут."}},
"world":{"width":2350,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2130,"y":770,"width":86,"height":110},
"collectible":{"x":1100,"y":790,"width":58,"height":58},
"targets":{"deaths":1,"seconds":52},
"platforms":[{"box":{"x":0,"y":880,"width":2350,"height":200}}],
"hazards":[{"type":"ELECTRIC","box":{"x":720,"y":780,"width":110,"height":100},"relayId":"living-power"}],
"switches":[],
"doors":[{"box":{"x":1350,"y":560,"width":84,"height":320},"requiredSignals":["living-power"]}]
}
@@ -0,0 +1,21 @@
{
"formatVersion":1,"physicsVersion":1,"number":26,
"id":"foundations.borrowed-time","worldId":"foundations",
"copy":{"en":{"title":"Second Hand","hint":"Start the clock, ride across, and use the high far relay if the gate closes."},"ru":{"title":"Секундная стрелка","hint":"Запустите часы, переедьте пропасть и используйте высокое дальнее реле, если ворота закроются."}},
"world":{"width":2700,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2470,"y":600,"width":86,"height":110},
"collectible":{"x":2350,"y":580,"width":58,"height":58},
"targets":{"deaths":0,"seconds":55},
"platforms":[
{"box":{"x":0,"y":880,"width":1100,"height":200}},
{"box":{"x":1580,"y":880,"width":1120,"height":200}},
{"box":{"x":2230,"y":710,"width":470,"height":34}},
{"box":{"x":940,"y":820,"width":360,"height":34},"travelX":380,"travelY":0,"periodSeconds":4.4,"controlId":"second-hand"}
],
"hazards":[{"type":"PIT","box":{"x":1100,"y":880,"width":480,"height":200}}],
"switches":[
{"type":"TIMED","box":{"x":760,"y":810,"width":90,"height":70},"signalId":"second-hand","durationSeconds":9.0,"requiredMass":1},
{"type":"TIMED","box":{"x":1700,"y":650,"width":90,"height":70},"signalId":"second-hand","durationSeconds":4.0,"requiredMass":1}
],
"doors":[{"box":{"x":1920,"y":560,"width":82,"height":320},"requiredSignals":["second-hand"]}]
}
@@ -85,7 +85,7 @@ public final class LevelContentToolTest {
assertEquals(0, exitCode); assertEquals(0, exitCode);
assertEquals("Validated production content matrix: worlds=4; planned=52; " assertEquals("Validated production content matrix: worlds=4; planned=52; "
+ "authored=5; releaseTotal=69\n", + "authored=9; releaseTotal=69\n",
output.toString(StandardCharsets.UTF_8)); output.toString(StandardCharsets.UTF_8));
assertEquals("", errors.toString(StandardCharsets.UTF_8)); assertEquals("", errors.toString(StandardCharsets.UTF_8));
} }