/* * 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); } /* --- ГДЕ КОНЧАЕТСЯ СТЕНА ПО X ---------------------------------------- * * * Колонка 5 — кладка (ряды 1-2). Тайл шириной 14, колонка c занимает * [58 + 14*c, 58 + 14*(c+1)). Для колонки 5 это [128, 142). Персонаж * НИКОГДА не должен оказаться внутри этого отрезка ниже верхнего ряда: * там сплошная кладка, и «падение частично в стене» — как раз оно. * * Проверка отдельная от той, что выше: та смотрит КОЛОНКУ в конце сцены, * а эта — X на КАЖДОМ кадре падения. Персонаж может кончить падение в * законной колонке, успев по дороге пройти сквозь кладку. */ #define WALL_X_LO 128 #define WALL_X_HI 142 /* Сколько кадров трассы имеют X внутри кладки, будучи ниже верхнего ряда. */ static uint8_t frames_inside_wall(uint8_t x) { uint8_t i, n = 0; sc_room(room14, 14); sc_kid_at_x(8, 0, x, -1); sc_trace_clear(); sc_run(SC_L | SC_U, 6); sc_run(0, 34); for (i = 0; i < sc_len; i++) { if (sc_trace[i].row == 0) continue; /* верхний ряд — не кладка */ if (sc_trace[i].x >= WALL_X_LO && sc_trace[i].x < WALL_X_HI) n++; } return n; } TC_TEST(wall_x_never_inside_masonry) { uint8_t x, bad = 0; puts_("inside x=177..196: "); for (x = X_LO; x <= X_HI; x++) { uint8_t n = frames_inside_wall(x); put((char)(n ? ('0' + (n > 9 ? 9 : n)) : '.')); if (n) bad++; } put('\n'); TC_EQ(bad, 0); } /* Симметричный случай: прыжок ВПРАВО через провал колонок 3-4 к площадке * (0,5). Стартовая площадка — колонки 0-2 верхнего ряда; под провалом * дно (ряд 2), по бокам кладка. Недолёт обязан кончиться на дне, а не * внутри кладки: выталкивание из стены работает в обе стороны. */ static char jump_right_from(uint8_t x) { sc_room(room14, 14); sc_kid_at_x(2, 0, x, 1 /* лицом вправо */); sc_trace_clear(); sc_run(SC_R | 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 'R'; /* остался на верхнем ряду */ /* Ниже верхнего ряда законны ОБА провала — слева от кладки * (колонки 3-4, недолёт) и справа (6-7, перелёт через площадку). * Незаконна только сама кладка колонки 5. */ if (col != 5) return '.'; /* Вис на уступе площадки (0,5) — законное состояние, и колонка * при нём как раз 5. Отличаем по действию персонажа. */ { uint8_t act = sc_trace[sc_len - 1].action; if (act == 2 || act == 6) return 'h'; /* hang_climb / hang_straight */ } /* Осталось одно: персонаж НИЖЕ верхнего ряда, в колонке кладки и * не висит — то есть находится ВНУТРИ стены. Именно так выглядит * «падение частично в стене» (docs/sdlpop_audit.md, находка 24). */ return '5'; } } /* ФИКС ПАДЕНИЯ СКВОЗЬ СТЕНУ ВЗЯТ — ЖДЁМ НОЛЬ. * * В ванили прыжок вправо через провал давал ДВА случая из четырнадцати, * где Кид оказывается в колонке кладки, будучи в воздухе, — «падение * частично в стене». Это баг оригинального PoP, для которого SDLPoP * держит опциональное исправление (fix_glide_through_wall, seg005 в * do_fall). * * 2026-08-31 исправление ПОРТИРОВАНО в текущий билд * (glide_through_wall_guard в pop_map.c), и оба случая исчезли. Поэтому * ожидание теперь НОЛЬ. * * Если когда-нибудь появится режим VANILLA без этого фикса, ожидание * станет зависеть от режима: 0 в ENHANCED и 2 в ванильном * (docs/vanilla_vs_bugfixed.md). Число 2 сохранено в имени константы * именно поэтому — оно измерено, а не выдумано. */ #define WALL_VANILLA_GLIDE_CASES 2 /* сколько их было ДО фикса */ #define WALL_EXPECTED_GLIDE_CASES 0 /* сколько ожидаем СЕЙЧАС */ TC_TEST(wall_stops_jump_from_left_side) { uint8_t x, bad = 0; /* Плита колонок 3-4: [100, 128). */ /* Плита колонок 0-2: колонка 2 — это [86, 100). */ puts_("right x=86..99: "); for (x = 86; x <= 99; x++) { char r = jump_right_from(x); put(r); if (r != 'R' && r != '.' && r != 'h') bad++; } put('\n'); TC_EQ(bad, WALL_EXPECTED_GLIDE_CASES); } int main(void) { sc_init(); TC_RUN(wall_stops_undershot_jump); TC_RUN(wall_x_never_inside_masonry); TC_RUN(wall_stops_jump_from_left_side); return 0; }