Files
Sprinter-SDCC/applications/PoP/roomtest/tests-host
Александр Петров 0cd6b2d737 L3-CHKP, зацеп в прыжке опцией, pop_tune.h; сборка 10 мин -> 1:48
L3-CHKP (чекпойнт уровня 3).  Флаг взводится, когда Кид уходит ВЛЕВО ИЗ
комнаты 7, а do_startpos по нему подменяет старт на комнату 2, тайл (0,6),
лицом влево и снимает loose-плиту (7,0,4).  Тонкость, на которой я сначала
ошибся: level3_set_chkp (seg002:0665) вызван из leave_room ДО
goto_other_room, поэтому `Char.room == 7` — это комната, ИЗ которой уходят,
а не в которую входят.  Поймал пользователь прогоном в SDLPoP: смерть В
комнате 7 вернула его в стартовую 9, а плита осталась цела.  Проверено в
MAME: вход в 7 флаг не ставит, уход влево — ставит; респавн в комнате 2;
после обычной смерти (без чекпойнта) плиты восстанавливаются как раньше.

Зацеп ПРЯМО В ПРЫЖКЕ (check_grab_run_jump, seg006:1228) — портирован за
выключателем POP_ENABLE_JUMP_GRAB.  Это НЕ ваниль: в оригинале зацепиться
можно только в начале падения (кадры 102..105), то есть Shift приходится
жать уже в полёте; у SDLPoP это enable_jump_grab, и работает он лишь при
включённых fixes-and-enhancements.  Три точки вызова как у оригинала:
check_action и обе ветки check_bumped (зацеп за верх стены вместо удара).

pop_tune.h — настраиваемые константы в одном месте (аналог
custom_options_type SDLPoP): чекпойнт, выключатель зацепа и отладочная
крутилка POP_DBG_GATE_HOLD (сколько кадров решётка держится поднятой;
оригинал 5, потолок 30 — таймер связи пятибитный, 31 = «заклинено»).
Задача TUNE-1 в TASKS.md: читать это из ini рядом с exe.

СБОРКА.  --max-allocs-per-node снижен со 100000 (дефолт sprinter-cc) до
3000 (дефолт SDCC) через `make ALLOCS=...`, а pop_trob.c уехал в БАНК 6 —
на 3000 резидент иначе не влезает (замер: конец _HOME 0xBC69 при стеке с
0xBB00).  Итог: сборка с нуля 1:48 вместо >10 минут, куча 2751 Б вместо
2298.  Релизная сборка — make ALLOCS=100000; сравнивать занятость банков
можно только при одинаковом ALLOCS.

Отдельно (вне git, SDLPoP в .gitignore): из референса вычищена вся наша
отладка DBG-GRAB — трасса JMP, GRAB try/probe/fail/OK/skip, автоскриншоты
seg003, печати DBG mob/mid/overlay/kidobj и счётчик dbg_shots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 00:57:51 +03:00
..

tests-host — модульные тесты движка roomtest

Прогоняются под ucsim_z80, обвязка общая — testkit/ (там же объяснение, почему под z80, а не хостовым gcc, и грабли).

make host-tests                                  # из корня, вместе с остальными
make -C applications/PoP/roomtest/tests-host     # только эти
make -C applications/PoP/roomtest/tests-host t_geom

Что покрыто

набор модуль что проверяет
t_geom pop_geom.c геометрия комнаты (x_bump, y_land, y_to_row) и PRNG оригинала
t_phys pop_map.c + pop_kid.c + pop_ctrl.c покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал

Главный тест в t_geomgeom_lcg_matches_reference. LCG оригинала (s = s*214013 + 2531011) написан в pop_geom.c на ассемблере по схеме Горнера ради обхода __mullong, и заявка «бит-в-бит как в SDLPoP» до появления теста держалась только на комментарии. Тест сверяет рукописный asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому значению, и по обеим половинам сида.

t_phys: характеризация, а не проверка правильности

Набор существует ради страховки под рефакторинг. Эталоны сняты с текущего билда, то есть консервируют и текущие баги — правильность по-прежнему проверяется сверкой с SDLPoP/src/. Ценность в другом: ближайший крупный шаг (перенос физики с Kid. на Char., чтобы её мог переиспользовать страж) обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит ровно тот класс ошибок, который там возможен.

Поэтому эталон переснимается только осознанно, и в коммите должно быть написано, что и почему поменялось. Молча обновлённый эталон обесценивает весь набор.

python3 record_golden.py          # переснять все трассы
python3 record_golden.py 7 9      # только сценарии 7 и 9

Трасса — по одной записи на кадр: frame, x, y, dir, col, row, action и смещение curr_seq от SEQTBL_BASE. Кадр гоняется ровно в том порядке, что и в главном цикле roomtest.c:

pop_ctrl_tick();   // ввод -> control(): смена последовательности
kid_tick();        // play_seq: следующий кадр
pop_phys_tick();   // падение/приземление/стена
pop_loose_tick();  // досчёт тряски и снятие провалившейся плиты

Покадровость здесь принципиальна: порт seg005/seg006 — конечный автомат, и ошибка почти всегда проявляется не в одной функции, а в расхождении траектории через несколько кадров. Плюс главный риск переноса на Char — не арифметика, а то, кто владеет окном Char внутри кадра; поймать это можно только прогоном полного кадра.

Проверка, что харнесс воспроизводит устройство: сценарий stand даёт frame=15, x=114, y=118, curr_seq=0x19A3 — те же значения, что читаются из _Kid в живом MAME.

Как это заработало под ucsim

Три шва, каждый закрыт без правок продуктового кода:

  1. Таблицы анимации. pop_kid.c ходит по ним абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве их приводит маппинг W0. В тесте kid_data.bin просто кладётся по 0x100 (gen_kid_blob.py), а gfx_w0_map заглушен. Следствие: код набора линкуется с --code-loc 0x1000 (CODE_LOC в Makefile), чтобы не налезть на блоб.
  2. Окружение. stubs.c — libc/libbgi (графика, память, файлы), соседние модули (pop_bg/pop_trob/pop_level/pop_redraw) и состояние стража. Заглушки не пустые там, где это меняет смысл: файловое чтение реально отдаёт kid_data.bin (иначе pop_kid_data_load тихо сдаётся, kdat_ok остаётся нулём и play_seq не делает ничего — трасса выходит из одного застывшего кадра), клавиатура отвечает по набору «нажатых» скан-кодов (так под тест попадают read_input и read_user_control, а не только диспетчер), а обращения к соседям журналируются (tk_log) — можно проверять «плита отвалилась и попросила перерисовку», а не только координаты.
  3. __banked. Модули физики помечены как банковые, SDCC генерирует на них трамплин ___sdcc_bcall_ehl, которому нужны set_bank/get_bank. В тесте память плоская — bank_stub.s отдаёт нулевой банк и пустое переключение, соблюдая контракт по регистрам.

Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные переключения банков, тайминги кадра, настоящая клавиатура.

Что нужно, чтобы двинуться дальше

  • Доступ к странице уровня. pop_level.c ходит по абсолютным адресам ((uint8_t *)(LVL_DATA_OFF + …) после gfx_w0_map). Для pop_map это не потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ через pop_map_set, — но набор на сам pop_level/pop_trob в это упрётся. Решение то же, что для kid_data: положить страницу по нужному адресу и поднять CODE_LOC.
  • Сценарии из bug_closed.md. У каждой закрытой записи есть симптом и ожидаемое поведение — готовый список регрессионных кейсов, который стоит переносить в t_phys по мере надобности.
  • BUG-LOOSE-2 (гонка «уйти из комнаты раньше, чем долетит плита») — через мост MAME воспроизвести не удалось, а на уровне логики это несколько строк: заспавнить кусок, сменить комнату, тикать до приземления, проверить щебень.