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

This commit is contained in:
2026-08-19 10:51:25 +03:00
parent 4869649b47
commit b31c2ea720
18 changed files with 592 additions and 19 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: claimed after challenge/progression commit 3de8d45 passed 116 app JVM + 2 content-tool JVM and cold host-GPU exact Android 52/52. Executable production matrix now fixes release at 69 levels: workshop 1-17 plus Foundations 18-30, Physics 31-43, States 44-56, Mastery 57-69; each world has 11 MAIN + 1 CHALLENGE + 1 FINALE. TDD REDs captured missing matrix contract and missing validate-matrix CLI; GREEN has 7/7 level-format + 116/116 app JVM, both validators, lintDebug, app/test APK and ZIP integrity. Android preBuild fail-closes matrix rules and authored-prefix drift; authored production prefix is Level 18 only. Next wave is Foundations 19-30 through graph/JSON/bot/DoD; issue remains in_progress.","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-19T06:39:18Z","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.","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-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
@@ -125,6 +125,10 @@ Burned/crushed corpse states are taught in levels 15–16, level 17 adds five
authored physical-object types over one shared pressure-mass contract, and level authored physical-object types over one shared pressure-mass contract, and level
18 teaches the casualty/time/cache challenge contract with persistent three-star 18 teaches the casualty/time/cache challenge contract with persistent three-star
progress keyed by stable `levelId`. progress keyed by stable `levelId`.
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
toggle-controlled horizontal ferry. All four routes pass three consecutive runs,
and restart clears the stateful bridge/ferry setups.
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
@@ -133,10 +137,11 @@ contract is in `docs/STAGE6_CORPSE_STATES.md`. Weighted-object evidence is in
`docs/STAGE6_CHALLENGES.md`. The executable content matrix fixes the release at `docs/STAGE6_CHALLENGES.md`. The executable content matrix fixes the release at
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`. The latest exact gate is `116 app JVM + 2 in `docs/STAGE6_CONTENT_MATRIX.md`; wave evidence is in
content-tool JVM / 52 Android` on the eighteen-level candidate (`OK (52 tests)`, `docs/STAGE6_FOUNDATIONS_WAVE1.md`. The latest exact gate is `117 app JVM + 7
551.773 seconds). The verified debug APK SHA-256 is content-tool JVM / 57 Android` on the twenty-two-level candidate (`57/57`,
`0b70a0a1a508425c311d7de473318e3bb83407f5f27e9dd4a8e1876b2eb25344`. 1807.264 seconds). The verified debug APK SHA-256 is
`81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`.
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,84 @@
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 FoundationsPlaythroughTest {
@Test public void firstReturnCompletesDeathlessThreeTimes() {
runThree(FoundationsScenarios.levelNineteen(), 0, 0);
}
@Test public void coveredPathUsesExactlyTwoBodiesThreeTimes() {
runThree(FoundationsScenarios.levelTwenty(), 2, 2);
}
@Test public void heldOpenUsesExactlyOnePlateBodyThreeTimes() {
runThree(FoundationsScenarios.levelTwentyOne(), 1, 1);
}
@Test public void clockworkStepCompletesDeathlessThreeTimes() {
runThree(FoundationsScenarios.levelTwentyTwo(), 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, 30_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)));
}
}
}
@@ -0,0 +1,96 @@
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 FoundationsRestartTest {
@Test
public void restartClearsCoveredPathBodiesAndClockworkFerryState() {
try (ActivityScenario<MainActivity> activity = ActivityScenario.launch(MainActivity.class)) {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
GameView[] target = new GameView[1];
activity.onActivity(current -> target[0] = current.gameViewForAutomation());
GameView view = target[0];
start(instrumentation, view, 20);
input(instrumentation, view, false, true, false);
GameView.AutomationSnapshot bridge = advanceUntil(instrumentation, view,
snapshot -> snapshot.deaths() == 1 && snapshot.corpseCount() == 1);
assertEquals(1, bridge.deaths());
instrumentation.runOnMainSync(() -> {
view.setAutomationInput(false, true, true);
view.restartLevelForAutomation();
});
assertCleanRestart(snapshot(instrumentation, view), 20);
start(instrumentation, view, 22);
input(instrumentation, view, false, true, false);
GameView.AutomationSnapshot ferry = advanceUntil(instrumentation, view,
GameView.AutomationSnapshot::firstMovingPlatformPowered);
assertTrue(ferry.firstMovingPlatformPowered());
instrumentation.runOnMainSync(() -> {
view.setAutomationInput(false, true, true);
view.restartLevelForAutomation();
});
GameView.AutomationSnapshot restartedFerry = snapshot(instrumentation, view);
assertCleanRestart(restartedFerry, 22);
assertFalse(restartedFerry.firstMovingPlatformPowered());
assertEquals(940f, restartedFerry.firstMovingPlatformX(), 0.5f);
}
}
private static void assertCleanRestart(GameView.AutomationSnapshot snapshot, int level) {
assertEquals(level, snapshot.levelNumber());
assertTrue(snapshot.playerAlive());
assertEquals(0, snapshot.deaths());
assertEquals(0, snapshot.corpseCount());
assertFalse(snapshot.collectibleFound());
assertFalse(snapshot.inputActive());
assertTrue(snapshot.elapsedMilliseconds() < 100L);
assertTrue(snapshot.playerX() >= 125f && snapshot.playerX() <= 135f);
}
private static void start(Instrumentation instrumentation, GameView view, int level) {
instrumentation.runOnMainSync(() -> {
view.enableDeterministicAutomationClock();
view.startLevelForAutomation(level);
});
}
private static void input(Instrumentation instrumentation, GameView view,
boolean left, boolean right, boolean jump) {
instrumentation.runOnMainSync(() -> view.setAutomationInput(left, right, jump));
}
private static GameView.AutomationSnapshot advanceUntil(
Instrumentation instrumentation, GameView view,
LevelPlaytestBot.SnapshotCondition condition) {
GameView.AutomationSnapshot current = snapshot(instrumentation, view);
for (int tick = 0; tick < 100; tick++) {
if (condition.reached(current)) return current;
instrumentation.runOnMainSync(() -> view.advanceDeterministicAutomation(96L));
current = snapshot(instrumentation, view);
}
throw new AssertionError("Foundations 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];
}
}
@@ -24,7 +24,7 @@ public final class LevelPaginationTest {
GameView view = current.gameViewForAutomation(); GameView view = current.gameViewForAutomation();
view.setLanguageForAutomation(GameLanguage.EN); view.setLanguageForAutomation(GameLanguage.EN);
assertEquals(0, view.levelPageIndexForAutomation()); assertEquals(0, view.levelPageIndexForAutomation());
assertEquals(2, view.levelPageCountForAutomation()); assertEquals(3, view.levelPageCountForAutomation());
assertTrue(view.visibleLocalizedTextForAutomation().contains("The Volunteer")); assertTrue(view.visibleLocalizedTextForAutomation().contains("The Volunteer"));
assertFalse(view.visibleLocalizedTextForAutomation().contains("Steam Charge")); assertFalse(view.visibleLocalizedTextForAutomation().contains("Steam Charge"));
}); });
@@ -39,10 +39,27 @@ public final class LevelPaginationTest {
assertTrue(visible.contains("Ashen Counterweight")); assertTrue(visible.contains("Ashen Counterweight"));
assertTrue(visible.contains("The Whole Weight")); assertTrue(visible.contains("The Whole Weight"));
assertTrue(visible.contains("Threefold Proof")); assertTrue(visible.contains("Threefold Proof"));
assertTrue(visible.contains("First Return"));
assertTrue(visible.contains("Covered Path"));
assertFalse(visible.contains("Held Open"));
assertFalse(visible.contains("The Volunteer")); assertFalse(visible.contains("The Volunteer"));
assertTrue(visible.contains("Page 2 of 2")); assertTrue(visible.contains("Page 2 of 3"));
}); });
tap(activity, 1_730f, 965f);
activity.onActivity(current -> {
GameView view = current.gameViewForAutomation();
List<String> visible = view.visibleLocalizedTextForAutomation();
assertEquals(2, view.levelPageIndexForAutomation());
assertTrue(visible.contains("Held Open"));
assertTrue(visible.contains("Clockwork Step"));
assertFalse(visible.contains("Covered Path"));
assertTrue(visible.contains("Page 3 of 3"));
});
tap(activity, 190f, 965f);
activity.onActivity(current ->
assertEquals(1, current.gameViewForAutomation().levelPageIndexForAutomation()));
tap(activity, 190f, 965f); tap(activity, 190f, 965f);
activity.onActivity(current -> activity.onActivity(current ->
assertEquals(0, current.gameViewForAutomation().levelPageIndexForAutomation())); assertEquals(0, current.gameViewForAutomation().levelPageIndexForAutomation()));
@@ -0,0 +1,119 @@
package com.projectsacrifice.game;
/** Production-input intended routes for the first authored Foundations wave. */
public final class FoundationsScenarios {
private FoundationsScenarios() {}
public static LevelPlaytestBot.Scenario levelNineteen() {
return LevelPlaytestBot.Scenario.builder(19, "Level 19 - first return")
.step("Approach the lower ledge", 2_500L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(500f)))
.step("Clear the lower ledge", 2_500L, LevelPlaytestBot.Input.rightJump(),
noDeaths(snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 920f))
.step("Release before the high ledge", 1_500L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(980f)))
.step("Jump onto the high ledge", 2_500L, LevelPlaytestBot.Input.rightJump(),
noDeaths(snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 1_400f))
.step("Collect the cache after both jumps", 1_500L, LevelPlaytestBot.Input.right(),
noDeaths(snapshot -> snapshot.playerAlive() && snapshot.collectibleFound()))
.step("Land beyond the high ledge", 2_500L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_450f)))
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
noDeaths(GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwenty() {
return LevelPlaytestBot.Scenario.builder(20, "Level 20 - covered path")
.step("Place the near bridge body", 3_500L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2, snapshot -> snapshot.deaths() == 1))
.step("Wait for the second volunteer", 1_500L, LevelPlaytestBot.Input.none(),
LevelPlaytestBot.whileDeathsAtMost(2, FoundationsScenarios::respawned))
.step("Approach the first bridge body", 2_500L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2,
LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
.step("Place the far bridge body", 3_000L, LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(2, snapshot -> snapshot.deaths() == 2))
.step("Wait for the crossing volunteer", 1_500L, LevelPlaytestBot.Input.none(),
LevelPlaytestBot.whileDeathsAtMost(2, FoundationsScenarios::respawned))
.step("Return to the covered path", 2_500L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2,
LevelPlaytestBot.groundedAliveAtOrBeyond(520f)))
.step("Cross both retained bodies", 3_500L, LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(2,
LevelPlaytestBot.groundedAliveAtOrBeyond(900f)))
.step("Approach the cache ledge", 2_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2,
LevelPlaytestBot.groundedAliveAtOrBeyond(980f)))
.step("Collect the cache", 2_500L, LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(2,
snapshot -> snapshot.playerAlive() && snapshot.collectibleFound()))
.step("Reach the relic", 4_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(2,
GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwentyOne() {
return LevelPlaytestBot.Scenario.builder(21, "Level 21 - held open")
.step("Leave one volunteer on the plate", 4_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1, snapshot -> snapshot.deaths() == 1))
.step("Prove the body holds the door", 2_000L, LevelPlaytestBot.Input.none(),
LevelPlaytestBot.whileDeathsAtMost(1,
GameView.AutomationSnapshot::allDoorsOpen))
.step("Wait for the next volunteer", 1_500L, LevelPlaytestBot.Input.none(),
LevelPlaytestBot.whileDeathsAtMost(1, FoundationsScenarios::respawned))
.step("Stage before the saw", 2_500L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
LevelPlaytestBot.groundedAliveAtOrBeyond(470f)))
.step("Take off before the saw", 1_500L, LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1, snapshot -> snapshot.playerAlive()
&& !snapshot.playerOnGround() && snapshot.playerY() <= 620f))
.step("Clear the saw and retained body", 2_500L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
snapshot -> snapshot.playerAlive() && snapshot.playerX() >= 820f))
.step("Stage before the open door", 2_000L, LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
LevelPlaytestBot.groundedAliveAtOrBeyond(1_020f)))
.step("Jump through the cache", 3_000L, LevelPlaytestBot.Input.rightJump(),
LevelPlaytestBot.whileDeathsAtMost(1, snapshot -> snapshot.playerAlive()
&& snapshot.collectibleFound() && snapshot.allDoorsOpen()))
.step("Reach the relic through the held door", 4_000L,
LevelPlaytestBot.Input.right(),
LevelPlaytestBot.whileDeathsAtMost(1,
GameView.AutomationSnapshot::completed))
.build();
}
public static LevelPlaytestBot.Scenario levelTwentyTwo() {
return LevelPlaytestBot.Scenario.builder(22, "Level 22 - clockwork step")
.step("Set the ferry toggle", 2_500L, LevelPlaytestBot.Input.right(),
noDeaths(GameView.AutomationSnapshot::firstMovingPlatformPowered))
.step("Approach the ferry", 2_500L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(850f)))
.step("Board the moving ferry", 2_000L, LevelPlaytestBot.Input.rightJump(),
noDeaths(snapshot -> snapshot.playerAlive()
&& snapshot.playerOnGround()
&& snapshot.playerOnMovingPlatform()))
.step("Ride to the far overlap and collect the cache", 5_500L,
LevelPlaytestBot.Input.none(),
noDeaths(snapshot -> snapshot.playerAlive()
&& snapshot.playerOnMovingPlatform()
&& snapshot.firstMovingPlatformX() >= 1_280f
&& snapshot.collectibleFound()))
.step("Step onto the far floor", 2_500L, LevelPlaytestBot.Input.right(),
noDeaths(LevelPlaytestBot.groundedAliveAtOrBeyond(1_650f)))
.step("Reach the relic", 3_000L, LevelPlaytestBot.Input.right(),
noDeaths(GameView.AutomationSnapshot::completed))
.build();
}
private static LevelPlaytestBot.SnapshotCondition noDeaths(
LevelPlaytestBot.SnapshotCondition condition) {
return LevelPlaytestBot.whileDeathsAtMost(0, condition);
}
private static boolean respawned(GameView.AutomationSnapshot snapshot) {
return snapshot.playerAlive() && snapshot.playerX() < 250f;
}
}
@@ -0,0 +1,56 @@
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.List;
public final class FoundationsContentTest {
@Test
public void firstFoundationsWaveMatchesTheApprovedMatrixAndTeachesOneIdeaAtATime() {
List<LevelDefinition> levels = LevelCatalog.all();
assertEquals(22, levels.size());
LevelDefinition firstReturn = levels.get(18);
assertIdentity(firstReturn, 19, "foundations.first-return", "First Return", 0, 28);
assertTrue(firstReturn.hazards.isEmpty());
assertTrue(firstReturn.switches.isEmpty());
assertTrue(firstReturn.doors.isEmpty());
LevelDefinition coveredPath = levels.get(19);
assertIdentity(coveredPath, 20, "foundations.covered-path", "Covered Path", 2, 42);
assertTrue(coveredPath.containsMechanic(LevelDefinition.HazardType.SPIKES));
assertTrue(coveredPath.switches.isEmpty());
LevelDefinition heldOpen = levels.get(20);
assertIdentity(heldOpen, 21, "foundations.held-open", "Held Open", 1, 45);
assertTrue(heldOpen.containsMechanic(LevelDefinition.HazardType.SAW));
assertTrue(heldOpen.containsMechanic(LevelDefinition.SwitchType.PRESSURE));
assertFalse(heldOpen.doors.isEmpty());
assertEquals(heldOpen.switches.get(0).signalId,
heldOpen.doors.get(0).requiredSignals.get(0));
LevelDefinition clockwork = levels.get(21);
assertIdentity(clockwork, 22, "foundations.clockwork-step", "Clockwork Step", 0, 48);
assertTrue(clockwork.containsMechanic(LevelDefinition.SwitchType.TOGGLE));
assertTrue(clockwork.containsMechanic(LevelDefinition.HazardType.PIT));
assertTrue(clockwork.hasMovingPlatform());
assertFalse(clockwork.platforms.get(2).controlId.isEmpty());
assertEquals(clockwork.switches.get(0).signalId, clockwork.platforms.get(2).controlId);
}
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);
}
}
@@ -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(18, levels.size()); assertEquals(22, 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);
+5
View File
@@ -213,6 +213,11 @@ Android matrix `52/52` (`551.773 s`). Evidence — `STAGE6_CHALLENGES.md`.
Каждый мир содержит 11 MAIN, один CHALLENGE и один FINALE; Gradle `preBuild` Каждый мир содержит 11 MAIN, один CHALLENGE и один FINALE; Gradle `preBuild`
проверяет матрицу и соответствие авторизованного JSON-префикса. Контракт — проверяет матрицу и соответствие авторизованного JSON-префикса. Контракт —
`STAGE6_CONTENT_MATRIX.md`; следующий authoring wave — Foundations 19–30. `STAGE6_CONTENT_MATRIX.md`; следующий authoring wave — Foundations 19–30.
Первый bounded wave Levels 19–22 принят: movement/jump, два тела над раздельными
spikes, retained pressure Door и toggle ferry имеют solution graphs, collectibles,
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`.
Evidence — `STAGE6_FOUNDATIONS_WAVE1.md`; следующий slice — Levels 23–26.
### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней ### Этап 7. Техническая стабилизация и release engineering — P0, 4–8 дней
+73
View File
@@ -462,6 +462,79 @@ time runs. Legacy levels без collectible получают третью зве
`bestStars=3`; новый `ProgressStore` после трёх запусков возвращает 3 звезды для `bestStars=3`; новый `ProgressStore` после трёх запусков возвращает 3 звезды для
`foundations.threefold-proof`. `foundations.threefold-proof`.
## Уровень 19 — `First Return`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `0`.
```text
LevelID="foundations.first-return", WorldID="foundations"
→ игрок подходит к нижнему уступу x=650..890 и перепрыгивает его
→ отпускает jump, подтверждает новое касание пола и создаёт второй jump edge
→ вторым прыжком проходит высокий уступ x=1080..1380
→ на безопасной дальней прямой забирает collectible x=1500
→ достигает goal x=2090..2176 без смертей
```
**Контракт:** уровень повторно калибрует движение и раздельные jump edges перед
production-комбинациями. На нём нет hazard, switch или Door; failure означает
ошибку управления, а не скрытое состояние головоломки.
## Уровень 20 — `Covered Path`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `2`; оба тела сохраняются.
```text
LevelID="foundations.covered-path", WorldID="foundations"
→ первый доброволец идёт в ближние spikes x=700..800 и оставляет pinned body
→ второй прыгает с x≈520 через первое тело и погибает на spikes x=930..1030
→ третий возвращается и одним прыжком пересекает две раздельные опоры
→ отдельным jump edge входит на ledge x=1240..1520 и забирает collectible
→ достигает goal с deaths=2, corpses=2
```
**Контракт:** две раздельные spike-зоны делают обе жертвы наблюдаемыми и не
позволяют одним ранним телом случайно закрыть весь hazard. Bot fail-fast отклоняет
третью смерть; три последовательных запуска доказывают очистку старых тел.
## Уровень 21 — `Held Open`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `1`; Door остаётся открыта телом.
```text
LevelID="foundations.held-open", WorldID="foundations"
→ первый доброволец достигает saw x=650..742
→ тело оседает на pressure plate x=625..785 и активирует held-gate
→ следующий доброволец подтверждает allDoorsOpen=true
→ начинает прыжок из рассчитанного окна x≈470 и очищает saw + retained body
→ прыгает через collectible x=1160 до Door x=1260
→ проходит удерживаемую Door и достигает goal с deaths=1, corpses=1
```
**Контракт:** plate и Door используют один signal ID `held-gate`. Коллекционный
маршрут лежит внутри того же доказанного прыжка и не требует второй жертвы.
## Уровень 22 — `Clockwork Step`
**Статус:** доказан production-input ботом, 3/3 последовательных прохождения.
**Ожидаемые и доказанные смерти:** `0`.
```text
LevelID="foundations.clockwork-step", WorldID="foundations"
→ игрок один раз касается toggle ferry x=760..832
→ подходит к launch overlap и прыгает на platform x=940..1260
→ snapshot подтверждает playerOnGround && playerOnMovingPlatform
→ без input едет до platformX≥1280 и по пути забирает collectible x=1435
→ сходит на дальний floor x=1600..2500, не касаясь PIT x=1100..1600
→ достигает goal без смерти
```
**Контракт:** toggle и moving platform разделяют signal `ferry`. Платформа имеет
160 px launch overlap до движения и 20 px far overlap в конечной фазе; bot ждёт
реальную поддержку, а не единичное пересечение в воздухе.
## Проверки, следующие из графов ## Проверки, следующие из графов
1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8. 1. Сохранять полный integrated device-regression: intended route принятых уровней по три раза и отдельный recovery route уровня 8.
+9 -9
View File
@@ -49,7 +49,7 @@ finale не вводят новую механику: они проверяют
Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение: Android `preBuild` зависит от этого gate. Текущее ожидаемое сообщение:
```text ```text
Validated production content matrix: worlds=4; planned=52; authored=1; releaseTotal=69 Validated production content matrix: worlds=4; planned=52; authored=5; releaseTotal=69
``` ```
## Content pipeline и Level DoD ## Content pipeline и Level DoD
@@ -75,18 +75,18 @@ errors), затем отсутствующую CLI-команду `validate-matr
добавлен отрицательный regression: изменение planned title Level 18 отклоняется добавлен отрицательный regression: изменение planned title Level 18 отклоняется
как drift реального JSON-префикса. как drift реального JSON-префикса.
Чистый gate на 19 августа 2026 года: Текущий gate после первого Foundations wave на 19 августа 2026 года:
- `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`; - `ProductionContentMatrixTest` + `LevelContentToolTest`: `7/7`;
- app JVM regression: `116/116`; - app JVM regression: `117/117`;
- `validateLevels`: `18` authored levels; - `validateLevels`: `22` authored levels;
- `validateContentMatrix`: `4` worlds, `52` planned, `1` authored production row, - `validateContentMatrix`: `4` worlds, `52` planned, `5` 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; matrix и `level-18.json` присутствуют в package;
- cold unfiltered Android: `57/57`, `1807.264 s`;
- debug APK SHA-256: - debug APK SHA-256:
`73d9647f7d5456c79e6fcfa828e0524ccdd5cdf4777a3998c51016c38ce2652c`. `81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`.
Этот срез не меняет runtime, physics или geometry, поэтому device playthrough Матрица не подменяет device evidence: Levels 19–22 прошли отдельные routes,
матрица остаётся acceptance gate следующего authoring wave, а не подменяется restart DoD и полный unfiltered gate; детали — `STAGE6_FOUNDATIONS_WAVE1.md`.
build-time validator тестами.
+56
View File
@@ -0,0 +1,56 @@
# Stage 6: Foundations wave 1
## Принятый результат
Первый authoring wave расширяет каталог с 18 до 22 уровней и реализует четыре
последовательные строки executable content matrix:
- 19 `First Return` — два раздельных jump edge, `0` смертей;
- 20 `Covered Path` — две spike-зоны, ровно `2` тела-моста;
- 21 `Held Open` — одно тело удерживает pressure Door;
- 22 `Clockwork Step` — toggle включает горизонтальную ferry platform над PIT.
Каждый уровень имеет RU/EN copy, stable LevelID/WorldID, casualty/time targets,
collectible, solution graph и state-driven production-input route. Четыре route
methods выполняют по три последовательных запуска без teleport, прямой мутации
completion или retry masking.
## TDD и найденные ошибки
RED контракты сначала зафиксировали каталог `18 != 22` и отсутствующие четыре
`FoundationsScenarios`. Первый device run затем дал четыре независимых RED:
1. Level 19 прошёл мимо слишком высокого cache (`x=1949.1761`, deaths `0`).
Cache перенесён на безопасную дальнюю прямую после двух обязательных прыжков.
2. Level 20 одним прыжком пропускал требуемое второе тело (`deaths=1`,
firstCorpseX `641.8331`). Один широкий strip разделён на две spike-зоны.
3. Level 21 повторно касался дальнего края saw (`x=734.3325`, deaths `2`).
Takeoff зафиксирован в рассчитанном окне `x≈470`; physics/saw не менялись.
4. Level 22 терял ferry до стабильной посадки (`x=1215.3331`, deaths `1`).
Toggle перенесён к launch zone, ferry получила достаточный стартовый overlap,
а bot подтверждает `playerOnGround && playerOnMovingPlatform`.
После GREEN отдельный restart test доказал, что user restart Level 20 удаляет
тело/смерть/held input, а Level 22 сбрасывает toggle, platform motion,
collectible и input. Первый unfiltered run нашёл один acceptance drift: каталог
теперь имеет три страницы, а тест ожидал две. UI-контракт обновлён на страницы
1–3 и повторный cold run прошёл полностью.
## Level DoD evidence
- JSON validator: `22` levels, format/physics version `1`;
- content matrix: `4` worlds, `52` planned, `5` authored production rows,
release total `69`;
- focused routes: `4/4` methods, `12` playthroughs, `8.372 s`;
- focused restart: `1/1`;
- app JVM: `117/117`; level-format JVM: `7/7`;
- clean `lintDebug`, app APK и AndroidTest APK: GREEN;
- final cold unfiltered Android 16: `57/57`, failures/errors `0/0`,
instrumentation time `1807.264 s` (`BUILD SUCCESSFUL` in `30m21s`);
- APK ZIP: no errors; `22` level JSON packaged;
- debug APK SHA-256:
`81e50c2b3247a2bf7fad2a2efc461c1d6f19d63e1b10a958e8c1a8544adf48ef`.
Процедурные visual/audio cues и camera bounds используют принятый общий Canvas
pipeline; wave не меняет physics/runtime systems. Следующий bounded authoring
slice — Foundations 23–26.
@@ -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"]} {"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"]}
@@ -0,0 +1,14 @@
{
"formatVersion":1,"physicsVersion":1,"number":19,
"id":"foundations.first-return","worldId":"foundations",
"copy":{"en":{"title":"First Return","hint":"Read the two ledges, release between jumps, and keep moving toward the relic."},"ru":{"title":"Первое возвращение","hint":"Оцените два уступа, отпускайте прыжок между ними и двигайтесь к реликвии."}},
"world":{"width":2300,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2090,"y":770,"width":86,"height":110},
"collectible":{"x":1500,"y":790,"width":58,"height":58},
"targets":{"deaths":0,"seconds":28},
"platforms":[
{"box":{"x":0,"y":880,"width":2300,"height":200}},
{"box":{"x":650,"y":760,"width":240,"height":34}},
{"box":{"x":1080,"y":690,"width":300,"height":34}}
],"hazards":[],"switches":[],"doors":[]
}
@@ -0,0 +1,18 @@
{
"formatVersion":1,"physicsVersion":1,"number":20,
"id":"foundations.covered-path","worldId":"foundations",
"copy":{"en":{"title":"Covered Path","hint":"Two volunteers can cover the spikes. Place them apart, then cross what remains."},"ru":{"title":"Накрытый путь","hint":"Два добровольца могут накрыть шипы. Разместите их раздельно и перейдите по оставшимся телам."}},
"world":{"width":2300,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2080,"y":770,"width":86,"height":110},
"collectible":{"x":1300,"y":665,"width":58,"height":58},
"targets":{"deaths":2,"seconds":42},
"platforms":[
{"box":{"x":0,"y":880,"width":2300,"height":200}},
{"box":{"x":1240,"y":740,"width":280,"height":34}}
],
"hazards":[
{"type":"SPIKES","box":{"x":700,"y":850,"width":100,"height":30}},
{"type":"SPIKES","box":{"x":930,"y":850,"width":100,"height":30}}
],
"switches":[],"doors":[]
}
@@ -0,0 +1,13 @@
{
"formatVersion":1,"physicsVersion":1,"number":21,
"id":"foundations.held-open","worldId":"foundations",
"copy":{"en":{"title":"Held Open","hint":"Let the saw leave one volunteer on the plate; the next can clear both body and door."},"ru":{"title":"Удержать открытым","hint":"Пусть пила оставит добровольца на плите; следующий перепрыгнет тело и открытую дверь."}},
"world":{"width":2350,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2140,"y":770,"width":86,"height":110},
"collectible":{"x":1160,"y":650,"width":58,"height":58},
"targets":{"deaths":1,"seconds":45},
"platforms":[{"box":{"x":0,"y":880,"width":2350,"height":200}}],
"hazards":[{"type":"SAW","box":{"x":650,"y":760,"width":92,"height":92},"impulseX":0,"impulseY":-150}],
"switches":[{"type":"PRESSURE","box":{"x":625,"y":860,"width":160,"height":20},"signalId":"held-gate","durationSeconds":0,"requiredMass":1}],
"doors":[{"box":{"x":1260,"y":570,"width":78,"height":310},"requiredSignals":["held-gate"]}]
}
@@ -0,0 +1,17 @@
{
"formatVersion":1,"physicsVersion":1,"number":22,
"id":"foundations.clockwork-step","worldId":"foundations",
"copy":{"en":{"title":"Clockwork Step","hint":"Set the brass toggle, board the ferry, and wait for its far-side overlap."},"ru":{"title":"Заводная ступень","hint":"Включите латунный рычаг, встаньте на паром и дождитесь стыковки с дальним берегом."}},
"world":{"width":2500,"height":1080},"spawn":{"x":130,"y":760},
"goal":{"x":2260,"y":770,"width":86,"height":110},
"collectible":{"x":1435,"y":650,"width":58,"height":58},
"targets":{"deaths":0,"seconds":48},
"platforms":[
{"box":{"x":0,"y":880,"width":1100,"height":200}},
{"box":{"x":1600,"y":880,"width":900,"height":200}},
{"box":{"x":940,"y":770,"width":320,"height":34},"travelX":360,"travelY":0,"periodSeconds":5.0,"controlId":"ferry"}
],
"hazards":[{"type":"PIT","box":{"x":1100,"y":880,"width":500,"height":200}}],
"switches":[{"type":"TOGGLE","box":{"x":760,"y":820,"width":72,"height":60},"signalId":"ferry","durationSeconds":0,"requiredMass":1}],
"doors":[]
}
@@ -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=1; releaseTotal=69\n", + "authored=5; 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));
} }