BUG-GATE-PASS-1 закрыт: смоук уровня 1 пройден, запись переехала в bug_closed.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 12:41:51 +03:00
parent 9234c03020
commit 6960e1cc76
2 changed files with 166 additions and 164 deletions
+165 -1
View File
@@ -1739,7 +1739,7 @@ Kid ставится на первый пол. Номер комнаты — п
Вторая волна того же прогона (вечер) дала ещё четыре наблюдения — BUG-KBD-4,
BUG-RESPAWN-2, BUG-DRAWORDER-1, BUG-LOOSE-2, — все закрыты, разборы выше.
Тогда же закрыт как невоспроизводящийся BUG-SEAM-DRAW-1 и заведён
BUG-GATE-PASS-1, который остаётся открытым (ждёт сценария).
BUG-GATE-PASS-1 (он закрыт ниже, 2026-08-09).
---
@@ -1807,3 +1807,167 @@ pop_guard_hurt = 0;`), а не безусловно — это и экономи
**Цвет брызг** зависит от палитры стража (слоты `0x90..0x9F`), поэтому он
изменится вместе с [BUG-GUARD-COLOR-1](bug_list.md#bug-guard-color-1) —
отдельной работы не требует.
---
<a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — **ЗАКРЫТ 2026-08-09**
**Приёмка:** смоук-прогон уровня 1 (пользователь, 2026-08-09) — с проблемного
`x` Кид больше сквозь решётку не пробегает, пинг-понг у шва не вернулся.
> **ВОСПРОИЗВЕДЁН И РАЗОБРАН.** Сценарий: уровень 1, комната 8, ряд 0,
> `Kid.x = 170`, лицом ВПРАВО, решётка шва (комната 8, колонка 9) ЗАКРЫТА.
> Разбежаться вправо и НЕ ОТПУСКАТЬ клавишу. Кид проходит сквозь решётку и
> убегает вглубь комнаты 6.
>
> Ниже — новый разбор; всё, что было раньше (гипотеза про x > 205), не
> подтвердилось и оставлено в конце для истории.
### Корень: история флагов коллизии индексируется НЕ ТАК, как в оригинале
Сняты две трассы — наша (брейкпоинт на резидентном `kid_tick`) и оригинала
(SDLPoP, собран с `POP_TRACE=1`, печать в `play_kid_frame` + маркеры в
`bumped` и `exit_room`). Стартовая позиция совпала до пикселя, поэтому
сравнение построчное.
**Оригинал:**
```
LEAVE dir=1 x=61 cxl=184 cxr=201 room=6 смена комнаты — ШТАТНАЯ
BUMPED dx=-2 push=-1 x=63 tile=4 col=9 и СРАЗУ бамп о решётку, откат
KID f=15 x=60 act=0 col=-1 room=6 drawn=6 сел за решёткой, стоит
```
**Мы:**
```
x 170 -> 196 бегом, col 7 -> 8 -> 9, НИ ОДНОГО бампа
x=198 -> room 8 -> 6
при удержании ВПРАВО доезжает до col 1 комнаты 6 и бежит дальше
```
Смена комнаты в этой точке правильна и у нас, и у оригинала: `leave_room`
(seg002:423) вправо блокируют ТОЛЬКО `doortop`-тайлы, решётки в списке нет, а
порог `char_x_right >= 201` совпадает с плоскостью ворот
(`x_bump[9+FIRST_ONSCREEN_COLUMN] + TILE_MIDX + wall_dist_from_left[1]`
= 191 + 10). Оригинал полагается не на запрет ухода, а на бамп СРАЗУ ПОСЛЕ
перехода.
**Почему бамп есть у них и нет у нас.** `check_collisions` (seg004:0004)
хранит флаги так:
```c
row_coll_flags_ptr[tile_col] = curr_flags; // индекс — колонка В РАЗРЕШЁННОЙ комнате
row_coll_room_ptr [tile_col] = curr_room; // и рядом — НОМЕР ЭТОЙ КОМНАТЫ
...
if (curr_row_coll_room[column] >= 0 &&
prev_coll_room[column] == curr_row_coll_room[column]) { /* только тогда сравниваем */ }
```
То есть история привязана к паре **(колонка внутри своей комнаты, комната)**.
Решётка комнаты 8 и до перехода, и после лежит в слоте 9 с `room = 8` —
переход 8->6 историю НЕ рвёт, переход флага 0->1 виден, бамп срабатывает.
У нас массив `coll_prev/coll_curr` индексируется колонкой ОТНОСИТЕЛЬНО
отрисованной комнаты (−2…11). При смене комнаты все индексы уезжают на 10,
сопоставить прошлый кадр с текущим нечем, и `enter_room` вынужден звать
`pop_coll_invalidate()`; тот ставит «прошлые флаги = уже перекрывал» (3), то
есть ПОДАВЛЯЕТ бамп на кадре входа. А к следующему кадру Кид уже за
плоскостью ворот — перехода 0->1 не будет никогда.
### Фикс сделан 2026-08-09 (вариант B — сдвиг истории), ЖДЁТ ПРИЁМКИ В MAME
Рассматривались два пути:
- **A, дословный порт.** 10 слотов + параллельный массив номера комнаты,
индекс по колонке разрешённой комнаты, бамп только при совпадении номеров;
`pop_coll_invalidate()` тогда не нужен вовсе. **Не взят:** оригиналу
хватает десяти слотов только потому, что он перебирает узкое окно вокруг
Кида, а мы намеренно считаем все четырнадцать колонок (FIX_COLL_FLAGS,
чтобы не оставалось протухших ячеек) — при таком переборе колонки −2/−1 и
8/9 сядут в одни слоты 8/9. То есть A тянет за собой ещё и сужение окна.
- **B, сдвиг истории (сделано).** Индексацию оставляем свою, но при
БОКОВОМ переходе историю не выбрасываем, а перенумеровываем на 10 слотов:
`check_leave` двигает `x` ровно на ∓140 = 10 тайлов, координата грани
(`pop_x_bump`) едет на те же 140 вместе с габаритом Кида, значит сами
флаги инвариантны — меняется только номер слота.
Реализация: `pop_coll_shift(int8_t dcol)` в `pop_map.c` сдвигает
`coll_curr/above/below` (не `prev` — его на следующем кадре всё равно
перезапишет `move_coll_to_prev`), освободившиеся слоты = 3 («уже
перекрывал», бампа нет — ровно то, что в оригинале даёт несовпадение номера
комнаты). `enter_room_side` зовёт её сразу после `pop_map_set_edges`:
`side == 0` (ушёл влево) → `+10`, `side == 1` (вправо) → `10`. Переходы
вверх/вниз и все прочие входы в комнату остаются на полной инвалидации —
там колонки не сдвигаются, но тайлы под ними уже из другой комнаты.
Расхождение с дословным вариантом A записано в `../docs/impl_diff.md` (D-1).
**Осторожно:** это сердце коллизии, вокруг которого разбирался
BUG-SEAM-PINGPONG (см. `bug_closed.md`). Приёмка:
1. Комнаты 6 ↔ 8 уровня 1, закрытая решётка, **обе** стороны.
2. Мелким шагом (упереться) И с разбега (не пройти насквозь) — сценарий
выше воспроизводится с `Kid.x = 170`, комната 8, ряд 0, лицом вправо.
3. Пинг-понг у шва не вернулся (экран не перескакивает туда-сюда).
4. `make -C tests-host` — прошли все 5 наборов (2026-08-09).
**Инструмент для сверки готов:** SDLPoP собран с трассой, включается
`POP_TRACE=1` (правки в `seg000.c` play_kid_frame, `seg004.c` bumped,
`seg002.c` exit_room, чтение переменной — `main.c`). Сборка на macOS:
`make CFLAGS="-std=gnu99 -O2 -I/opt/homebrew/include -Wno-implicit-function-declaration" LIBS="-L/opt/homebrew/lib -lSDL2 -lSDL2_image"`
(в системе нет pkg-config, штатный Makefile без этого не собирается).
---
### История: прежний разбор (гипотеза НЕ подтвердилась)
**Статус: наблюдался один раз (2026-08-03), воспроизвести не удалось ни
тогда, ни прогоном всех комнат уровня 1 (2026-08-07).** Заведён, чтобы
наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
«не баг», пока нет надёжного сценария. Оговорка: BUG-GATEMOD-1, из-за
которого решётка стартовала не в том состоянии, с тех пор закрыт, и
наблюдение могло быть его следствием.
**Что наблюдалось.** Комната 5: Кид стоял НА тайле решётки (0,9)
и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
и упал на (1,1).
**Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
ДЕРЖИТ — то есть штатно.
Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7` —
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
**Что снять в следующий раз** (без этих чисел вопрос не закрыть):
1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
стоит на её тайле — до шага вправо;
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
есть пока она опустилась достаточно низко относительно РОСТА кадра;
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME; подгонка позы по пикселю —
читы `[`/`]`.
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
+1 -163
View File
@@ -18,11 +18,11 @@
| ID | что | тип | статус |
|----|-----|-----|--------|
| [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт |
| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрытую решётку шва (комн. 8 -> 6) | Major | **фикс сделан 2026-08-09** (сдвиг истории флагов, вариант B) — ждёт приёмки в MAME |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводимый: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводимый: на повторе не поднялся; на пререлиз |
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| ~~T-2~~ | Кид перерисовывается в покое | оптимизация | **ЗАКРЫТ 2026-08-08** — [DRAW-COST шаг 1](TASKS_OPEN.md#draw-cost) |
| ~~BUG-GATE-PASS-1~~ | проход сквозь закрытую решётку шва | Major | **ЗАКРЫТ 2026-08-09** — [bug_closed.md](bug_closed.md#bug-gate-pass-1), смоук уровня 1 пройден |
---
@@ -190,168 +190,6 @@ Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал
---
# Открытые баги уровня 1
<a id="bug-gate-pass-1"></a>
## BUG-GATE-PASS-1. Проход сквозь закрывшуюся решётку — **КОРЕНЬ НАЙДЕН 2026-08-08**
> **ВОСПРОИЗВЕДЁН И РАЗОБРАН.** Сценарий: уровень 1, комната 8, ряд 0,
> `Kid.x = 170`, лицом ВПРАВО, решётка шва (комната 8, колонка 9) ЗАКРЫТА.
> Разбежаться вправо и НЕ ОТПУСКАТЬ клавишу. Кид проходит сквозь решётку и
> убегает вглубь комнаты 6.
>
> Ниже — новый разбор; всё, что было раньше (гипотеза про x > 205), не
> подтвердилось и оставлено в конце для истории.
### Корень: история флагов коллизии индексируется НЕ ТАК, как в оригинале
Сняты две трассы — наша (брейкпоинт на резидентном `kid_tick`) и оригинала
(SDLPoP, собран с `POP_TRACE=1`, печать в `play_kid_frame` + маркеры в
`bumped` и `exit_room`). Стартовая позиция совпала до пикселя, поэтому
сравнение построчное.
**Оригинал:**
```
LEAVE dir=1 x=61 cxl=184 cxr=201 room=6 смена комнаты — ШТАТНАЯ
BUMPED dx=-2 push=-1 x=63 tile=4 col=9 и СРАЗУ бамп о решётку, откат
KID f=15 x=60 act=0 col=-1 room=6 drawn=6 сел за решёткой, стоит
```
**Мы:**
```
x 170 -> 196 бегом, col 7 -> 8 -> 9, НИ ОДНОГО бампа
x=198 -> room 8 -> 6
при удержании ВПРАВО доезжает до col 1 комнаты 6 и бежит дальше
```
Смена комнаты в этой точке правильна и у нас, и у оригинала: `leave_room`
(seg002:423) вправо блокируют ТОЛЬКО `doortop`-тайлы, решётки в списке нет, а
порог `char_x_right >= 201` совпадает с плоскостью ворот
(`x_bump[9+FIRST_ONSCREEN_COLUMN] + TILE_MIDX + wall_dist_from_left[1]`
= 191 + 10). Оригинал полагается не на запрет ухода, а на бамп СРАЗУ ПОСЛЕ
перехода.
**Почему бамп есть у них и нет у нас.** `check_collisions` (seg004:0004)
хранит флаги так:
```c
row_coll_flags_ptr[tile_col] = curr_flags; // индекс — колонка В РАЗРЕШЁННОЙ комнате
row_coll_room_ptr [tile_col] = curr_room; // и рядом — НОМЕР ЭТОЙ КОМНАТЫ
...
if (curr_row_coll_room[column] >= 0 &&
prev_coll_room[column] == curr_row_coll_room[column]) { /* только тогда сравниваем */ }
```
То есть история привязана к паре **(колонка внутри своей комнаты, комната)**.
Решётка комнаты 8 и до перехода, и после лежит в слоте 9 с `room = 8`
переход 8->6 историю НЕ рвёт, переход флага 0->1 виден, бамп срабатывает.
У нас массив `coll_prev/coll_curr` индексируется колонкой ОТНОСИТЕЛЬНО
отрисованной комнаты (−2…11). При смене комнаты все индексы уезжают на 10,
сопоставить прошлый кадр с текущим нечем, и `enter_room` вынужден звать
`pop_coll_invalidate()`; тот ставит «прошлые флаги = уже перекрывал» (3), то
есть ПОДАВЛЯЕТ бамп на кадре входа. А к следующему кадру Кид уже за
плоскостью ворот — перехода 0->1 не будет никогда.
### Фикс сделан 2026-08-09 (вариант B — сдвиг истории), ЖДЁТ ПРИЁМКИ В MAME
Рассматривались два пути:
- **A, дословный порт.** 10 слотов + параллельный массив номера комнаты,
индекс по колонке разрешённой комнаты, бамп только при совпадении номеров;
`pop_coll_invalidate()` тогда не нужен вовсе. **Не взят:** оригиналу
хватает десяти слотов только потому, что он перебирает узкое окно вокруг
Кида, а мы намеренно считаем все четырнадцать колонок (FIX_COLL_FLAGS,
чтобы не оставалось протухших ячеек) — при таком переборе колонки −2/−1 и
8/9 сядут в одни слоты 8/9. То есть A тянет за собой ещё и сужение окна.
- **B, сдвиг истории (сделано).** Индексацию оставляем свою, но при
БОКОВОМ переходе историю не выбрасываем, а перенумеровываем на 10 слотов:
`check_leave` двигает `x` ровно на ∓140 = 10 тайлов, координата грани
(`pop_x_bump`) едет на те же 140 вместе с габаритом Кида, значит сами
флаги инвариантны — меняется только номер слота.
Реализация: `pop_coll_shift(int8_t dcol)` в `pop_map.c` сдвигает
`coll_curr/above/below` (не `prev` — его на следующем кадре всё равно
перезапишет `move_coll_to_prev`), освободившиеся слоты = 3 («уже
перекрывал», бампа нет — ровно то, что в оригинале даёт несовпадение номера
комнаты). `enter_room_side` зовёт её сразу после `pop_map_set_edges`:
`side == 0` (ушёл влево) → `+10`, `side == 1` (вправо) → `10`. Переходы
вверх/вниз и все прочие входы в комнату остаются на полной инвалидации —
там колонки не сдвигаются, но тайлы под ними уже из другой комнаты.
Расхождение с дословным вариантом A записано в `../docs/impl_diff.md` (D-1).
**Осторожно:** это сердце коллизии, вокруг которого разбирался
BUG-SEAM-PINGPONG (см. `bug_closed.md`). Приёмка:
1. Комнаты 6 ↔ 8 уровня 1, закрытая решётка, **обе** стороны.
2. Мелким шагом (упереться) И с разбега (не пройти насквозь) — сценарий
выше воспроизводится с `Kid.x = 170`, комната 8, ряд 0, лицом вправо.
3. Пинг-понг у шва не вернулся (экран не перескакивает туда-сюда).
4. `make -C tests-host` — прошли все 5 наборов (2026-08-09).
**Инструмент для сверки готов:** SDLPoP собран с трассой, включается
`POP_TRACE=1` (правки в `seg000.c` play_kid_frame, `seg004.c` bumped,
`seg002.c` exit_room, чтение переменной — `main.c`). Сборка на macOS:
`make CFLAGS="-std=gnu99 -O2 -I/opt/homebrew/include -Wno-implicit-function-declaration" LIBS="-L/opt/homebrew/lib -lSDL2 -lSDL2_image"`
(в системе нет pkg-config, штатный Makefile без этого не собирается).
---
### История: прежний разбор (гипотеза НЕ подтвердилась)
**Статус: наблюдался один раз (2026-08-03), воспроизвести не удалось ни
тогда, ни прогоном всех комнат уровня 1 (2026-08-07).** Заведён, чтобы
наблюдение не потерялось; закрывать нельзя — ни как исправленный, ни как
«не баг», пока нет надёжного сценария. Оговорка: BUG-GATEMOD-1, из-за
которого решётка стартовала не в том состоянии, с тех пор закрыт, и
наблюдение могло быть его следствием.
**Что наблюдалось.** Комната 5: Кид стоял НА тайле решётки (0,9)
и ждал, пока она опустится. После закрытия пошёл вправо — прошёл в комнату 1
и упал на (1,1).
**Что уже измерено и в чём загвоздка.** Сразу после наблюдения повторить не
получилось: в том же месте Кид стоит на `x = 196`, `col = 9`, и решётка его
ДЕРЖИТ — то есть штатно.
Арифметика оригинала объясняет разницу. `is_obstacle` (seg004) ставит
плоскость блокировки в `x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_MIDX`, для
колонки 9 это **x = 205**. При этом «колонка 9» по `get_tile_div_mod_m7`
это `x ∈ [191, 205)`. Пока `curr_col == 9`, Кид гарантированно левее
плоскости и обязан блокироваться; чтобы пройти, он должен оказаться **правее
205**, то есть уже на дальней стороне решётки, — и тогда уход вправо законен:
решётка закрылась у него за спиной, в оригинале она блокирует плоскость, а не
весь тайл.
Отсюда рабочая гипотеза: в момент наблюдения Кид стоял правее 205 (успел зайти
по тайлу дальше, пока решётка была поднята), и поведение штатное. Но повторить
эту позу и снять `x` пока не удалось, поэтому гипотеза НЕ подтверждена.
**Что снять в следующий раз** (без этих чисел вопрос не закрыть):
1. `Kid.x` и `Kid.curr_col` в момент, когда решётка уже закрылась, а Кид ещё
стоит на её тайле — до шага вправо;
2. модификатор решётки (openness) комнаты 5, тайл 9 — `can_bump_into_gate()`
считает её препятствием только пока `(modif >> 2) + 6 < char_height`, то
есть пока она опустилась достаточно низко относительно РОСТА кадра;
3. `Kid.x` покадрово на самом шаге вправо — где именно перестал блокировать.
Быстрый способ снять первое: отладочный стоп-кадр (**1** заморозить, **2**
продолжить), затем чтение `_Kid` из отладчика MAME; подгонка позы по пикселю —
читы `[`/`]`.
**Возможный корень, если гипотеза не подтвердится.** Проверка идёт по колонке,
которая на шве уже принадлежит СОСЕДНЕЙ комнате (`curr_row_coll_room[]` в
оригинале); у нас межкомнатная коллизия на шве — исторически проблемное место
(ср. закрытый BUG-SEAM-PINGPONG). Второй кандидат — `char_height` в
`can_bump_into_gate()`: если он берётся не от того кадра, решётка может
перестать считаться препятствием раньше времени.
---
# Оптимизация отрисовки (записано 2026-07-29)
Не баги — план работ. Оба пункта про одно: у оригинала пометка тайла к