Files
Sprinter-SDCC/applications/SprPoP/tests/host/t_grab.c
T
snark13 31b82661eb SprPoP: автономное приложение, выделенное из roomtest
Порт PoP переехал в applications/SprPoP — приложение, которое собирается
само: код, оригинальные данные, конверторы ресурсов и сборка внутри одной
папки.  Наружу знает единственный путь — корень тулчейна (SPRINTER_ROOT,
по умолчанию ../..).  applications/PoP/roomtest ЗАМОРОЖЕНА и остаётся
архивом закрытых задач, багов и исполненных планов.

Скопировано из applications/PoP/roomtest@4b74478.  Перенос проверен
побайтово: собранный sprpop.exe совпал с roomtest.exe того же коммита,
все 39 дисковых ресурсов и все 16 генерируемых заголовков — тоже, host-
тесты зелёные (15/15).

Раскладка:
  src/           рукописный C (roomtest.c -> sprpop.c)
  gen/           генерируемые заголовки, в репозитории
  assets/orig/   оригинальные данные игры, вне репозитория (копирайт)
  assets/packed/ то, что ложится на диск, в раскладке диска
  tools/         конверторы; все пути — в одном tools/paths.py
  build/         выход: exe, каталоги ресурсов, hdd/, промежуточные atl/

Сборка ресурсов: assets/packed и gen — версионируемые ВХОДЫ, а не то, что
пересчитывается каждым make.  Автоматика построена на ОТСУТСТВИИ файла, а
не на таймстемпах: git не хранит времена, и в свежем клоне сравнение по
времени превращалось бы в лотерею.  Недостающий ресурс или заголовок
чинится сам, рекурсивным вызовом в ветку генерации.

Музыка собирается из любого из четырёх наборов записей (make music-mp3,
music-mt32, ...); набор входит в имя stamp'а, поэтому смена набора сама
делает музыку устаревшей.  Длины реплик больше не захардкожены: упаковщик
печатает их в gen/pop_music_ticks.h, и шкала сцены выражена через них —
иначе mt32 (реплики на 6% длиннее) молча ломал катсцену.

Тулчейн: в app.mk два обратносовместимых крючка (SRC_DIR/BUILD_DIR),
HDD_IMG стал ?=; команда сборки roomtest не изменилась.  Корневой
make host-tests переключён на SprPoP.

Подгонка тайминга катсцены с принцессой (PV_MAGIC_LEAD): сцена
render-bound и идёт ~49 тиков/с вместо 60, из-за чего кода реплики
приходила раньше молнии.  Это обход, а не лечение; разбор с замерами —
docs/BUGS_OPEN.md, записи SND-PACE-DEAD, PV-RENDER-BOUND, MUS-LEFT-TEAR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 12:12:28 +03:00

284 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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);
}
}
/* ---- Спуск через НИЖНЮЮ КРОМКУ комнаты (BUG-GRAB-SEAM-DOWN) ---------- *
* Уровень 7, комната 14 — по ней уровень и проходят: Кид на ряду 0 висит на
* кромке кнопки (0,2), отпускает и в падении должен зацепиться за кромку
* (2,2). Тайл-цель лежит В НИЖНЕМ ряду, то есть в момент зацепа персонаж
* уже НИЖЕ комнаты — в оригинале это curr_row == 3, а get_tile для него
* уходит по links.down в комнату снизу (find_room_of_tile, seg006:005D).
*
* Пока do_fall не пускал ряд дальше 2, check_grab на этом отрезке не звался
* НИ РАЗУ: Кид пролетал мимо кромки, набирал fall_y > 32, и дальше окно
* зацепа было закрыто уже по скорости — спуск становился непроходимым.
* Тест держит именно это: окно существует и достижимо. */
#define C 6 /* closer */
#define O 15 /* opener */
static const uint8_t room14[30] = {
C, F, O, E, E, E, E, E, F, F,
W, W, W, E, E, E, E, E, W, W,
F, F, P, E, E, E, E, E, W, W,
};
static const uint8_t room15[30] = {
W, W, W, E, E, E, E, E, W, W,
F, F, P, E, E, E, E, E, W, W,
W, W, W, E, E, E, E, E, W, W,
};
/* Ряд 2 комнаты 14 = «ряд −1» комнаты 15 (pop_map_set_above): после перехода
* зацеп продолжает целиться в ту же кромку, уже как в потолок. */
static uint8_t above14[10];
#define ACT_MIDAIR 3
#define ACT_FREEFALL 4
static uint8_t went_below; /* Kid уже пересёк нижнюю границу комнаты 14 */
/* Один кадр + переход в комнату снизу — ровно то, что делает главный цикл
* sprpop.c по pop_fell_out (enter_room(down) + exit_room_timer). */
static void step14(uint8_t keys)
{
sc_trace_clear(); /* трасса тут не нужна, важен исход */
sc_run(keys, 1);
if (pop_fell_out) {
uint8_t i;
pop_fell_out = 0;
went_below = 1;
for (i = 0; i < 10; i++) above14[i] = room14[20 + i];
sc_room(room15, 15);
pop_map_set_edges(0, 0, 14, 0); /* сверху — комната 14 */
pop_map_set_above(above14);
pop_leave_timer = 2;
}
}
/* Исход попытки:
* 'G' — повис на НИЖНЕЙ кромке (то, ради чего тест: ряд 2 комнаты 14);
* 'U' — зацепился обратно за ту кромку, с которой сорвался (ряд 0) —
* это другой, всегда работавший случай, и засчитывать его нельзя;
* '.' — пролетел мимо; 'x' — так и не сорвался (проба не состоялась). */
static char probe_down(uint8_t x, uint8_t delay)
{
uint8_t i;
sc_room(room14, 14);
pop_map_set_edges(0, 0, 0, 15); /* links комнаты 14: снизу — 15 */
pop_map_set_above(0);
pop_leave_timer = 0;
pop_fell_out = 0;
went_below = 0;
/* Лицом ВЛЕВО у правой кромки кнопки: обрыв позади — это и есть поза
* спуска (pop_down_action, seg005:482). */
sc_kid_at_x(2, 0, x, -1);
/* ↓ + Shift — спуск с уступа (pop_down_action, seq_68) и вис на кромке.
* Shift держим, иначе Кид сорвётся, не успев повиснуть. */
for (i = 0; i < 30; i++) {
step14(SC_D | SC_SHIFT);
if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) break;
}
if (Kid.action != ACT_HANG_CLIMB && Kid.action != ACT_HANG_STRAIGHT) return 'x';
/* отпустить Shift — сорваться с виса (hang_fall, seg005) */
for (i = 0; i < 12; i++) {
step14(0);
if (Kid.action == ACT_MIDAIR || Kid.action == ACT_FREEFALL) break;
}
if (Kid.action != ACT_MIDAIR && Kid.action != ACT_FREEFALL) return 'x';
for (i = 0; i < delay; i++) step14(0); /* пауза перед Shift */
for (i = 0; i < 30; i++) {
step14(SC_SHIFT);
if (Kid.action == ACT_HANG_CLIMB || Kid.action == ACT_HANG_STRAIGHT) {
if (!went_below) return 'U';
/* За ЧТО взялся: после перехода вниз ряд 0 = кромка ряда 2
* комнаты 14 (наша цель), ряд 2 = кромка ряда 1 комнаты 15
* (этажом ниже — этот случай работал и раньше). */
return Kid.curr_row == 0 ? 'G' : 'g';
}
}
return '.';
}
#define XD_LO 90
#define XD_HI 113
#define XD_DELAY_MAX 8
TC_TEST(grab_below_room_edge_window_exists)
{
uint8_t x, d, n = 0, n0 = 0;
for (x = XD_LO; x <= XD_HI; x++)
for (d = 0; d <= XD_DELAY_MAX; d++)
if (probe_down(x, d) == 'G') { n++; if (!d) n0++; }
TC_TRUE(n > 0);
/* И играбельность: зацеп обязан удаваться при самом естественном вводе —
* отпустил вис и сразу зажал Shift (d = 0). Окно, доступное только с
* точной задержкой, для игрока почти равно отсутствующему. */
TC_TRUE(n0 > 0);
if (!n) { /* карта исходов: строка = задержка Shift */
uint8_t i;
for (i = 0; i <= XD_DELAY_MAX; i++) {
put('d'); put((char)('0' + i)); put(':'); put(' ');
for (x = XD_LO; x <= XD_HI; x++) put(probe_down(x, i));
put('\n');
}
}
}
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);
TC_RUN(grab_below_room_edge_window_exists);
return 0;
}