3fe083331f
Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько кадров. Харнесс гоняет тот же кадр, что и главный цикл (pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и сравнивает трассу состояния с эталоном. - scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса; sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла). - stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает). - t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/). - t_grab.c — окно зацепа: существует, достижимо коротким шагом, не зависит от рисунка нажатий. - record_golden.py — снятие эталона по одному сценарию за прогон. - testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS. Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем самым перевёл поиск BUG-GRAB-1 на клавиатуру. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
160 lines
6.4 KiB
C
160 lines
6.4 KiB
C
/*
|
|
* t_grab.c — прыжок с места через провал в три тайла с зацепом за дальнюю
|
|
* кромку (уровень 2, комната 9: Кид на краю (1,1) прыгает и должен
|
|
* повиснуть на (1,5)).
|
|
*
|
|
* Зачем отдельный набор. Трюк требует точной фазы и вручную выходит далеко
|
|
* не всегда — в SDLPoP тоже. Поэтому проверяем не «получилось с первого
|
|
* раза», а то, что действительно важно: **окно существует и достижимо**.
|
|
* Разница между «сложно» и «невозможно» — это разница между «так в
|
|
* оригинале» и «баг у нас», и глазами в MAME её не различить.
|
|
*
|
|
* Два утверждения:
|
|
* 1. есть хотя бы одна стартовая X, из которой зацеп удаётся;
|
|
* 2. короткий шаг (Shift+вперёд) приводит Кида В ЭТО окно — иначе окно
|
|
* было бы недостижимо для игрока, что равно его отсутствию.
|
|
*
|
|
* При провале в отчёт уходит карта исходов по всей ширине плиты:
|
|
* G — зацепился, L — приземлился на дальнюю плиту, . — сорвался.
|
|
*
|
|
* Механику зацепа задаёт check_grab (seg006:0A28): она работает ТОЛЬКО на
|
|
* кадрах 102..105 (начало падения) и только при зажатом Shift. mininim,
|
|
* для сравнения, разрешает повиснуть прямо в дуге прыжка (kid-jump.c,
|
|
* кадры 7..10) — но это переработка, а не порт, и источник истины у нас
|
|
* SDLPoP.
|
|
*/
|
|
#include "tcheck.h"
|
|
#include "scene.h"
|
|
#include "stubs.h"
|
|
#include "pop_kid.h"
|
|
#include "pop_map.h"
|
|
|
|
#define E 0
|
|
#define F 1
|
|
#define P 3
|
|
#define W 20
|
|
|
|
/* Уровень 2, комната 9, ряд 1: факел(0), кнопка(1), провал(2,3,4),
|
|
* колонна(5). Ряд 2 под провалом пуст — падение уходит из комнаты. */
|
|
static const uint8_t room9[30] = {
|
|
E, E, E, E, E, W, W, W, W, W,
|
|
F, F, E, E, E, P, F, F, F, P,
|
|
W, W, E, E, E, P, F, W, W, W,
|
|
};
|
|
|
|
/* Судить по curr_row нельзя: у улетающего вниз Кида ряд заворачивается
|
|
* (0,1,2,0,…), и падение выглядит как «остался наверху». Надёжный признак
|
|
* — ДЕЙСТВИЕ: 2 = вис/подтягивание, 6 = вис у стены. */
|
|
#define ACT_STAND 0
|
|
#define ACT_RUN_JUMP 1
|
|
#define ACT_HANG_CLIMB 2
|
|
#define ACT_HANG_STRAIGHT 6
|
|
|
|
#define X_LO 76
|
|
#define X_HI 99
|
|
|
|
static char probe(uint8_t x, uint8_t k1, uint8_t k2)
|
|
{
|
|
uint8_t i;
|
|
sc_room(room9, 9);
|
|
sc_kid_at_x(1, 1, x, 0 /* лицом вправо */);
|
|
sc_trace_clear();
|
|
sc_run(k1, 4); /* фаза 1: ↑+вперёд — прыжок с места */
|
|
sc_run(k2, 26); /* фаза 2: Shift — попытка зацепа */
|
|
|
|
for (i = 0; i < sc_len; i++) {
|
|
uint8_t a = sc_trace[i].action;
|
|
if (a == ACT_HANG_CLIMB || a == ACT_HANG_STRAIGHT) return 'G';
|
|
}
|
|
i = sc_len ? (uint8_t)(sc_len - 1) : 0;
|
|
if (sc_trace[i].col >= 5 &&
|
|
(sc_trace[i].action == ACT_STAND || sc_trace[i].action == ACT_RUN_JUMP))
|
|
return 'L';
|
|
return '.';
|
|
}
|
|
|
|
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++); }
|
|
|
|
/* Карта исходов — только когда что-то сломалось: по ней сразу видно,
|
|
* окно съехало или исчезло. */
|
|
static void dump_map(const char *tag, uint8_t k1, uint8_t k2)
|
|
{
|
|
uint8_t x;
|
|
puts_(tag);
|
|
for (x = X_LO; x <= X_HI; x++) put(probe(x, k1, k2));
|
|
put('\n');
|
|
}
|
|
|
|
/* Куда встаёт Кид после n-го короткого шага от x0. */
|
|
static uint8_t step_x(uint8_t x0, uint8_t nsteps)
|
|
{
|
|
uint8_t n, fx = x0;
|
|
sc_room(room9, 9);
|
|
sc_kid_at_x(1, 1, x0, 0);
|
|
for (n = 0; n < nsteps; n++) {
|
|
sc_trace_clear();
|
|
sc_run(SC_SHIFT | SC_R, 4); /* шаг = нажатие, не удержание */
|
|
sc_run(0, 6); /* отпустить, дать доиграть */
|
|
if (sc_len) fx = sc_trace[sc_len - 1].x;
|
|
}
|
|
return fx;
|
|
}
|
|
|
|
TC_TEST(grab_window_exists)
|
|
{
|
|
uint8_t x, n = 0, lo = 0, hi = 0;
|
|
for (x = X_LO; x <= X_HI; x++)
|
|
if (probe(x, SC_U | SC_R, SC_SHIFT) == 'G') {
|
|
if (!n) lo = x;
|
|
hi = x;
|
|
n++;
|
|
}
|
|
/* Окно обязано существовать: без него трюк непроходим в принципе, а
|
|
* уровень 2 без него не пройти. */
|
|
TC_TRUE(n > 0);
|
|
if (!n) {
|
|
dump_map("no-window: ", SC_U | SC_R, SC_SHIFT);
|
|
} else {
|
|
/* Границы фиксируем: если поедут — это осознанное изменение физики,
|
|
* а не случайность. */
|
|
TC_EQ(lo, 91);
|
|
TC_EQ(hi, 95);
|
|
}
|
|
}
|
|
|
|
TC_TEST(grab_window_reachable_by_safe_step)
|
|
{
|
|
/* Игрок доходит до кромки короткими шагами. Если они не попадают в
|
|
* окно, существование окна ничего не стоит. */
|
|
TC_EQ(step_x(80, 1), 91);
|
|
TC_EQ(step_x(80, 2), 95);
|
|
}
|
|
|
|
TC_TEST(grab_same_for_all_key_patterns)
|
|
{
|
|
/* Живой игрок держит «вперёд» и жмёт Shift по-разному. Исход не должен
|
|
* зависеть от того, отпущена ли стрелка и когда нажат Shift. */
|
|
uint8_t x;
|
|
for (x = X_LO; x <= X_HI; x++) {
|
|
char a = probe(x, SC_U | SC_R, SC_SHIFT);
|
|
char b = probe(x, SC_U | SC_R, SC_SHIFT | SC_R);
|
|
char c = probe(x, SC_U | SC_R | SC_SHIFT, SC_SHIFT | SC_R);
|
|
TC_EQ(b, a);
|
|
TC_EQ(c, a);
|
|
}
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
sc_init();
|
|
TC_RUN(grab_window_exists);
|
|
TC_RUN(grab_window_reachable_by_safe_step);
|
|
TC_RUN(grab_same_for_all_key_patterns);
|
|
return 0;
|
|
}
|