Files
Sprinter-SDCC/applications/SprPoP/tests/host/README.md
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

122 lines
9.4 KiB
Markdown
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.
# tests-host — модульные тесты движка SprPoP
Прогоняются под `ucsim_z80`, обвязка общая — `testkit/` (там же объяснение,
почему под z80, а не хостовым gcc, и грабли).
```
make host-tests # из корня, вместе с остальными
make -C applications/SprPoP/tests/host # только эти
make -C applications/SprPoP/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_char` | те же + `pop_guard.c` | физика над `Char` для РАЗНЫХ персонажей: кому достаётся урон (`take_hp` по charid), ветки `land()` (Кид −1 HP с двух рядов, страж гибнет, тень цела), падение и гард по X у слота Guard, `check_guard_fallout`, «окна Char не путают персонажей» |
Главный тест в `t_geom``geom_lcg_matches_reference`. LCG оригинала
(`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме
Горнера ради обхода `__mullong`, и заявка «бит-в-бит как в SDLPoP» до
появления теста держалась только на комментарии. Тест сверяет рукописный
asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому
значению, и по обеим половинам сида.
## t_phys: характеризация, а не проверка правильности
Набор существует ради **страховки под рефакторинг**. Эталоны сняты с
текущего билда, то есть консервируют и текущие баги — правильность
по-прежнему проверяется сверкой с `SDLPoP/src/`. Ценность в другом:
ближайший крупный шаг (перенос физики с `Kid.` на `Char.`, чтобы её мог
переиспользовать страж) обязан быть **поведение-сохраняющим**, и «трасса до
== трасса после» ловит ровно тот класс ошибок, который там возможен.
Поэтому эталон **переснимается только осознанно**, и в коммите должно быть
написано, что и почему поменялось. Молча обновлённый эталон обесценивает
весь набор.
Своё дело набор сделал: перенос физики на `Char` (GUARD-PHYS, 2026-08-07)
прошёл при **неизменных трассах** — 1723 проверки без единого расхождения.
## Грабли раскладки: `DATA_LOC`
Набор линкует уже полдвижка, и его код перевалил за `--data-loc 0x8000`.
Линкер `sdld` это НЕ ошибка: хвост кода (`_HOME`, деления из libsdcc) он
молча кладёт ПОВЕРХ `_DATA`, после чего тест ведёт себя как «зациклился»
— хотя логика ни при чём. Час на поиск стоило именно это.
Поэтому: `DATA_LOC` в Makefile набора (сейчас `0x9000`), а `host-tests.mk`
перед каждым прогоном проверяет `CODE_LOC + l__CODE <= DATA_LOC` и падает
с внятным сообщением. Упёрлись снова — поднять `DATA_LOC` (стек в
`crt0_ucsim` начинается с 0xFEF0).
```
python3 record_golden.py # переснять все трассы
python3 record_golden.py 7 9 # только сценарии 7 и 9
```
Трасса — по одной записи на кадр: `frame, x, y, dir, col, row, action` и
смещение `curr_seq` от `SEQTBL_BASE`. Кадр гоняется ровно в том порядке,
что и в главном цикле `sprpop.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`.
- **Сценарии из `BUGS_CLOSED.md`.** У каждой закрытой записи есть симптом и
ожидаемое поведение — готовый список регрессионных кейсов, который стоит
переносить в `t_phys` по мере надобности.
- **BUG-LOOSE-2** (гонка «уйти из комнаты раньше, чем долетит плита») —
через мост MAME воспроизвести не удалось, а на уровне логики это
несколько строк: заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень.