90e304f071
КОД ИГРЫ НЕ МЕНЯЛСЯ — правка находки 24 сделана и откачена. ТЕСТЫ. Набор t_wall вырос с одной проверки до трёх: добавлены проверка X на каждом кадре падения (персонаж не должен оказываться внутри кладки) и симметричный прыжок через провал с другой стороны. Ключевое в них — форма ожидания. Тест НЕ требует нуля заходов в кладку, а сторожит их ЧИСЛО: сейчас ровно два случая из четырнадцати стартовых позиций. Это ванильное поведение оригинала, для которого SDLPoP держит отдельное опциональное исправление; больше двух — значит правка сделала нас хуже ванили, меньше — значит фикс кем-то портирован. То есть тест сразу готов обслуживать оба будущих режима. По дороге тест дважды ловил не баг движка, а мою ошибку в самой сцене (старт в пустой клетке; перелёт через площадку считался нарушением). Оба раза чинился тест, а не движок. НАХОДКА 24 ОТЛОЖЕНА. Перезагрузка кадра в in_wall верна по букве оригинала, но эффекта показать не удалось: все 15 наборов host-тестов дают одинаковый результат до и после. При этом правка не бесплатна — маппинг окна и перезагрузка кадра на каждое выталкивание. Платить за недоказанное не стали. НОВАЯ ЗАДАЧА: docs/vanilla_vs_bugfixed.md — поддержка двух поведений, ванильного и с багфиксами. Туда переехали находка 24, три опциональных фикса SDLPoP (скольжение сквозь стену, прыжок над воротами, гобелен) и готовый детектор из t_wall. Открытые вопросы записаны: чем переключать (возможно, объединить с уже существующим VANILLA/ENHANCED), цена рантайм-проверки в горячем пути, что считать умолчанием, как гонять тесты в двух режимах. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MsUsEFAQfsjjQpJ7RtKVY
204 lines
10 KiB
C
204 lines
10 KiB
C
/*
|
||
* 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), которое мы намеренно не портировали, повторяя ваниль
|
||
* (docs/sdlpop_audit.md, находка 21).
|
||
*
|
||
* Поэтому тест не требует нуля, а СТОРОЖИТ ЧИСЛО: пока их ровно два, мы
|
||
* ведём себя как оригинал. Стало больше — значит правка сделала хуже
|
||
* ванили; стало меньше — кто-то портировал фикс и об этом нужно знать.
|
||
* Проверено: фикс 24 (перезагрузка кадра в in_wall) на это число НЕ
|
||
* влияет. */
|
||
#define WALL_VANILLA_GLIDE_CASES 2
|
||
|
||
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_VANILLA_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;
|
||
}
|