From 9234c03020335dcd7c2322f18ee67a6d1333c114 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 9 Aug 2026 12:25:38 +0300 Subject: [PATCH] =?UTF-8?q?BUG-GATE-PASS-1:=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=8F=20=D1=84=D0=BB=D0=B0=D0=B3=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7=D0=B8=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=D0=BE=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85?= =?UTF-8?q?=D0=BE=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Оригинал (seg004:0004) индексирует флаги перекрытия колонкой ВНУТРИ разрешённой комнаты и хранит рядом её номер, поэтому решётка комнаты 8 остаётся в своём слоте и после перехода 8->6: переход флага 0->1 виден, bumped() срабатывает. У нас индекс — колонка отрисованной комнаты, тот же тайл менял слот, и enter_room вынужден был выбрасывать историю целиком — на кадре входа бампа не было, и Кид с разбега уходил сквозь закрытые ворота. Вариант B (сдвиг вместо тега комнаты): при БОКОВОМ переходе история не выбрасывается, а перенумеровывается на 10 слотов. check_leave двигает x ровно на ∓140 = 10 тайлов, координата грани едет на те же 140 вместе с габаритом Кида — сами флаги инвариантны, меняется только номер слота. Сдвигаются curr/above/below (prev на следующем кадре всё равно перезапишет move_coll_to_prev), освободившиеся слоты = 3 «уже перекрывал». Вверх/вниз и прочие входы в комнату — по-прежнему полная инвалидация. Дословный вариант A (10 слотов + массив номеров комнат) не взят: он тянет за собой сужение окна перебора колонок, то есть отказ от FIX_COLL_FLAGS. Заведён docs/impl_diff.md — список осознанных расхождений с SDLPoP; правило «фиксировать расхождение» в обоих CLAUDE.md теперь указывает туда. tests-host: все 5 наборов прошли. Приёмка в MAME впереди. Co-Authored-By: Claude Opus 5 --- applications/PoP/CLAUDE.md | 4 +- applications/PoP/docs/README.md | 1 + applications/PoP/docs/impl_diff.md | 110 ++++++++++++++++++++++++++ applications/PoP/roomtest/CLAUDE.md | 4 +- applications/PoP/roomtest/bug_list.md | 43 +++++++--- applications/PoP/roomtest/pop_map.c | 71 ++++++++++++++++- applications/PoP/roomtest/pop_map.h | 9 +++ applications/PoP/roomtest/roomtest.c | 6 ++ 8 files changed, 233 insertions(+), 15 deletions(-) create mode 100644 applications/PoP/docs/impl_diff.md diff --git a/applications/PoP/CLAUDE.md b/applications/PoP/CLAUDE.md index 2e4bae9..21640b5 100644 --- a/applications/PoP/CLAUDE.md +++ b/applications/PoP/CLAUDE.md @@ -23,7 +23,9 @@ sprinter-cc / libc / libbgi. Действуют правила корневог исходнике, а не додумывать. 3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока не доказано обратное (наша платформа/ABI требует отличия — тогда явно - зафиксировать почему в комментарии). + зафиксировать почему в комментарии **и записью в `docs/impl_diff.md`**: + что делает оригинал, что делаем мы, чем платим, что проверять при + регрессе). Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/ seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/ diff --git a/applications/PoP/docs/README.md b/applications/PoP/docs/README.md index c6d7de5..028415e 100644 --- a/applications/PoP/docs/README.md +++ b/applications/PoP/docs/README.md @@ -8,6 +8,7 @@ |----------|-------| | [`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) | | [`../roomtest/bug_list.md`](../roomtest/bug_list.md) | Открытые баги roomtest (закрытые — в `bug_closed.md` рядом) | +| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки | | [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди | diff --git a/applications/PoP/docs/impl_diff.md b/applications/PoP/docs/impl_diff.md new file mode 100644 index 0000000..71bbd97 --- /dev/null +++ b/applications/PoP/docs/impl_diff.md @@ -0,0 +1,110 @@ +# Осознанные расхождения с SDLPoP + +Правило подпроекта (`../CLAUDE.md`): расхождение нашей реализации с +`SDLPoP/src/` — по умолчанию **баг у нас**. Этот файл — список исключений: +мест, где мы сознательно сделали иначе, потому что платформа/ABI/бюджет +кадра требуют другого, а НАБЛЮДАЕМОЕ поведение обязано совпадать. + +Формат записи: что делает оригинал → что делаем мы → почему → чем платим и +что проверять при регрессе. Если запись перестала быть верной (портировали +дословно, отказались от обхода) — удалять, а не оставлять «для истории»: +история в git. + +--- + +## D-1. История флагов перекрытия у бокового шва: сдвиг вместо тега комнаты + +**Файлы:** `roomtest/pop_map.c` (`pop_coll_shift`, `pop_coll_invalidate`, +`check_collisions`), `roomtest/roomtest.c` (`enter_room_side`). +**Связанный баг:** BUG-GATE-PASS-1 (`roomtest/bug_closed.md`). +**Дата:** 2026-08-09. + +### Как в оригинале + +`check_collisions` (seg004:0004) вместе с `get_row_collision_data` +(seg004:0185) держит **10 слотов** флагов перекрытия и рядом — +**параллельный массив номера комнаты**: + +```c +row_coll_flags_ptr[tile_col] = curr_flags; /* tile_col — колонка ВНУТРИ разрешённой комнаты (0..9) */ +row_coll_room_ptr [tile_col] = curr_room; /* и номер этой комнаты */ +... +for (short column = 9; column >= 0; --column) { + if (curr_row_coll_room[column] >= 0 && + prev_coll_room[column] == curr_row_coll_room[column]) { + if ((prev_coll_flags[column] & 0x0F) == 0 && + (curr_row_coll_flags[column] & 0x0F) != 0) + bump_col_left_of_wall = column; + ... +``` + +Ключ слота — пара **(колонка в своей комнате, номер комнаты)**. Решётка +комнаты 8 и до перехода 8→6, и после лежит в слоте 9 с `room = 8`: история +переживает смену комнаты, переход флага 0→1 виден, `bumped()` срабатывает. +Комнату оригинал резолвит на лету через `find_room_of_tile` (seg006:005D), +никакого кэша всех комнат у него нет. + +### Что делаем мы + +Индекс — **колонка ОТРИСОВАННОЙ комнаты**, диапазон −2…11 (14 слотов, +`COLL_C0`/`COLL_N`/`COLL_IDX`), номер комнаты рядом не хранится. При смене +комнаты тот же физический тайл менял бы слот на ±10, поэтому раньше история +просто выбрасывалась (`pop_coll_invalidate` → `prev = 3` = «уже +перекрывал» → бампа нет). Именно это и был BUG-GATE-PASS-1. + +Теперь при **боковом** переходе история не выбрасывается, а +**перенумеровывается**: `pop_coll_shift(∓10)` сдвигает `coll_curr`, +`coll_above`, `coll_below` на 10 слотов и заполняет освободившиеся +тройками. `enter_room_side` зовёт её сразу после `pop_map_set_edges`. + +Корректность держится на том, что `check_leave` двигает `Char.x` ровно на +∓140 = 10 тайлов по 14 px, и координата грани (`pop_x_bump[col + …]`) +сдвигается на те же 140 вместе с габаритом Кида, — **сами флаги +инвариантны**, меняется только номер слота. Сдвигаются `curr/above/below`, +а не `prev`: `prev` на следующем кадре всё равно перезапишет +`move_coll_to_prev`, выбирая источник как раз из этих трёх. + +Переходы **вверх/вниз** и все прочие входы в комнату (старт уровня, +респавн, чит-навигация) остаются на полной инвалидации: там колонки не +сдвигаются, но тайлы под ними принадлежат другой комнате — история +действительно недействительна. + +### Почему не дословно (вариант A) + +Дословный порт — 10 слотов + параллельный массив номера комнаты, индекс по +колонке разрешённой комнаты, бамп только при совпадении номеров; тогда +`pop_coll_invalidate` не нужен вовсе, история сама «не совпадает» там, где +колонка сменила комнату. + +Не взяли по одной причине: **десяти слотов нам не хватит**. Оригинал +перебирает узкое окно вокруг Кида (от `col(char_x_left_coll) − 1` до +`col(char_x_right_coll) + 2`), поэтому коллизии слотов у него практически +не случаются. Мы намеренно считаем все четырнадцать колонок сразу +(FIX_COLL_FLAGS из SDLPoP — чтобы не оставалось протухших ячеек), а при +таком переборе колонки −2/−1 и 8/9 сядут в одни и те же слоты 8/9. То +есть вариант A тянет за собой ещё и сужение окна перебора — то есть отказ +от FIX_COLL_FLAGS, — и это заметно рискованнее точечного сдвига. + +### Чем платим + +- Расхождение структур: если в будущем понадобится знать, из какой комнаты + пришёл тайл конкретного слота, этого у нас нет — придётся идти в вариант A. +- Сдвиг работает только для чисто горизонтальных переходов на ровно 10 + колонок. Любая будущая диагональ/иная ширина комнаты его сломает молча. +- `coll_last_row`: `pop_coll_invalidate` прячет прошлый ряд, чтобы + `pop_coll_shift` мог отменить инвалидацию. Порядок вызовов в + `enter_room_side` (сначала `pop_map_set_edges`, потом `pop_coll_shift`) + стал значимым. + +### Что проверять при регрессе + +Это сердце коллизии, вокруг которого разбирался BUG-SEAM-PINGPONG. После +любой правки здесь — прогон швов: + +1. Уровень 1, комнаты 6 ↔ 8, закрытая решётка, **обе** стороны. +2. Оба режима подхода: мелким шагом (упереться) и с разбега (не пройти + насквозь). +3. Проверить, что пинг-понг у шва не вернулся (экран не перескакивает + туда-сюда на кадре бампа о ворота). +4. `make -C roomtest/tests-host` — наборы `t_wall`/`t_char` ходят по этой же + геометрии. diff --git a/applications/PoP/roomtest/CLAUDE.md b/applications/PoP/roomtest/CLAUDE.md index ae3ad6d..45e63b3 100644 --- a/applications/PoP/roomtest/CLAUDE.md +++ b/applications/PoP/roomtest/CLAUDE.md @@ -9,7 +9,9 @@ Перед реализацией ЛЮБОЙ функции и по ЛЮБОМУ вопросу «как в оригинале» — СНАЧАЛА прочитай соответствующий код SDLPoP и портируй по нему; не пиши по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP = -по умолчанию баг у нас. +по умолчанию баг у нас; ОСОЗНАННЫЕ расхождения (платформа/бюджет требуют +иначе) перечислены в `../docs/impl_diff.md` — новое туда же записью, а не +только комментарием в коде. **Что в работе сейчас — `TASKS_OPEN.md`** (доска задач: приоритеты, критерии готовности); сделанное с протоколами замеров — `TASKS_CLOSED.md`. Баги — diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index bd57e71..b9843df 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -18,7 +18,7 @@ | ID | что | тип | статус | |----|-----|-----|--------| | [BUG-CHEAT-FIGHT-1](#bug-cheat-fight-1) | `+`/`−` в бою с вынутым мечом → Кид теряет управление | Major (чит) | открыт | -| [BUG-GATE-PASS-1](#bug-gate-pass-1) | проход сквозь закрытую решётку шва (комн. 8 -> 6) | Major | **воспроизведён, корень найден 2026-08-08**: история флагов коллизии не привязана к комнате | +| [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) | пики перерисовываются безусловно | оптимизация | открыт | @@ -255,17 +255,42 @@ if (curr_row_coll_room[column] >= 0 && есть ПОДАВЛЯЕТ бамп на кадре входа. А к следующему кадру Кид уже за плоскостью ворот — перехода 0->1 не будет никогда. -### Что делать +### Фикс сделан 2026-08-09 (вариант B — сдвиг истории), ЖДЁТ ПРИЁМКИ В MAME -Порт индексации оригинала: хранить флаги в 10 слотах ПО КОЛОНКЕ РАЗРЕШЁННОЙ -комнаты плюс параллельный массив её номера, и сравнивать prev/curr только при -совпадении номеров. Тогда `pop_coll_invalidate()` не нужен вовсе — история -сама «не совпадает» там, где колонка сменила комнату. +Рассматривались два пути: + +- **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`). После правки обязателен прогон -швов: комнаты 6/8 первого уровня в обе стороны, осторожный шаг и разбег, плюс -проверка, что пинг-понг не вернулся. +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, diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index e31f898..76fc3e4 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -236,7 +236,8 @@ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __ } /* смена комнаты — колонки перепроецированы, прошлые флаги перекрытия * недействительны (в оригинале prev_coll_room != curr_row_coll_room и - * бампа на кадре входа нет). */ + * бампа на кадре входа нет). БОКОВОЙ переход это отменяет: вызывающий + * сразу зовёт pop_coll_shift(∓10) и историю не теряет. */ pop_coll_invalidate(); } @@ -1373,10 +1374,67 @@ static int8_t bump_col_l; /* bump_col_left_of_wall (Kid лицом ВПРА static int8_t bump_col_r; /* bump_col_right_of_wall (Kid лицом ВЛЕВО) */ static int coll_xl, coll_xr; /* char_x_left_coll / char_x_right_coll */ +/* Ряд, к которому относилась история ДО последней инвалидации — чтобы + * pop_coll_shift мог её восстановить (см. ниже). */ +static int8_t coll_last_row = COLL_ROW_NONE; + /* Смена комнаты: колонки перепроецированы, прошлые флаги недействительны. * В оригинале это несовпадение prev_coll_room и curr_row_coll_room — - * на кадре входа бампа нет. */ -static void pop_coll_invalidate(void) { coll_prev_row = COLL_ROW_NONE; } + * на кадре входа бампа нет. Безопасный дефолт для ЛЮБОГО входа в комнату + * (старт уровня, респавн, чит-навигация, переход вверх/вниз); боковой + * переход тут же отменяет его через pop_coll_shift. */ +static void pop_coll_invalidate(void) +{ + coll_last_row = coll_prev_row; + coll_prev_row = COLL_ROW_NONE; +} + +/* БОКОВОЙ переход: историю не выбрасываем, а СДВИГАЕМ (BUG-GATE-PASS-1). + * + * Оригинал (seg004:0004) индексирует флаги колонкой ВНУТРИ разрешённой + * комнаты и хранит рядом номер этой комнаты (row_coll_room_ptr), сравнивая + * prev/curr только при совпадении номеров. Решётка комнаты 8 и до перехода + * 8->6, и после лежит в слоте 9 с room = 8 — история переживает переход, + * переход флага 0->1 виден, bumped() срабатывает. У нас индекс — колонка + * ОТРИСОВАННОЙ комнаты, поэтому тот же тайл менял слот, история терялась, и + * Кид с разбега проходил сквозь закрытые ворота шва. + * + * Физически колонки сдвигаются ровно на 10 (check_leave двигает x на ∓140 = + * 10 тайлов по 14 px), и координата грани (pop_x_bump) сдвигается на те же + * 140 вместе с габаритом Кида — значит САМИ ФЛАГИ инвариантны, меняется + * только их номер слота. Отсюда весь фикс — перенумерация. + * + * Сдвигаем curr/above/below, а не prev: prev на следующем кадре всё равно + * перезапишет move_coll_to_prev, выбирая источник как раз из этих трёх. + * Освободившиеся слоты — 3 («уже перекрывал»), то есть бампа по ним нет: + * ровно то, что в оригинале даёт несовпадение номера комнаты. + * + * dcol — насколько изменились НОМЕРА колонок: уход вправо −10, влево +10. + * Отличия от дословного порта варианта A — applications/PoP/docs/impl_diff.md. */ +static void coll_shift_one(uint8_t *a, int8_t dcol) +{ + int8_t i, j; + if (dcol < 0) { /* источник правее — идём вперёд */ + for (i = 0; i < COLL_N; i++) { + j = (int8_t)(i - dcol); + a[i] = (j < COLL_N) ? a[j] : 3; + } + } else { /* источник левее — идём назад */ + for (i = COLL_N - 1; i >= 0; --i) { + j = (int8_t)(i - dcol); + a[i] = (j >= 0) ? a[j] : 3; + } + } +} + +void pop_coll_shift(int8_t dcol) __banked +{ + if (coll_last_row == COLL_ROW_NONE) return; /* истории и не было */ + coll_prev_row = coll_last_row; /* отменить инвалидацию */ + coll_shift_one(coll_curr, dcol); + coll_shift_one(coll_above, dcol); + coll_shift_one(coll_below, dcol); +} /* set_char_collision (seg006:0723), горизонтальная часть: габарит персонажа * в комнатных координатах. char_x_left = obj_x/2 + 58 (минус полуширина при @@ -1445,7 +1503,12 @@ static void check_collisions(void) /* Вне рядов комнаты (падение мимо пола) get_tile отдаёт WALL во всех * колонках — считать по нему нечего. Стену в падении держит * do_fall/in_wall, выход из комнаты — check_leave_below. */ - if (Char.curr_row < 0 || Char.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; } + if (Char.curr_row < 0 || Char.curr_row > 2) { + /* И coll_last_row тоже: curr/above/below остались от старого кадра, + * сдвигать их при боковом переходе нечего (поведение как раньше). */ + coll_prev_row = coll_last_row = COLL_ROW_NONE; + return; + } set_char_collision(); move_coll_to_prev(Char.curr_row); diff --git a/applications/PoP/roomtest/pop_map.h b/applications/PoP/roomtest/pop_map.h index 48e787f..4fb2036 100644 --- a/applications/PoP/roomtest/pop_map.h +++ b/applications/PoP/roomtest/pop_map.h @@ -28,6 +28,15 @@ void pop_map_set(uint8_t *fg) __banked; * get_tile отдавал мнимую стену (BUG-SWORD-GHOST-1). Звать в enter_room. */ void pop_map_set_edges(uint8_t left, uint8_t right, uint8_t up, uint8_t down) __banked; +/* БОКОВОЙ переход в соседнюю комнату: перенумеровать историю флагов + * перекрытия вместо того, чтобы её выбросить (иначе Кид с разбега проходит + * сквозь закрытые ворота шва — BUG-GATE-PASS-1). dcol — на сколько + * изменились номера колонок: ушли ВПРАВО −10, ВЛЕВО +10. Звать СРАЗУ после + * pop_map_set_edges и только для боковых переходов; вверх/вниз и все прочие + * входы в комнату остаются на полной инвалидации. Разбор — bug_closed.md, + * расхождение с дословным портом — ../docs/impl_diff.md. */ +void pop_coll_shift(int8_t dcol) __banked; + /* check_skel: тайл скелета -> пол + пометка перерисовки (обе страницы). */ void pop_skel_wake_tile(uint8_t tilepos) __banked; diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index fe6caf6..375e3b3 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -283,6 +283,12 @@ static void enter_room_side(uint8_t room, uint8_t side) * pop_map_set_edges) — отдельной копии в приложении нет. */ pop_map_set_edges(pop_room_link(room, 0), pop_room_link(room, 1), pop_room_link(room, 2), pop_room_link(room, 3)); + /* Боковой переход: колонки сдвинулись ровно на 10 (x ∓140), флаги + * перекрытия при этом инвариантны — историю перенумеровываем, а не + * теряем. Без этого решётка шва не бампит на кадре входа и Кид с + * разбега проходит сквозь закрытые ворота (BUG-GATE-PASS-1). */ + if (side == 0) pop_coll_shift(+10); /* ушёл ВЛЕВО: col += 10 */ + else if (side == 1) pop_coll_shift(-10); /* ушёл ВПРАВО: col −= 10 */ pop_room_set_below(below_fg); /* Ряд 2 верхнего соседа → полоса кладки у потолка (row0). fg — статика * уровня, mod — живой room_modif (ленивая инициализация). */