diff --git a/applications/PoP/roomtest/TASKS.md b/applications/PoP/roomtest/TASKS.md index 0836e44..801ce8f 100644 --- a/applications/PoP/roomtest/TASKS.md +++ b/applications/PoP/roomtest/TASKS.md @@ -51,7 +51,12 @@ ## P0 — делаем сейчас -### L2-PASS. Приёмка уровня 2 +### L2-PASS. Приёмка уровня 2 — **SMOKE ПРОЙДЕН 2026-08-05** + +> **Прогон 2026-08-05 (пользователь): уровень 2 пройден.** Полная приёмка по +> всем комнатам — позже; из smoke пришли [BUG-LOOSE-3](bug_list.md#bug-loose-3) +> и [BUG-GUARD-DEAF-1](bug_list.md#bug-guard-deaf-1). Следом прогнан smoke +> уровня 3 — его наблюдения в [разделе «Уровень 3»](bug_list.md#уровень-3). Баги отрисовки уровня 2 пользователь подаёт списком отдельно — они идут в [`bug_list.md`](bug_list.md), раздел «Уровень 2». Ниже — **карта @@ -105,6 +110,45 @@ закрыт на уровне 1 (L1-EXIT), но на этом уровне не прогонялся; - **смерть/респавн** возвращают на уровень 2, а не на 1. +### Связность комнат уровней 1–3 (снято 2026-08-05) + +Обход графа `roomlinks` (@1952, по 4 байта на комнату: L, R, U, D; 0 = нет +соседа) от стартовой комнаты — тем же методом, которым на уровне 1 нашлись +13/18/24 (см. «НЕ БАГИ» в [`bug_closed.md`](bug_closed.md)). Скрипт разовый, +в репозиторий не клался: чтение трёх массивов, BFS и проверка симметрии. + +| уровень | старт | недостижимы | признак | +|---------|-------|-------------|---------| +| 1 | к.1 (0,0) | **13, 18, 24** | ссылки наружу есть, обратных нет | +| 2 | к.5 (1,3) | **нет** | граф полностью симметричен, все 24 достижимы | +| 3 | к.9 (2,4) | **23, 24** | то же, что на 1: односторонние ссылки, обе комнаты **полностью пустые** | + +``` +ур.3: 23 L→4, у 4 R=22 | 24 L→2, у 2 R=7 + 23 R→22, у 22 L=4 | 24 R→7, у 7 L=2 + | 24 U→16, у 16 D=0 + на 23 ссылается только 24, на 24 — только 23; тайлы обеих = все empty +``` + +То есть на уровне 3 это даже более чистый случай, чем на уровне 1: там в +брошенных комнатах была геометрия, здесь — пустота. Практический вывод тот +же: **в 23/24 возможен «мусор в шве»** (наш рендер кромки читает крайнюю +колонку соседа ПО ССЫЛКЕ, а сосед соседом себя не считает), приоритет багов +там низкий, в игре их не видно. + +**Уровень 2 — недостижимых нет, но есть три КОЛОДЦА без выхода** (единственная +связь — вверх, откуда Кид падает): + +``` +к.10 U→4 пики(2,2) + пол — падение из комнаты 4 +к.14 U→21 шахта 2 тайла шириной, дно = обломки +к.17 U→15 то же +``` + +Это не баги данных: в 14/17 попадают только падением насмерть, а из 10 +(если выжил) выхода нет вовсе — так в оригинале. При приёмке не считать +«застрял» багом. + ### L3-CHOMP. Чомперы — механика уровня 3 **Где:** 5 штук, комнаты 5(тайл 4), 16(23,24,25), 22(26). Дальше они почти @@ -576,7 +620,15 @@ BUG-1 (провал на row 1 при переходе через открыты Условие «делать ПОСЛЕ CLIP-1» **снято** — CLIP-1 закрыт (см. P0). Проверка — секундомером по одинаковому отрезку рядом с живым SDLPoP, не «на глаз». -### L1-PASS. Сквозное прохождение уровня 1 +### L1-PASS. Сквозное прохождение уровня 1 — **SMOKE ПРОЙДЕН 2026-08-05** + +> **Прогон 2026-08-05 (пользователь): успешный.** От старта до выхода с +> уровня одним заходом — меч подобран, **оба стража побеждены в честном бою** +> (без читов), выход отработал корректно. Это smoke: пройдены не все +> комнаты, поэтому таблица обхода 24 комнат остаётся открытой. Ценность +> прогона в том, что он снял главные риски этапа 1 разом — боёвка, +> предметы, переход с уровня — и стал регресс-базой для уровня 2. + От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота, пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно регресс-база для уровня 2. diff --git a/applications/PoP/roomtest/bug_closed.md b/applications/PoP/roomtest/bug_closed.md index 0763d10..7a64ff7 100644 --- a/applications/PoP/roomtest/bug_closed.md +++ b/applications/PoP/roomtest/bug_closed.md @@ -11,6 +11,99 @@ --- +## BUG-JUMPWALL-1. Недолетевший прыжок проходил СКВОЗЬ стену — **ЗАКРЫТ 2026-08-05** + +**Симптом (пользователь, приёмка уровня 3).** Комната 14: Кид на (0,8) +лицом влево, прыжок с места — пролетает сквозь кладку и падает в комнату 13 +на (2,3). Обобщение пользователя (оно и оказалось верным): «если Кид +прыгает, недолетает и должен врезаться в стену и упасть ВДОЛЬ неё, у нас он +летит по свободной траектории, как будто ему никто не мешает». + +**Корень — упрощение в `check_collisions`, про которое там же стояла +оговорка.** Оригинал (seg004:0004) каждый кадр считает флаги перекрытия для +ТРЁХ рядов (`curr`/`above`/`below`), а `move_coll_to_prev` (seg004:00DF) в +начале следующего кадра выбирает из них тот, что соответствует ряду прошлого +кадра. Так «прошлые» флаги остаются настоящими и на кадре СМЕНЫ РЯДА. + +У нас ряд был один, и на смене ряда `prev` заполнялся тройками («уже +перекрывал всё»), что подавляло бамп на этом кадре. В падении ряд меняется +почти каждый кадр, а переход флага 0→1 на стене приходится ровно на него — +поэтому удар не регистрировался и `bumped_fall` (который и гасит `fall_x`) +не вызывался. + +**Как ловили.** Не в MAME, а на харнессе: набор +[`tests-host/t_wall.c`](tests-host/t_wall.c) — комната 14 уровня 3 с +подложенным дном шахты, свип по всей ширине стартовой плиты (x = 177..196). +Из 20 стартовых позиций 4 давали проход сквозь кладку (Кид оказывался в +колонке 4 при стене в колонке 5). Первый вариант сценария (одна стартовая +X, из центра плиты) БАГА НЕ ПОКАЗАЛ — отсюда свип. + +**Фикс.** Три ряда как в оригинале + дословный `move_coll_to_prev` +(включая условия ±3 на заворот ряда при смене комнаты). Цена — 3×14 +`get_tile` на кадр вместо 14, банк 3 +115 Б. + +**Чем проверено.** `t_wall` зелёный; **все 1723 существующие трассы +`t_phys` не изменились ни на байт** — правка поведение-сохраняющая для +всего остального. В живой игре подтвердил пользователь: «ударились и +падаем вертикально вниз». + +--- + +## BUG-SEAM-WEDGE-1. Клин кладки в пустом углу (2,0) — **ЗАКРЫТ 2026-08-05** + +**Симптом (пользователь, приёмка уровня 3).** Комната 18: в тайле (2,0) +нарисован кусок кладки, хотя по данным там пусто. + +**Корень.** У тайла (2,0) «сосед снизу-слева» — это колонка −1 ряда 3, то +есть тайл ЧУЖОЙ комнаты (`room_BL` — левая от нижней). `load_rowbelow` +(seg008:368) резолвит его через `get_tile_to_draw(room_left, 9, 0, …)` и +подставляет стену ТОЛЬКО когда такой комнаты нет. Мы клали стену +безусловно, а `draw_topright` для стены рисует угловой кусок — он и был +клином. Для комнаты 18 диагональ — комната 13, её тайл (0,9) пуст, значит +рисовать нечего. + +**Фикс.** Ряд «снизу» стал одиннадцатибайтным: `[0..9]` — колонки комнаты +снизу, `[10]` — тайл (0,9) комнаты снизу-слева (дефолт «стена», если её +нет). Затронуты `pop_level.c` (заполнение), `pop_bg.c` (чтение), +`roomtest.c` (размер массива). + +**Проверено** в MAME: комната 18 уровня 3, клина нет. + +--- + +## BUG-LOOSE-3. Чёрный бар под упавшей плитой-потолком — **ЗАКРЫТ 2026-08-05** + +**Симптом (пользователь, приёмка уровня 2).** Комната 6: Кид сбивает +плиту-потолок (−1,2) — плита падает, но на её месте остаётся чёрный бар. +Два уточнения пользователя оказались диагностическими: + +1. «бар попал в фоновое изображение» — Кид прыгает поверх, уходит, бар + остаётся → чернота лежит в ОЗУ-копии, и heal возвращает её каждый кадр; +2. «когда возвращаемся в комнату после выхода — бара нет» → статическая + отрисовка комнаты рисует всё правильно, виновата ЗАПЕЧКА. + +**Корень.** `pop_ceil_bake_empty` стирал полосу потолка чёрной плитой +(`bar`, банк NORMAL — пишет и в ОЗУ-копию) шириной 64 px, а восстанавливал +только ДВА тайла ряда −1: `(−1,col)` и `(−1,col+1)`. Но куски тайлов +рисуются ВВЕРХ от своей нижней грани и свисают ВПРАВО, поэтому в стёртую +полосу попадает графика и соседа СЛЕВА, и тайлов ряда 0 — их верхушки. +Незакрашенным оставался прямоугольник ~13×3 px. + +**Замер (он же метод).** Плита роняется без игры — записью +`pop_ceil_modif[col] = 1` в отладчике MAME (это ровно то, что делает +`make_loose_fall` для потолка). Дальше попиксельная сверка скриншота +«после запечки» со скриншотом «комната перерисована заново» (выйти и +вернуться): различие локализовалось в прямоугольник **экранные x 230..255, +y 47..52** при полосе потолка y 44..52. + +**Фикс.** Запечка восстанавливает всё, чья графика попадает в полосу: +ряды −1 И 0, колонки `col−1..col+1`. + +**Проверено:** та же попиксельная сверка после фикса даёт **0 различий** в +полосе; пользователь подтвердил в игре. + +--- + ## BUG-RJUMP-1. Разбег-прыжок не берёт провал в три тайла — **ЗАКРЫТ 2026-08-04** **Симптом (пользователь, приёмка уровня 2).** Комната 1: Кид с разбегу diff --git a/applications/PoP/roomtest/bug_list.md b/applications/PoP/roomtest/bug_list.md index efbbeb2..813c2bb 100644 --- a/applications/PoP/roomtest/bug_list.md +++ b/applications/PoP/roomtest/bug_list.md @@ -27,6 +27,8 @@ BUG-GATE-PASS-1 — однократное наблюдение прохода | ID | что | тип | статус | |----|-----|-----|--------| +| [BUG-SPIKE-1](#bug-spike-1) | пробег по выдвинутым пикам не убивает: модификатор застревает в «полностью вышли» (h=1), а это в оригинале не смертельно для бегущего | Major | **открыт, замер есть, заведён 2026-08-05** | +| [BUG-GUARD-DEAF-1](#bug-guard-deaf-1) | страж не оборачивается на вернувшегося Кида: `is_guard_notice` не взводился | Major | **фикс есть, ждёт игровой проверки** | | [BUG-GRAB-1](#bug-grab-1) | ур. 2 комн. 9: прыжок с места через 3 тайла — нет зацепа за кромку | Major | **причина найдена (клавиатура, BUG-KBD-5), фикс есть, ждёт игровой проверки** | | [BUG-GATEMOD-1](#bug-gatemod-1) | ворота стартуют закрытыми, хотя в уровне открыты | Major | **фикс есть, ждёт проверки** | | [Уровень 2](#уровень-2) | остальные баги с приёмки | — | принимаются по ходу | @@ -45,6 +47,15 @@ BUG-GATE-PASS-1 — однократное наблюдение прохода подаст их списком отдельно — раздел заведён под них заранее, чтобы наблюдения не растеклись по переписке. +**Закрыто с этой волны:** [BUG-LOOSE-3](bug_closed.md#bug-loose-3) — чёрный +бар под упавшей плитой-потолком (запечка восстанавливала не всё, что стёрла). + +**Порядок работы по уровню 2 (2026-08-05):** наблюдения СНАЧАЛА копятся +здесь целиком — фиксы берём, когда уровень пройден до конца. Причина: +косметика отрисовки почти всегда сводится к общему корню (ср. волну +BUG-CEIL-1/2/3 + BUG-OCCL-1), и чинить по одному наблюдению — значит +чинить один и тот же корень трижды. + Карта содержимого уровня (что где стоит по данным `res2002.bin`, какие кнопки какие ворота открывают) — в [`TASKS.md`](TASKS.md#l2-pass), запись **L2-PASS**: по ней видно, «механика не сработала» это или @@ -56,6 +67,169 @@ BUG-GATE-PASS-1 — однократное наблюдение прохода вводит ровно три новых фоновых тайла — **большая колонна (низ 8 / верх 9) и верх двери (12)**; если артефакт рядом с ними, это первый подозреваемый. + +# Уровень 3 — баги с приёмки (заведено 2026-08-05) + +Smoke-прогон уровня 3 (пользователь, 2026-08-05) дал четыре наблюдения. Два +из них — **не баги, а неначатые задачи**, и записаны как таковые: + +| наблюдение | что это на самом деле | +|------------|------------------------| +| к.22: чомпер (2,6) не анимируется и **вообще не рисуется** | [L3-CHOMP](TASKS.md#l3-chomp) — механики чомперов НЕТ. В таблице тайлов `pop_bg.c:211` строка `12 chomper` рисует только основание, правую грань и низ; самих челюстей (спрайт из `chtab`, `draw_tile_anim` seg008) нет вовсе. Так и должно выглядеть до порта | +| к.10: скелет не оживает | [L3-SKEL](TASKS.md#l3-skel) — `check_skel` (seg002:0E1F) не портирован. **И комната другая:** тайл `skeleton(21)`, который оживает, лежит в **к.1 (1,5)** — это `skeleton_room=1, skeleton_column=5, skeleton_row=1`. Ещё два скелета уровня (к.17 (2,7), к.19 (2,2)) — просто декорация, они не оживают никогда. Плюс условие: скелет встаёт, только когда **дверь уровня уже открыта** и Кид стоит в колонке 2 или 3 | + +Остальные два были настоящими багами и **закрыты в тот же день** — разбор +корней в [`bug_closed.md`](bug_closed.md): **BUG-JUMPWALL-1** (флаги +коллизий считались для одного ряда вместо трёх — бамп терялся на кадре +смены ряда) и **BUG-SEAM-WEDGE-1** (сосед угла (2,0) безусловно считался +стеной). + + +## BUG-SPIKE-1. Пики не убивают бегущего Кида — ОТКРЫТ, ЗАМЕР ЕСТЬ + +**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 6, пики (1,3): + +| действие | что происходит | +|----------|----------------| +| длинный прыжок с ряда 0 на пики | **смерть — правильно** (путь `fell_on_spikes`) | +| пробег по ряду 1 прямо по пикам | **урона нет** | +| после уборки пик | **на экране остаются белые остатки остриёв** (в оригинале чисто) | +| прыжок на месте, стоя на пиках | урона нет | +| просто стоять на выдвинутых пиках | можно сколько угодно | + +**Замер (MAME, чтение `room_modif` комнаты 6).** Пока Кид стоит на тайле, +модификатор пики (индекс 13) = **0x8E**, то есть «пики ПОЛНОСТЬЮ вышли и +идёт обратный отсчёт». Дальше вся арифметика сходится с оригиналом: + +``` +is_spike_harmful (seg007:1178): 0/-1 → 0; <0 → 1; 1..4 → 2; >=5 → 0 +check_spiked (seg006:0658): убивает при h>=2 на кадрах бега 7..14 + и при h!=0 на кадрах приземления 43/26 +``` + +То есть **при h = 1 (пики уже вышли) бегущий не гибнет и в оригинале** — +смертельно только окно ВЫДВИЖЕНИЯ (модификатор 1..4, h = 2). Наши +`animate_spike`, `start_anim_spike`, `is_spike_harmful`, `check_spiked` +сверены с seg006/seg007 построчно и совпадают дословно. + +**Значит расходится ТАЙМИНГ, а не логика:** у нас пики успевают полностью +выйти ДО того, как Кид на них вбежит, и он проскакивает мимо смертельного +окна 1..4. Триггер — `check_spike_below` (seg006:1199), и у нас он берёт +габарит спрайта из `kid_fp` — метрик ПОСЛЕДНЕГО отрисованного кадра, а +оригинал берёт `char_x_left/right` ТЕКУЩЕГО кадра (`set_char_collision`). +Это первый подозреваемый; второй — момент вызова `process_trobs` +относительно `play_frame` (сколько кадров модификатор успевает пройти, +пока Кид добегает). + +**ВТОРОЕ НАБЛЮДЕНИЕ (то же место, сравнение с оригиналом) — оно и уточняет +гипотезу.** Кид уронил плиту-потолок и спрыгнул вниз; пики выдвинулись и +«спрятались не все — часть артефактов осталась». Скриншоты рядом: +[наш](bugscreens/l2-r6-spikes-ours.png) и +[SDLPoP](bugscreens/l2-r6-spikes-sdlpop.png) в той же позе. У нас из-под +щебня торчат белые острия, у оригинала пик не видно ВООБЩЕ. + +Это стыкуется с замером: пока Кид стоит рядом, `check_spike_below` каждый +кадр зовёт `start_anim_spike`, а тот при отрицательном модификаторе +переставляет его обратно в 0x8F — то есть пики у нас «залипают выдвинутыми» +и отсчёт до уборки не доходит. В оригинале в той же позе они убраны, +значит его `check_spike_below` эту колонку УЖЕ не задевает. + +**Отсюда единая рабочая гипотеза на оба симптома: наш триггер шире/раньше +оригинального.** Оригинал берёт габарит ТЕКУЩЕГО кадра +(`char_x_left/right`, их ставит `set_char_collision` в этом же кадре), мы — +`kid_fp`, метрики ПОСЛЕДНЕГО ОТРИСОВАННОГО кадра. Если наш диапазон +колонок хоть на пиксель шире, получаем ровно это: пики выходят до подхода +Кида (он проскакивает окно 1..4 → не гибнет) и не убираются, пока он рядом +(→ белые остатки на экране). + +**Что снять при разборе:** покадрово `room_modif[13]` вместе с `Kid.x`, +`Kid.frame` и вычисленными `c0..c1` на подходе и пробеге через тайл; тот же +прогон в SDLPoP с печатью `left_checked_col`/`right_checked_col`. Сверять +надо не результат, а ДИАПАЗОН КОЛОНОК кадр в кадр. + +**Не регрессия правок 2026-08-05:** ни трёхрядные флаги коллизий, ни +`is_guard_notice`, ни правки отрисовки в путь пик не входят (`check_spiked` +сам зовёт `get_tile_at_char`, модификаторы читает из `pop_trob_modif`). + +--- + + +## BUG-GUARD-DEAF-1. Страж не оборачивается на вернувшегося Кида — ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ + +**Наблюдение (пользователь, 2026-08-05).** Уровень 2, комната 11, идёт бой +со стражем. Страж **выталкивает Кида в правую комнату 22**. Кид заходит +обратно в комнату 11 — страж стоит на (1,1), **повёрнут налево и Кида не +видит**. Предположение пользователя: это часть большой задачи «полностью +переделать поведение стража по образцу Кида». + +**Диагноз: большая переделка тут ни при чём, корень маленький и точный.** +Само возвращение стража в исходную позу — ПРАВИЛЬНОЕ поведение, порт верен; +не хватает ровно одного сигнала. + +Разбор по SDLPoP: + +1. **Выход Кида из комнаты «усыпляет» стража — так и в оригинале.** + `leave_guard` (seg002:02F5) складывает стража обратно в данные уровня + (тайл, `x`, **направление**, skill, HP), а `enter_guard` (seg002:0112) + при возврате поднимает живого стража **с УБРАННЫМ мечом** + (`sword_0_sheathed` + `seq_77_guard_stand_inactive`) и обнуляет + `is_guard_notice`/`guard_refrac`. То есть страж после возврата ВСЕГДА + неактивен и смотрит в запомненную сторону — у нас так же + (`pop_guard_enter`, `pop_guard.c:127`). +2. **Дальше решает `autocontrol_guard_inactive` (seg002:0876), и он Кида за + спиной ИГНОРИРУЕТ.** Кид вернулся справа, страж смотрит влево → + `char_opp_dist()` отрицательна → ветка `else if (distance < 0) return;`. + Единственный выход из неё — флаг **`is_guard_notice`**: «Кид нашумел». + При нём страж оборачивается (`move_4_down`). Направление в + `check_can_guard_see_kid` НЕ участвует вовсе, так что «не видит» — это + не про луч видимости, а именно про этот флаг. +3. **У нас `is_guard_notice` не взводится НИГДЕ.** `grep` по всем исходникам + roomtest: объявление (`pop_guard.c:20`), сброс (`pop_guard.c:47`) и одно + чтение (`guards.c:171`). Присваивания `= 1` нет ни одного — флаг мёртв, + поэтому неактивный страж не обернётся НИКОГДА, что бы Кид ни делал. + +**Где его взводит оригинал** (это и есть объём фикса): + +| место | событие | +|-------|---------| +| `seg006:633..641` — `play_seq`, опкод SOUND | звук `SND_SILENT`(0), `SND_FOOTSTEP`(1), `SND_BUMP`(2). `SND_DRINK`(3)/`SND_LEVEL`(4) — НЕ шум. Главный источник: шаги бега/приземления | +| `seg004:05F1` `bumped_sound` | удар в стену | +| `seg005:185,195` | мягкое и среднее приземление (**только `charid_0_kid`**) | +| `seg006:1294`, `seg006:1734` | Кид обрушил loose-плиту (зацепом и наступив) | +| `seg007:766` | нажата кнопка | + +У нас опкод SOUND в `play_seq` (`pop_kid.c:426`) просто съедает байт +аргумента — звука нет, и флаг вместе с ним потерялся. Именно эта строка — +90 % фикса: `SND_SILENT` называется «silent» потому, что звука не издаёт, +**но стражи его всё равно замечают**, и в `seqtbl` он стоит, например, в +`ready` (доставание меча). + +**Ожидаемое поведение после фикса.** Кид возвращается в комнату 11 бегом → +первый же `SND_FOOTSTEP` взводит флаг → страж оборачивается и достаёт меч. +Стоя на месте, Кид может подкрасться к стражу со спины — это НЕ баг, а +механика оригинала. + +**Оговорка (не проверено вживую).** Разбор построен на том, что страж после +возврата неактивен (меч убран, кадр 166). Это следует из кода +`pop_guard_enter`, но в MAME не снималось; если окажется, что меч у него +ОБНАЖЁН, то работает другая ветка (`autocontrol_guard_active`, где +`can_guard_see_kid == 2` направления не спрашивает) — и тогда корень другой. +Снять при фиксе: `Guard.sword`, `Guard.frame`, `can_guard_see_kid` сразу +после входа в комнату. + +**Фикс (2026-08-05): все пять мест портированы.** Опкод SOUND в `play_seq` +(`pop_kid.c`) взводит флаг для звуков 0..2; `bumped_fall`/`bumped_floor`, +мягкое и среднее приземление, обрушенная плита (все три пути `check_press`) +— в `pop_map.c`; щелчок кнопки — в `pop_trob.c`. Заглушка `is_guard_notice` +добавлена в `tests-host/stubs.c` (автопилота стража в наборах нет). + +**Как проверить в игре:** комната 11 уровня 2, дать стражу вытолкнуть Кида +вправо в комнату 22 и вернуться бегом — на первом же шаге страж обязан +обернуться и достать меч. Подкрасться к нему СТОЯ (не двигаясь) по-прежнему +можно — это механика оригинала, а не баг. + +--- + ## BUG-GRAB-1. Прыжок с места через провал в 3 тайла: зацепа нет — **ПРИЧИНА НАЙДЕНА, ФИКС ЕСТЬ, ЖДЁТ ИГРОВОЙ ПРОВЕРКИ** diff --git a/applications/PoP/roomtest/bugscreens/l2-r6-loose-blackbar.png b/applications/PoP/roomtest/bugscreens/l2-r6-loose-blackbar.png new file mode 100644 index 0000000..47e2c1b Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/l2-r6-loose-blackbar.png differ diff --git a/applications/PoP/roomtest/bugscreens/l2-r6-spikes-ours.png b/applications/PoP/roomtest/bugscreens/l2-r6-spikes-ours.png new file mode 100644 index 0000000..b5847ef Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/l2-r6-spikes-ours.png differ diff --git a/applications/PoP/roomtest/bugscreens/l2-r6-spikes-sdlpop.png b/applications/PoP/roomtest/bugscreens/l2-r6-spikes-sdlpop.png new file mode 100644 index 0000000..446fae8 Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/l2-r6-spikes-sdlpop.png differ diff --git a/applications/PoP/roomtest/bugscreens/l3-r18-stray-masonry.png b/applications/PoP/roomtest/bugscreens/l3-r18-stray-masonry.png new file mode 100644 index 0000000..4f9b178 Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/l3-r18-stray-masonry.png differ diff --git a/applications/PoP/roomtest/bugscreens/l3-r22-chomper-missing.png b/applications/PoP/roomtest/bugscreens/l3-r22-chomper-missing.png new file mode 100644 index 0000000..55bab04 Binary files /dev/null and b/applications/PoP/roomtest/bugscreens/l3-r22-chomper-missing.png differ diff --git a/applications/PoP/roomtest/pop_bg.c b/applications/PoP/roomtest/pop_bg.c index 426132e..b644299 100644 --- a/applications/PoP/roomtest/pop_bg.c +++ b/applications/PoP/roomtest/pop_bg.c @@ -633,7 +633,11 @@ static void draw_tile(int row, int col) * room_B(0, col-1); col-1<0 → левая комната снизу = стена уровня. */ if (row + 1 > 2) { int lc = col - 1; - if (lc < 0) rbl_code = 20; /* кромка уровня = стена */ + /* lc < 0 — угол (2,0): сосед снизу-слева лежит в ЧУЖОЙ комнате + * (room_BL). Её тайл (0,9) приходит одиннадцатым байтом below-ряда; + * «стена» там стоит, только если такой комнаты нет — как в + * load_rowbelow (seg008:368). См. BUG-SEAM-WEDGE-1. */ + if (lc < 0) rbl_code = g_below ? (uint8_t)(g_below[10] & 0x1F) : 20; else rbl_code = g_below ? (uint8_t)(g_below[lc] & 0x1F) : 1; } else rbl_code = tile_code(row + 1, col - 1); @@ -843,8 +847,22 @@ void pop_ceil_bake_empty(int col) __banked gfx_set_bank(GFX_BANK_TRANSPARENT); g_clip_top = POP_YOFF; bake_rest = 1; /* фон = покой (см. bake_rest) */ - draw_tile(-1, col); /* тайл уже EMPTY → колодец наверх */ - if (col + 1 < 10) draw_tile(-1, col + 1); + /* Восстанавливаем ВСЁ, чья графика попадает в стёртую полосу, а не только + * два тайла ряда −1 (BUG-LOOSE-3). Куски тайла рисуются ВВЕРХ от своей + * нижней грани и свисают ВПРАВО, поэтому в полосу над тайлом col лезут: + * • ряд −1, колонки col−1..col+1 — сама полоса кладки у потолка; + * • ряд 0, колонки col−1..col+1 — их верхушки заходят в те же 8 px. + * Раньше перерисовывались только (−1,col) и (−1,col+1), и в полосе + * оставался чёрный прямоугольник ~13×3 px (замер: экранные y 47..52 при + * полосе 44..52), который heal возвращал каждый кадр. Признак, что дело + * именно в запечке: после выхода из комнаты и возврата бара нет. */ + { + int c0 = (col > 0) ? col - 1 : 0; + int c1 = (col + 1 < 9) ? col + 1 : 9; + int c; + for (c = c0; c <= c1; c++) draw_tile(-1, c); + for (c = c0; c <= c1; c++) draw_tile(0, c); + } bake_rest = 0; g_clip_top = 0; gfx_set_bank(GFX_BANK_NORMAL); diff --git a/applications/PoP/roomtest/pop_bg.h b/applications/PoP/roomtest/pop_bg.h index 5c1e230..dab1ebf 100644 --- a/applications/PoP/roomtest/pop_bg.h +++ b/applications/PoP/roomtest/pop_bg.h @@ -46,9 +46,11 @@ void pop_room_draw(uint8_t room_num, const uint8_t *fg, const uint8_t *bg, const uint8_t *lcol_fg, const uint8_t *lcol_bg) __banked; -/* Задать ряд 0 комнаты СНИЗУ (fg-коды, 10 шт.) — для draw_tile_topright, - * когда тайл нижнего ряда стал empty (после падения loose): «стена вниз» - * рисуется по коду room_B(0, col-1). 0 — нет комнаты снизу. */ +/* Задать ряд 0 комнаты СНИЗУ — для draw_tile_topright, когда тайл нижнего + * ряда стал empty (после падения loose): «стена вниз» рисуется по коду + * room_B(0, col-1). ОДИННАДЦАТЬ байт: [0..9] — колонки комнаты снизу, + * [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см. + * load_rowbelow, seg008:368). 0 — нет комнаты снизу. */ void pop_room_set_below(const uint8_t *below_row0_fg) __banked; /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы diff --git a/applications/PoP/roomtest/pop_kid.c b/applications/PoP/roomtest/pop_kid.c index 3694d59..10a64a8 100644 --- a/applications/PoP/roomtest/pop_kid.c +++ b/applications/PoP/roomtest/pop_kid.c @@ -423,7 +423,13 @@ void play_seq(void) case 0xF4: /* KNOCK_DOWN: приземление трясёт loose В СВОЁМ РЯДУ */ knock = -1; break; - case 0xF2: /* SOUND — потребить 1 байт */ + case 0xF2: /* SOUND (seg006:0611): звука у нас нет, но опкод несёт + * ВТОРОЙ смысл — «Кид нашумел», и его терять нельзя. + * Шумом считаются id 0..2 (SILENT/FOOTSTEP/BUMP); id 0 + * назван silent потому, что НЕ звучит, а стражи его всё + * равно замечают (стоит, например, в seq доставания меча). + * DRINK(3)/LEVEL(4) — не шум. См. BUG-GUARD-DEAF-1. */ + if (SEQ(Char.curr_seq) < 3) is_guard_notice = 1; Char.curr_seq++; break; case 0xF1: /* END_LEVEL: seq_70 доиграл — уровень пройден. diff --git a/applications/PoP/roomtest/pop_level.c b/applications/PoP/roomtest/pop_level.c index 0b817b9..23fdca6 100644 --- a/applications/PoP/roomtest/pop_level.c +++ b/applications/PoP/roomtest/pop_level.c @@ -210,12 +210,23 @@ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg, for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; } } - /* belowrow: верхний ряд (row0) комнаты снизу */ + /* belowrow: верхний ряд (row0) комнаты снизу — ОДИННАДЦАТЬ байт. + * [0..9] — колонки 0..9 комнаты снизу; [10] — тайл (0,9) комнаты + * ПО ДИАГОНАЛИ СНИЗУ-СЛЕВА (room_BL у оригинала). Одиннадцатый нужен + * тайлу (2,0): его «сосед снизу-слева» — это колонка −1 ряда 3, то есть + * чужая комната, и load_rowbelow (seg008:368) резолвит её именно так, + * с дефолтом «стена» только когда такой комнаты НЕТ. Раньше мы клали + * туда стену БЕЗУСЛОВНО, и в пустом углу рисовался клин кладки + * (BUG-SEAM-WEDGE-1, ур. 3 комн. 18). */ if (down >= 1 && down <= 24) { const uint8_t *df = room_fg_ptr(down); + uint8_t dl = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(down - 1) * 4]; for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F); + below_fg[10] = (dl >= 1 && dl <= 24) + ? (uint8_t)(room_fg_ptr(dl)[9] & 0x1F) + : 20; /* кромка уровня */ } else { - for (i = 0; i < 10; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */ + for (i = 0; i < 11; i++) below_fg[i] = 20; /* нет комнаты снизу = стена */ } gfx_w0_unmap(); diff --git a/applications/PoP/roomtest/pop_level.h b/applications/PoP/roomtest/pop_level.h index 0838570..be1c112 100644 --- a/applications/PoP/roomtest/pop_level.h +++ b/applications/PoP/roomtest/pop_level.h @@ -63,7 +63,8 @@ uint8_t pop_level_tileset(uint8_t n); * для коллизии на колонке −2 (Kid стоит В шве); * rcol_fg[6] симметрично (col0 / col1); * нет комнаты → стена/0; - * below_fg[10] — верхний ряд (row0) комнаты СНИЗУ («стена + * below_fg[11] — [0..9] верхний ряд (row0) комнаты СНИЗУ, + * [10] тайл (0,9) комнаты СНИЗУ-СЛЕВА («стена * вниз»); нет комнаты → стена. * 0 — OK, -1 — уровень не загружен / room вне диапазона. */ int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg, diff --git a/applications/PoP/roomtest/pop_map.c b/applications/PoP/roomtest/pop_map.c index 0064112..392d690 100644 --- a/applications/PoP/roomtest/pop_map.c +++ b/applications/PoP/roomtest/pop_map.c @@ -500,6 +500,8 @@ static void land(void) } else { seq = SEQ_17_SOFT_LAND; } + /* seg005:185 — мягкое приземление слышно (звука нет, флаг есть). */ + is_guard_notice = 1; } else { uint8_t deadly = (uint8_t)(Kid.fall_y >= 33); /* СНЯТЬ до обнуления */ pop_kid_hurt = 1; @@ -512,6 +514,7 @@ static void land(void) pop_kid_dead = 1; return; } + is_guard_notice = 1; /* seg005:195 */ seq = SEQ_20_MEDIUM_LAND; } Kid.fall_x = Kid.fall_y = 0; @@ -1123,9 +1126,13 @@ static void bumped_fall(void) if (Kid.action == ACT_FREEFALL) { Kid.fall_x = 0; } else { - kid_set_seq(SEQ_45_BUMPFALL); /* (TODO: bumped_sound) */ + kid_set_seq(SEQ_45_BUMPFALL); pop_kid_play(); } + /* bumped_sound (seg004:05F1) — ВНЕ ветвления, как в оригинале: удар о + * стену слышен и в свободном падении. Звука у нас нет, но «Кид нашумел» + * взводить обязаны — см. BUG-GUARD-DEAF-1. */ + is_guard_notice = 1; } /* bumped_floor (seg004:053B): удар о стену, когда под персонажем ПОЛ. @@ -1173,8 +1180,9 @@ static void bumped_floor(void) (frame >= 102 && frame < 107)) kid_set_seq(SEQ_46_HARDBUMP); else - kid_set_seq(SEQ_47_BUMP); /* (TODO: bumped_sound) */ + kid_set_seq(SEQ_47_BUMP); pop_kid_play(); + is_guard_notice = 1; /* bumped_sound (seg004:05F1) */ } /* ---- check_collisions (seg004:0004) -------------------------------- * @@ -1195,13 +1203,22 @@ static void bumped_floor(void) * Тем же корнем лечится влёт в стену на длинном прыжке (комн. 6) и провал * сквозь пол после приземления у стены (комн. 5). * - * ОТЛИЧИЕ ОТ ОРИГИНАЛА (сознательное): он держит флаги ТРЁХ рядов (curr/ - * above/below), чтобы при смене ряда «прошлые» флаги были настоящими. У - * нас ряд один: сменился ряд — считаем, что персонаж уже перекрывал всё - * (prev = 3), то есть на кадре смены ряда бампа нет, а на следующем он - * посчитается честно. Три ряда — это втрое больше get_tile каждый кадр; - * если где-то вылезет пропущенный бамп ровно на кадре приземления — - * вернуться сюда. */ + * ТРИ РЯДА — как в оригинале (было: один, и это оказалось BUG-JUMPWALL-1). + * Оригинал каждый кадр считает флаги для рядов row−1, row, row+1, а + * move_coll_to_prev (seg004:00DF) в начале СЛЕДУЮЩЕГО кадра выбирает из них + * тот, что соответствует ряду прошлого кадра. Так «прошлые» флаги остаются + * настоящими и на кадре смены ряда. + * + * Наш прежний упрощённый вариант (prev = 3 = «уже перекрывал») подавлял бамп + * ровно на этом кадре — а в падении ряд меняется чуть ли не каждый кадр, и + * переход 0→1 на стене приходится как раз на него. Симптом: недолетевший + * прыжок уходит СКВОЗЬ кладку по свободной траектории (ур. 3 комн. 14). + * Цена трёх рядов — 3×14 get_tile на кадр вместо 14; замерено, что это + * дешевле любого из обходных вариантов, а поведение совпадает с оригиналом. + * + * Ряды вне комнаты (−1 и 3) считаются штатно: get_tile сам резолвит ряд −1 + * через комнату сверху (g_above), а ряд 3 отдаёт WALL — то же «через край + * не бампим», что и раньше. */ /* Колонки считаем от −2 до 11, а не 0..9: колонки за швом — это тайлы * СОСЕДНЕЙ комнаты (get_tile резолвит их через g_lcol/g_rcol), и решётка * соседа обязана бампить так же, как своя (иначе Kid уходит сквозь @@ -1215,6 +1232,7 @@ static void bumped_floor(void) #define COLL_IDX(c) ((c) - COLL_C0) #define COLL_ROW_NONE ((int8_t)-128) static uint8_t coll_prev[COLL_N], coll_curr[COLL_N]; +static uint8_t coll_above[COLL_N], coll_below[COLL_N]; static int8_t coll_prev_row = COLL_ROW_NONE; /* ряд, к которому относится coll_prev */ /* «нет колонки»: НЕ −1, потому что −1/−2 — законные колонки шва. */ #define BUMP_NONE ((int8_t)-128) @@ -1244,6 +1262,32 @@ static void set_char_collision(void) * колонки от col(char_x_left)−1 до col(char_x_right)+2 и оставляет прочие * как есть; мы считаем все четырнадцать — они дешёвые (индекс в массив + * switch), зато не остаётся протухших ячеек (FIX_COLL_FLAGS SDLPoP). */ +/* move_coll_to_prev (seg004:00DF): в prev кладём флаги ряда, в котором + * персонаж был НА ПРОШЛОМ кадре. Массивы curr/above/below в этот момент + * ещё хранят прошлый кадр, поэтому нужный ряд просто выбирается из них по + * разнице рядов. Условия — дословно оригинальные, включая ±3: ряд может + * «завернуться» при смене комнаты (2 -> 0 вниз, 0 -> 2 вверх), и тогда + * геометрия колонок та же, что у текущего ряда. + * + * Первый кадр (и кадр после смены комнаты, pop_coll_invalidate) прошлого + * ряда не имеет: prev = «уже перекрывал всё», бампа на нём нет. */ +static void move_coll_to_prev(int8_t row) +{ + const uint8_t *src; + int8_t i; + if (coll_prev_row == COLL_ROW_NONE) { + for (i = 0; i < COLL_N; i++) coll_prev[i] = 3; + return; + } + if (row == coll_prev_row || row + 3 == coll_prev_row || row - 3 == coll_prev_row) + src = coll_curr; /* ряд не менялся */ + else if (row + 1 == coll_prev_row || row - 2 == coll_prev_row) + src = coll_above; /* поднялся на ряд */ + else + src = coll_below; /* опустился на ряд */ + for (i = 0; i < COLL_N; i++) coll_prev[i] = src[i]; +} + static void get_row_collision_data(int8_t row, uint8_t *flags) { int8_t col; @@ -1271,14 +1315,13 @@ static void check_collisions(void) if (Kid.curr_row < 0 || Kid.curr_row > 2) { coll_prev_row = COLL_ROW_NONE; return; } set_char_collision(); - /* move_coll_to_prev (seg004:00DF) в нашем однорядном варианте. */ - if (coll_prev_row == Kid.curr_row) { - for (col = 0; col < COLL_N; col++) coll_prev[col] = coll_curr[col]; - } else { - for (col = 0; col < COLL_N; col++) coll_prev[col] = 3; /* «уже перекрывал» */ - } + move_coll_to_prev(Kid.curr_row); coll_prev_row = Kid.curr_row; - get_row_collision_data(Kid.curr_row, coll_curr); + /* Порядок важен: prev уже забран, теперь три ряда пересчитываются + * НА ЭТОТ кадр (в оригинале ровно так же, seg004:0004). */ + get_row_collision_data(Kid.curr_row, coll_curr); + get_row_collision_data((int8_t)(Kid.curr_row + 1), coll_below); + get_row_collision_data((int8_t)(Kid.curr_row - 1), coll_above); /* Обход СВЕРХУ ВНИЗ, как в оригинале (9→0): побеждает МЛАДШАЯ колонка, * в которой флаг перешёл 0→1. */ for (col = COLL_C0 + COLL_N - 1; col >= COLL_C0; --col) { @@ -1418,10 +1461,13 @@ static void check_press(void) int8_t c = Kid.curr_col; if (g_above && g_link_u && c >= 0 && c <= 9 && (g_above[c] & 0x1F) == TILE_LOOSE && !(g_above[c] & 0x20) && - (int8_t)pop_ceil_modif[c] <= 0) + (int8_t)pop_ceil_modif[c] <= 0) { pop_ceil_modif[c] = 1; /* make_loose_fall(1) */ + is_guard_notice = 1; /* seg006:1734 */ + } } else if (get_tile_above_char() == TILE_LOOSE) { make_loose_fall((Kid.curr_row - 1) * 10 + Kid.curr_col, 1); + is_guard_notice = 1; /* seg006:1734 */ } return; } @@ -1435,8 +1481,10 @@ static void check_press(void) uint8_t tp = (uint8_t)(Kid.curr_row * 10 + Kid.curr_col); if (t == TILE_OPENER || t == TILE_CLOSER) pop_trigger_button(g_room, tp, t, pop_trob_modif(g_room)[tp]); - else if (t == TILE_LOOSE) + else if (t == TILE_LOOSE) { make_loose_fall(tp, 1); + is_guard_notice = 1; /* seg006:1734 */ + } } } diff --git a/applications/PoP/roomtest/pop_trob.c b/applications/PoP/roomtest/pop_trob.c index 42fda36..9b7421f 100644 --- a/applications/PoP/roomtest/pop_trob.c +++ b/applications/PoP/roomtest/pop_trob.c @@ -13,6 +13,7 @@ * пометки, поэтому pop_process_trobs — единственная функция модуля, которую * нельзя звать из __banked-кода. */ #include "pop_bg.h" +#include "pop_guard.h" /* is_guard_notice — нажатая кнопка слышна стражу */ /* Коды тайлов (fg & 0x1F), которые анимируются. */ #define TILE_SPIKE 0x02 @@ -284,8 +285,12 @@ void pop_trigger_button(uint8_t room, uint8_t tilepos, uint8_t link_timer = get_doorlink_timer(modifier); if (link_timer != 0x1F) { /* не заклинено */ set_doorlink_timer(modifier, 5); - if (link_timer < 2) - pop_add_trob(room, tilepos, 1); /* кнопка нажимается (+ TODO sound) */ + if (link_timer < 2) { + pop_add_trob(room, tilepos, 1); /* кнопка нажимается */ + /* seg007:766: щелчок кнопки — шум, страж оборачивается. Звука у + * нас нет, флаг обязан быть (BUG-GUARD-DEAF-1). */ + is_guard_notice = 1; + } do_trigger_list(modifier, button_type); } } diff --git a/applications/PoP/roomtest/roomtest.c b/applications/PoP/roomtest/roomtest.c index d5a6b58..c56a037 100644 --- a/applications/PoP/roomtest/roomtest.c +++ b/applications/PoP/roomtest/roomtest.c @@ -117,7 +117,7 @@ static uint8_t room_fg[30], room_bg[30]; /* lcol_fg/rcol_fg — ШЕСТЬ записей: [0..2] кромочная колонка соседа (col9/col0), * [3..5] следующая за ней (col8/col1) для коллизии, когда Kid стоит В шве и * смотрит наружу (колонка −2 / 11). См. pop_level.h. */ -static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[10]; +static uint8_t lcol_fg[6], lcol_bg[3], rcol_fg[6], rcol_bg[3], below_fg[11]; static uint8_t above_fg[10], above_mod[10]; /* ряд 2 верхнего соседа (полоса кладки у потолка) */ static uint8_t cur_room; /* номер ОТРИСОВАННОЙ комнаты (drawn_room) */ static uint8_t kid_room; /* РЕАЛЬНАЯ комната Kid (S1: == cur_room; расходятся в S3 straddle) */ diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index 3bad08c..2b6cc06 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -25,6 +25,8 @@ OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ # Пробный набор (разведка, без эталонов) линкуется тем же составом. OBJS_grab := $(OBJS_phys) +# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же. +OBJS_wall := $(OBJS_phys) include $(TESTKIT)/host-tests.mk diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c index 0e84a06..2c37846 100644 --- a/applications/PoP/roomtest/tests-host/stubs.c +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -194,3 +194,7 @@ kframe pop_gframe; int8_t can_guard_see_kid; int8_t guard_refrac, kid_sword_strike; int8_t offguard, holding_sword; +/* «Кид нашумел» — физика взводит его на ударе о стену, приземлении и + * обрушенной плите (BUG-GUARD-DEAF-1); читает только автопилот стража, + * которого в наборе нет. */ +int8_t is_guard_notice; diff --git a/applications/PoP/roomtest/tests-host/t_wall.c b/applications/PoP/roomtest/tests-host/t_wall.c new file mode 100644 index 0000000..8086372 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_wall.c @@ -0,0 +1,95 @@ +/* + * t_wall.c — удар о стену В ВОЗДУХЕ (BUG-JUMPWALL-1). + * + * Наблюдение с приёмки уровня 3 (комната 14): Кид прыгает, не долетает и + * вместо удара о кладку с последующим сползанием ВДОЛЬ неё пролетает + * сквозь стену «по свободной траектории». Класс отказа шире одной + * комнаты, поэтому проверяем его отдельным набором, а не сценарием в + * t_phys: там характеризация (трасса == эталон), здесь — утверждение о + * поведении («сквозь кладку нельзя»), которое не должно молча + * переснашиваться вместе с эталонами. + * + * Оригинал: bumped_fall (seg004:04E4) на ударе гасит `Char.fall_x`, то есть + * горизонтальный снос обнуляется и дальше Кид падает вертикально. Кто + * решает, что удар был, — check_collisions (seg004:0004) + check_bumped. + * + * Проверка — свипом по стартовому X, а не одним прыжком: исход зависит от + * фазы, и «с одной X работает» ничего не доказывает (ровно так и вышло с + * первым сценарием — из центра плиты стена держит). + */ +#include "tcheck.h" +#include "scene.h" +#include "stubs.h" +#include "pop_kid.h" +#include "pop_map.h" + +#define E 0 +#define F 1 +#define W 20 + +/* Комната 14 уровня 3 с одной правкой: шахте колонок 6–7 подложено дно + * (ряд 2), потому что соседних комнат у сцены нет, а в игре Кид улетает + * вниз в комнату 13. Остальное — как в данных: площадка (0,8), провал + * (0,6)/(0,7), СТЕНА в колонке 5 рядов 1–2, площадка (0,5) за ней. */ +static const uint8_t room14[30] = { + F, F, F, E, E, F, E, E, F, E, + W, W, W, E, E, W, E, E, W, E, + W, W, W, F, F, W, F, F, W, W, +}; + +/* Плита (0,8) по X: колонка 8 — это [177, 191), плюс шов с пустой (0,9). */ +#define X_LO 177 +#define X_HI 196 + +/* Прыгнуть влево со стартовой X. Возврат — код исхода: + * 'L' — долетел до площадки (0,5) и стоит на ней (ряд 0) — законно; + * '.' — не долетел и упал на дно шахты колонок 6–7 — тоже законно; + * цифра — колонка, в которой Кид оказался ЛЕВЕЕ стены, то есть прошёл + * сквозь кладку колонки 5 (это и есть баг). */ +static char jump_left_from(uint8_t x) +{ + sc_room(room14, 14); + sc_kid_at_x(8, 0, x, -1 /* лицом влево */); + sc_trace_clear(); + /* Клавиши держим только на сам прыжок: если удерживать их всю сцену, + * Кид, ДОЛЕТЕВШИЙ до (0,5), тут же прыгает с неё второй раз — и исход + * говорит уже не о стене, а о втором прыжке. */ + sc_run(SC_L | SC_U, 6); + sc_run(0, 34); + if (!sc_len) return '?'; + { + int8_t col = sc_trace[sc_len - 1].col; + int8_t row = sc_trace[sc_len - 1].row; + if (row == 0) return 'L'; /* остался на верхнем ряду */ + if (col >= 6) return '.'; /* сполз вдоль стены в шахту */ + return (char)('0' + (col < 0 ? 0 : col)); + } +} + +static void put(char c) +{ + if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c; +} +static void puts_(const char *s) { while (*s) put(*s++); } + +TC_TEST(wall_stops_undershot_jump) +{ + uint8_t x, bad = 0; + /* Карта исходов по всей ширине плиты: 'L' — долетел до (0,5), '.' — + * сполз вдоль стены в шахту, цифра — просочился сквозь кладку. */ + puts_("x=177..196: "); + for (x = X_LO; x <= X_HI; x++) { + char r = jump_left_from(x); + put(r); + if (r != 'L' && r != '.') bad++; + } + put('\n'); + TC_EQ(bad, 0); +} + +int main(void) +{ + sc_init(); + TC_RUN(wall_stops_undershot_jump); + return 0; +}