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>
This commit is contained in:
2026-08-27 12:12:28 +03:00
parent 4b74478d19
commit 31b82661eb
235 changed files with 51293 additions and 10 deletions
+123
View File
@@ -0,0 +1,123 @@
# Модульные тесты движка SprPoP под ucsim_z80. Обвязка общая (testkit/),
# здесь — только сами наборы и список модулей, которые в них линкуются.
# Наружу тесты знают тот же единственный путь, что и само приложение, —
# корень тулчейна (см. ../../Makefile). Всё остальное считается от него.
SPRINTER_ROOT ?= $(abspath $(CURDIR)/../../../..)
TESTKIT := $(SPRINTER_ROOT)/testkit
APP_DIR := $(abspath $(CURDIR)/../..)
ENGINE_DIR := $(APP_DIR)/src
# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве
# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код
# набора обязан начинаться выше его конца. С добавлением таблицы кадров
# КАТСЦЕН блоб вырос до 4150 Б (0x100 + 4150 = 0x1136), и прежние 0x1000
# уже резали его хвост — набор падал как «main() не позвал ни одной
# проверки». Держим запас: следующая таблица снова сдвинет границу.
CODE_LOC := 0x1400
# Данные — выше 0x8000: набор t_phys линкует уже полдвижка (геометрия, Kid,
# карта/физика, управление, страж) и его код перевалил за 0x8000. Раньше
# это проявлялось как «тест зациклился»: хвост кода ложился поверх _DATA.
# 0x9000 оставляет ~24 КБ под код и ~28 КБ до стека (0xFEF0).
DATA_LOC := 0xA000
# Заголовки libc/libbgi нужны потому, что pop_kid.c включает gfx/sprite/
# sprinter: под тестом эти функции заглушены (stubs.c), но объявления
# берутся настоящие — иначе разъедутся сигнатуры.
PROJ_ROOT := $(abspath $(TESTKIT)/..)
EXTRA_INC := -I$(PROJ_ROOT)/libc/include -I$(PROJ_ROOT)/libbgi/include -I$(CURDIR) \
-I$(APP_DIR)/gen # генерируемые заголовки (pop_bg_atlas.h и прочие)
OBJS_geom := build/eng_pop_geom.rel
# pop_guard.c линкуется НАСТОЯЩИЙ, а не заглушкой: после перевода физики на
# Char урон идёт через pop_take_hp, и кому именно он достанется (hitp_* или
# guardhp_*) решает charid — то есть ровно та логика, которую тесты обязаны
# проверять, а не имитировать.
# eng_pop_kboot — загрузчик таблиц анимации: с 2026-08-12 он живёт отдельным
# модулем (в продукте — банк 8), а сцене он нужен: sc_init зовёт
# pop_kid_data_load, и стабы open/read отдают ему настоящий блоб.
OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \
build/eng_pop_kboot.rel \
build/eng_pop_map.rel build/eng_pop_ctrl.rel \
build/eng_pop_state.rel build/eng_pop_guard.rel \
build/eng_pop_guard_cold.rel \
build/stubs.rel build/scene.rel build/kid_blob.rel \
build/bank_stub.rel
# Пробный набор (разведка, без эталонов) линкуется тем же составом.
OBJS_grab := $(OBJS_phys)
# t_wall — удар о стену в воздухе (BUG-JUMPWALL-1), состав тот же.
OBJS_wall := $(OBJS_phys)
# t_gate — открытая решётка не должна считаться стеной у осторожного шага
# (нужны pop_map + физика, то есть тот же состав).
OBJS_gate := $(OBJS_phys)
# t_char — физика над Char для РАЗНЫХ персонажей (Кид / страж / тень).
OBJS_char := $(OBJS_phys)
# t_mouse — спецсобытие уровня 8 (мышь). ЕДИНСТВЕННЫЙ набор, куда линкуется
# guards.c: здесь проверяется сам ИИ (autocontrol_mouse) вместе с рождением
# (pop_check_mouse) и общей физикой соперника — то есть ровно то, что в
# остальных наборах заменено ручным прогоном play_guard_frame.
OBJS_mouse := $(OBJS_phys) build/eng_guards.rel
# t_shadow — спецсобытия уровня 12 (встреча с тенью, общий урон, слияние).
# guards.c нужен по той же причине, что и мыши: check_shadow/
# check_killed_shadow/sword_disappears живут в нём.
OBJS_shadow := $(OBJS_phys) build/eng_guards.rel
# t_jaffar — спецсобытия уровня 13 (встреча, победа, выход, падающая гряда).
OBJS_jaffar := $(OBJS_phys) build/eng_guards.rel
# t_cfg — весь codec POP.CFG из единственного прикладного модуля.
OBJS_cfg := build/eng_pop_config.rel build/bank_stub.rel
# pop_app — чистая таблица переходов, без DSS/графики.
OBJS_app := build/eng_pop_app.rel build/bank_stub.rel
# pop_demo — запись управления; t_demo подменяет выход control и raw input.
OBJS_demo := build/eng_pop_demo.rel build/bank_stub.rel
# pop_timer — чистое ядро логического времени; поток QuickSave заглушен
# memory-stream'ом в t_timer.c.
OBJS_timer := build/eng_pop_timer.rel build/bank_stub.rel
# pop_cutscene — общий интерпретатор статических сценариев. Он не знает
# графики, поэтому проверяется отдельно от PV/title-ресурсов.
OBJS_cutscene := build/eng_pop_cutscene.rel build/bank_stub.rel
# pop_flow — чистая таблица маршрута перед загрузкой следующего уровня.
OBJS_flow := build/eng_pop_flow.rel build/bank_stub.rel
include $(TESTKIT)/host-tests.mk
# У host-теста нет DSS/EMM: ему нужен тот же codec/state, но не target-I/O.
build/eng_pop_config.rel: $(ENGINE_DIR)/pop_config.c | $(BUILD)
@$(SDCC) $(CFLAGS) -DPOP_CONFIG_HOST_TEST -c -o $@ $<
build/eng_pop_timer.rel: $(ENGINE_DIR)/pop_timer.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_demo.rel: $(ENGINE_DIR)/pop_demo.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_cutscene.rel: $(ENGINE_DIR)/pop_cutscene.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/eng_pop_flow.rel: $(ENGINE_DIR)/pop_flow.c | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
# --- t_phys: блоб таблиц анимации ------------------------------------
# Берём дисковый ресурс из assets/packed (он в репозитории), а не выхлоп
# упаковщика из build/: тесты обязаны собираться и без assets/orig/.
# Прежнее имя kid_data.bin устарело — файл давно называется kid.ani.
KID_BIN := $(APP_DIR)/assets/packed/KID/kid.ani
build/kid_blob.c: gen_kid_blob.py $(KID_BIN) | $(BUILD)
@python3 gen_kid_blob.py $(KID_BIN) $@
build/kid_blob.rel: build/kid_blob.c kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/stubs.rel: stubs.c stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/scene.rel: scene.c scene.h stubs.h kidblob.h | $(BUILD)
@$(SDCC) $(CFLAGS) -c -o $@ $<
build/bank_stub.rel: bank_stub.s | $(BUILD)
@$(SDASZ80) -plosgff $@ $<
# Эталоны трасс: пересобрать набор, если их правили (см. шапку t_phys.c).
build/phys.rel: $(wildcard golden/*.h)
+121
View File
@@ -0,0 +1,121 @@
# 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 воспроизвести не удалось, а на уровне логики это
несколько строк: заспавнить кусок, сменить комнату, тикать до
приземления, проверить щебень.
@@ -0,0 +1,25 @@
;--------------------------------------------------------------------------
; bank_stub.s — set_bank/get_bank для прогона под ucsim.
;
; Модули движка помечены __banked (в продукте они лежат в страницах W3), и
; SDCC генерирует на такие вызовы трамплин ___sdcc_bcall_ehl из своей
; библиотеки. Трамплин обязан найти две пользовательские подпрограммы:
;
; get_bank — вернуть в A номер текущего банка, прочие регистры не трогать
; set_bank — переключить банк на A, прочие регистры не трогать
;
; В тесте банков нет: вся память плоская, все модули слинкованы вместе.
; Поэтому «банк» всегда нулевой, а переключение — пустое. Важно только
; соблюсти контракт по регистрам, иначе трамплин испортит аргументы вызова.
;--------------------------------------------------------------------------
.area _CODE
.globl set_bank
.globl get_bank
get_bank::
ld a, #0x00
ret
set_bank::
ret
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
gen_kid_blob.py — kid_data.bin как C-массив для тестов под ucsim.
Зачем. Таблицы кадров и seqtbl живут в EMM-странице, замапленной в W0, и
pop_kid ходит по ним АБСОЛЮТНЫМИ адресами от KD_DATA_OFF (0x100). В ucsim
никакого W0 нет, зато есть ровная 64-КБ память, поэтому тест просто кладёт
файл по 0x100 и глушит gfx_w0_map. Единственное следствие для сборки —
код набора линкуется с --code-loc выше конца блоба (см. Makefile).
Файл берётся ровно тот, что уходит на устройство, — pop_extract_kid_data.py
делает его из оригинального PRINCE.EXE, так что тест проверяет порт против
НАСТОЯЩИХ данных игры, а не против переписанной от руки таблицы.
"""
import sys
from pathlib import Path
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
data = src.read_bytes()
out = [
"/* Сгенерировано gen_kid_blob.py — не править руками. */",
'#include "kidblob.h"',
"",
f"const unsigned char kid_blob[{len(data)}] = {{",
]
for i in range(0, len(data), 16):
out.append(" " + ",".join(str(b) for b in data[i:i + 16]) + ",")
out += [
"};",
"",
f"const unsigned int kid_blob_size = {len(data)};",
"",
"/* Разложить блоб по KD_DATA_OFF. Без memcpy: тестовый бинарь не",
" * линкуется с libc, а тянуть ради этого z80-рантайм незачем. */",
"void kid_blob_install(void)",
"{",
" unsigned char *d = (unsigned char *)KID_BLOB_ADDR;",
" const unsigned char *s = kid_blob;",
f" unsigned int n = {len(data)};",
" while (n--) *d++ = *s++;",
"}",
"",
]
dst.write_text("\n".join(out))
print(f"gen_kid_blob: {src.name} -> {dst.name} ({len(data)} Б)")
@@ -0,0 +1,22 @@
/* Эталон трассы: crouch. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x6B, 0x81, 0x76, 0x00, 0x04, 0x01, 0x01, 0x3E, 0x06,
0x6C, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x41, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6D, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x42, 0x06,
0x6E, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4A, 0x06,
0x6F, 0x84, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4B, 0x06,
0x70, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4E, 0x06,
0x71, 0x86, 0x76, 0x00, 0x04, 0x01, 0x05, 0x4F, 0x06,
0x72, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x52, 0x06,
0x73, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x53, 0x06,
0x74, 0x87, 0x76, 0x00, 0x04, 0x01, 0x05, 0x54, 0x06,
0x75, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x57, 0x06,
0x76, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x58, 0x06,
0x77, 0x83, 0x76, 0x00, 0x04, 0x01, 0x05, 0x59, 0x06,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x83, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: fall_ledge. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x66, 0x9E, 0x79, 0x00, 0x06, 0x02, 0x03, 0x77, 0x02,
0x67, 0xA0, 0x7F, 0x00, 0x06, 0x02, 0x03, 0x7C, 0x02,
0x68, 0x9F, 0x88, 0x00, 0x06, 0x02, 0x03, 0x81, 0x02,
0x69, 0x9F, 0x94, 0x00, 0x06, 0x02, 0x03, 0x84, 0x02,
0x6A, 0x9E, 0xA6, 0x00, 0x06, 0x02, 0x04, 0xDE, 0x03,
0x6B, 0xA0, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC3, 0x06,
0x6C, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x05, 0xC6, 0x06,
0x6D, 0xA2, 0xB5, 0x00, 0x06, 0x02, 0x01, 0xC9, 0x06,
0x6E, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4A, 0x06,
0x6F, 0xA3, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4B, 0x06,
0x70, 0xA5, 0xB5, 0x00, 0x06, 0x02, 0x05, 0x4E, 0x06,
@@ -0,0 +1,22 @@
/* Эталон трассы: jump_up. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x43, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x12, 0x04,
0x44, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x13, 0x04,
0x45, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x14, 0x04,
0x46, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x15, 0x04,
0x47, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x16, 0x04,
0x48, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x17, 0x04,
0x49, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x18, 0x04,
0x4A, 0x80, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x19, 0x04,
0x4B, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1A, 0x04,
0x4C, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1B, 0x04,
0x4D, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1C, 0x04,
0x4E, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1D, 0x04,
0x4F, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x1E, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x21, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x24, 0x04,
0x4F, 0x80, 0x70, 0xFF, 0x04, 0x01, 0x01, 0x25, 0x04,
0x4F, 0x80, 0x72, 0xFF, 0x04, 0x01, 0x01, 0x28, 0x04,
0x51, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB8, 0x03,
0x52, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x01, 0xB9, 0x03,
0x53, 0x80, 0x76, 0xFF, 0x04, 0x01, 0x05, 0xBC, 0x03,
@@ -0,0 +1,36 @@
/* Эталон трассы: loose. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
0x09, 0x9E, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA0, 0x76, 0x00, 0x06, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0xA4, 0x76, 0x00, 0x06, 0x01, 0x01, 0x22, 0x00,
0x0C, 0xA9, 0x76, 0x00, 0x07, 0x01, 0x01, 0x25, 0x00,
0x0D, 0xAB, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0xAE, 0x76, 0x00, 0x07, 0x01, 0x01, 0x2D, 0x00,
0x07, 0xB2, 0x76, 0x00, 0x07, 0x01, 0x01, 0x14, 0x00,
0x08, 0xB7, 0x76, 0x00, 0x08, 0x01, 0x01, 0x17, 0x00,
0x09, 0xB8, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xBA, 0x76, 0x00, 0x08, 0x01, 0x01, 0x1F, 0x00,
@@ -0,0 +1,36 @@
/* Эталон трассы: rjump_gap3. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x08, 0x00,
0x02, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x09, 0x00,
0x03, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0A, 0x00,
0x04, 0xB8, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0B, 0x00,
0x05, 0xB0, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x0E, 0x00,
0x06, 0xAD, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x11, 0x00,
0x07, 0xAA, 0x76, 0xFF, 0x08, 0x01, 0x01, 0x14, 0x00,
0x08, 0xA5, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x17, 0x00,
0x09, 0xA4, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0xA2, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x9E, 0x76, 0xFF, 0x07, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x99, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x97, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x2A, 0x00,
0x22, 0x95, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x64, 0x01,
0x23, 0x90, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x67, 0x01,
0x24, 0x8A, 0x76, 0xFF, 0x06, 0x01, 0x01, 0x6A, 0x01,
0x25, 0x87, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x6D, 0x01,
0x26, 0x82, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x72, 0x01,
0x27, 0x7B, 0x76, 0xFF, 0x05, 0x01, 0x01, 0x75, 0x01,
0x28, 0x6F, 0x73, 0xFF, 0x04, 0x01, 0x01, 0x7A, 0x01,
0x29, 0x67, 0x6A, 0xFF, 0x03, 0x01, 0x01, 0x7F, 0x01,
0x2A, 0x5F, 0x68, 0xFF, 0x02, 0x01, 0x01, 0x84, 0x01,
0x2B, 0x5B, 0x73, 0xFF, 0x01, 0x01, 0x01, 0x89, 0x01,
0x2C, 0x57, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x8E, 0x01,
0x07, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x14, 0x00,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
0x33, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x94, 0x04,
0x34, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x95, 0x04,
0x0F, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x00, 0x35, 0x00,
0x10, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x28, 0x01,
0x11, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x29, 0x01,
0x12, 0x50, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2C, 0x01,
0x13, 0x4E, 0x76, 0xFF, 0x01, 0x01, 0x01, 0x2F, 0x01,
0x32, 0x52, 0x76, 0xFF, 0x01, 0x01, 0x05, 0x93, 0x04,
@@ -0,0 +1,26 @@
/* Эталон трассы: run_right. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x01, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x08, 0x00,
0x02, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x09, 0x00,
0x03, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x56, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x5E, 0x76, 0x00, 0x01, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x61, 0x76, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00,
0x07, 0x64, 0x76, 0x00, 0x01, 0x01, 0x01, 0x14, 0x00,
0x08, 0x69, 0x76, 0x00, 0x02, 0x01, 0x01, 0x17, 0x00,
0x09, 0x6A, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x6C, 0x76, 0x00, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x70, 0x76, 0x00, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x75, 0x76, 0x00, 0x03, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x7A, 0x76, 0x00, 0x03, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x83, 0x76, 0x00, 0x04, 0x01, 0x01, 0x17, 0x00,
0x09, 0x84, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x86, 0x76, 0x00, 0x04, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x8A, 0x76, 0x00, 0x04, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x8F, 0x76, 0x00, 0x05, 0x01, 0x01, 0x25, 0x00,
0x0D, 0x91, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2A, 0x00,
0x0E, 0x94, 0x76, 0x00, 0x05, 0x01, 0x01, 0x2D, 0x00,
0x07, 0x98, 0x76, 0x00, 0x05, 0x01, 0x01, 0x14, 0x00,
0x08, 0x9D, 0x76, 0x00, 0x06, 0x01, 0x01, 0x17, 0x00,
@@ -0,0 +1,14 @@
/* Эталон трассы: stand. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x72, 0x76, 0xFF, 0x03, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,22 @@
/* Эталон трассы: step_shift. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x79, 0x72, 0x76, 0x00, 0x03, 0x01, 0x01, 0x6F, 0x05,
0x7A, 0x73, 0x76, 0x00, 0x03, 0x01, 0x01, 0x72, 0x05,
0x7B, 0x74, 0x76, 0x00, 0x03, 0x01, 0x01, 0x75, 0x05,
0x7C, 0x77, 0x76, 0x00, 0x03, 0x01, 0x01, 0x78, 0x05,
0x7D, 0x7B, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7B, 0x05,
0x7E, 0x7E, 0x76, 0x00, 0x03, 0x01, 0x01, 0x7E, 0x05,
0x7F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x81, 0x05,
0x80, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x82, 0x05,
0x81, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x83, 0x05,
0x82, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x84, 0x05,
0x83, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x85, 0x05,
0x84, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x01, 0x86, 0x05,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
0x0F, 0x7D, 0x76, 0x00, 0x04, 0x01, 0x00, 0x35, 0x00,
@@ -0,0 +1,18 @@
/* Эталон трассы: turn. Снят record_golden.py — не править руками.
* Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */
0x2D, 0x7A, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD1, 0x01,
0x2E, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD4, 0x01,
0x2F, 0x77, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xD7, 0x01,
0x30, 0x78, 0x76, 0xFF, 0x04, 0x01, 0x07, 0xDA, 0x01,
0x01, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x08, 0x00,
0x02, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x09, 0x00,
0x03, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0A, 0x00,
0x04, 0x79, 0x76, 0xFF, 0x04, 0x01, 0x01, 0x0B, 0x00,
0x05, 0x71, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x0E, 0x00,
0x06, 0x6E, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x11, 0x00,
0x07, 0x6B, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x14, 0x00,
0x08, 0x66, 0x76, 0xFF, 0x03, 0x01, 0x01, 0x17, 0x00,
0x09, 0x65, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1C, 0x00,
0x0A, 0x63, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x1F, 0x00,
0x0B, 0x5F, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x22, 0x00,
0x0C, 0x5A, 0x76, 0xFF, 0x02, 0x01, 0x01, 0x25, 0x00,
+18
View File
@@ -0,0 +1,18 @@
/*
* kidblob.h — таблицы анимации Кида в памяти теста.
*
* KID_BLOB_ADDR обязан совпадать с KD_DATA_OFF из pop_kid.c: продукт
* адресует таблицы именно так, и в этом весь смысл — под тестом работает
* тот же код доступа, что на устройстве, просто без маппинга W0.
*/
#ifndef POP_TESTS_KIDBLOB_H
#define POP_TESTS_KIDBLOB_H
#define KID_BLOB_ADDR 0x0100
extern const unsigned char kid_blob[];
extern const unsigned int kid_blob_size;
void kid_blob_install(void);
#endif
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
record_golden.py — снять эталонные трассы для t_phys.
Зачем скрипт, а не один прогон. Отчёт tcheck ограничен 768 байтами лога, а
трасс на все сценарии сильно больше. Поэтому набор умеет снимать РОВНО
ОДИН сценарий за прогон (-DSC_RECORD=<n>), а перебор делает этот скрипт:
собирает набор N раз, каждый раз забирает из отчёта имя сценария и его
трассу и пишет golden/<имя>.h.
Эталон фиксирует ТЕКУЩЕЕ поведение. Это осознанно: набор страхует
поведение-сохраняющие правки (перенос физики Kid -> Char), а не доказывает
правильность. Правильность — сверка с SDLPoP. Поэтому переснимать эталоны
можно только вместе с внятным объяснением, что и почему поменялось.
Использование:
python3 record_golden.py # снять все сценарии
python3 record_golden.py 7 9 # только эти номера
"""
import re
import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
GOLDEN = HERE / "golden"
# Число сценариев = число TC_RUN в t_phys.c: список ведётся там, здесь его
# дублировать незачем.
NSCEN = len(re.findall(r"^\s*TC_RUN\(", (HERE / "t_phys.c").read_text(), re.M))
def record(n: int) -> str:
# make не видит смены -DSC_RECORD (в зависимостях только файлы), поэтому
# объектник набора сносим руками — иначе следующий прогон молча снимет
# трассу предыдущего сценария под новым именем.
for f in ("phys.rel", "phys.ihx"):
(HERE / "build" / f).unlink(missing_ok=True)
subprocess.run(["make", "-s", "build/phys.ihx", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, check=True)
p = subprocess.run(["make", "-s", "run-phys", f"EXTRA_CFLAGS=-DSC_RECORD={n}"],
cwd=HERE, capture_output=True, text=True)
out = p.stdout
m = re.search(r"^\s*### (\S+)\s*$", out, re.M)
if not m:
raise SystemExit(f"сценарий {n}: в отчёте нет строки '### имя'\n{out}")
name = m.group(1)
rows = re.findall(r"^\s*([0-9A-F]{18})\s*$", out, re.M)
if not rows:
raise SystemExit(f"сценарий {n} ({name}): трасса пустая\n{out}")
lines = [f"/* Эталон трассы: {name}. Снят record_golden.py — не править руками.",
" * Поля кадра: frame, x, y, dir, col, row, action, seq_lo, seq_hi. */"]
for r in rows:
lines.append(" " + ", ".join(f"0x{r[i:i+2]}" for i in range(0, 18, 2)) + ",")
GOLDEN.mkdir(exist_ok=True)
(GOLDEN / f"{name}.h").write_text("\n".join(lines) + "\n")
return f"{name}: {len(rows)} кадров"
def main() -> int:
nums = [int(a) for a in sys.argv[1:]] or list(range(1, NSCEN + 1))
for n in nums:
print(f"[{n}/{NSCEN}] " + record(n))
# Итоговая пересборка в обычном режиме, чтобы каталог не остался с
# объектниками, собранными под SC_RECORD.
subprocess.run(["make", "-s", "clean"], cwd=HERE, check=True)
print("эталоны сняты; проверить: make -C", HERE.name, "t_phys")
return 0
if __name__ == "__main__":
sys.exit(main())
+213
View File
@@ -0,0 +1,213 @@
/*
* scene.c — реализация покадрового раннера (см. scene.h).
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_geom.h"
#include "pop_guard.h"
#include "kid_data.h"
#include "scene.h"
#include "stubs.h"
#include "kidblob.h"
sc_rec sc_trace[SC_TRACE_MAX];
uint8_t sc_len;
static uint8_t sc_fg[30];
/* Соседей у сцены нет (links = 0) — по краям комнаты стена: карта сама
* решает так, когда соседняя комната не задана. */
void sc_init(void)
{
kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */
pop_kid_data_load("KID\\KID.ANI");
pop_kid_hp_newgame();
}
void sc_room(const uint8_t *fg, uint8_t room)
{
uint8_t i;
for (i = 0; i < 30; i++) { sc_fg[i] = fg[i]; tk_modif[i] = 0; }
pop_map_set(sc_fg);
pop_map_set_room(room);
/* Изолированная сцена: соседей нет. Тип TILE_WALL (20) с обеих сторон
* и сверху/снизу — тогда решения физики зависят только от sc_fg, и
* трасса не поедет, если когда-нибудь поменяется загрузка кромок. */
pop_map_set_edges(0, 0, 0, 0);
tk_start_room = room;
tk_log_clear();
}
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
/* kid_init комнату не трогает (в продукте её ставит enter_room_side), а
* сцена обязана быть согласованной: логика, спрашивающая «персонаж в
* ОТРИСОВАННОЙ комнате?» (pop_gate_over_char), иначе всегда отвечает
* «нет». Тест, которому нужна другая комната, задаёт её сам. */
Kid.room = tk_start_room;
pop_kid_hp_reset();
}
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir)
{
kid_init(2 /* seq_2_stand */, (int8_t)col, (int8_t)row, dir);
Kid.room = tk_start_room;
Kid.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */
/* Колонку пересчитываем В ОКНЕ Char: determine_col — часть физики, а та
* работает с активным персонажем (порт load_fram_det_col). */
pop_loadkid();
pop_determine_col();
pop_savekid();
pop_kid_hp_reset();
}
/* ---- слот Guard ------------------------------------------------------ */
void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp)
{
/* Поля — как в enter_guard (seg002:0112): X из колонки, Y с пола ряда. */
Guard.charid = charid;
Guard.curr_col = (int8_t)col;
Guard.curr_row = (int8_t)row;
Guard.x = (uint8_t)(pop_x_bump[col + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX);
Guard.y = (uint8_t)pop_y_land[row + 1];
Guard.direction = dir;
Guard.alive = -1;
Guard.action = 1; /* actions_1_run_jump */
Guard.sword = 2; /* у живого стража меч в руке */
Guard.fall_x = Guard.fall_y = 0;
Guard.repeat = 0;
Guard.room = 1;
guardhp_max = guardhp_curr = hp;
guardhp_delta = 0;
/* Стойка с мечом (seq_77 у неактивного стража — но нам нужна поза, из
* которой персонаж просто стоит и может упасть). */
pop_loadshad();
pop_char_set_seq(77 /* seq_77_guard_stand_inactive */);
play_seq();
pop_saveshad();
}
void sc_guard_run(uint8_t n)
{
uint8_t i;
for (i = 0; i < n; i++) {
/* play_guard_frame (seg000:1246) без ИИ: окно, кадр, seq, физика. */
pop_loadshad_and_opp();
pop_load_fram_det_col();
play_seq();
pop_saveshad_and_opp();
pop_guard_phys_tick();
}
}
void sc_trace_clear(void) { sc_len = 0; }
static void sc_record(void)
{
sc_rec *r;
if (sc_len >= SC_TRACE_MAX) return;
r = &sc_trace[sc_len++];
r->frame = Kid.frame;
r->x = Kid.x;
r->y = Kid.y;
r->dir = Kid.direction;
r->col = Kid.curr_col;
r->row = Kid.curr_row;
r->action = Kid.action;
r->seq = (uint16_t)(Kid.curr_seq - SEQTBL_BASE);
}
uint8_t sc_run(uint8_t keys, uint8_t n)
{
uint8_t i, done = 0;
for (i = 0; i < n; i++) {
tk_keys_clear();
if (keys & SC_L) tk_key_down(KBD_LEFT);
if (keys & SC_R) tk_key_down(KBD_RIGHT);
if (keys & SC_U) tk_key_down(KBD_UP);
if (keys & SC_D) tk_key_down(KBD_DOWN);
if (keys & SC_SHIFT) tk_key_down(KBD_LSHIFT);
/* Порядок ровно как в главном цикле sprpop.c (kid_heal и
* отрисовка выпадают — они ничего не решают). pop_loose_tick
* оставлен: он не рисование, а МЕХАНИКА — досчитывает тряску и
* убирает провалившуюся плиту из карты, то есть меняет то, по чему
* физика в следующем кадре принимает решения. */
pop_ctrl_tick();
kid_tick();
pop_phys_tick();
pop_loose_tick();
sc_record();
done++;
if (sc_len >= SC_TRACE_MAX) break;
}
return done;
}
/* ---- сверка с эталоном ---------------------------------------------- */
void sc_expect(const uint8_t *golden, uint8_t nrec)
{
uint8_t i;
const uint8_t *g = golden;
tc_check((int16_t)sc_len, (int16_t)nrec, "sc_len", __LINE__);
for (i = 0; i < nrec && i < sc_len; i++, g += SC_REC_BYTES) {
const sc_rec *r = &sc_trace[i];
/* Номер кадра идёт первым полем каждой проверки: в отчёте видно,
* на каком именно кадре трасса разошлась. */
tc_check((int16_t)r->frame, (int16_t)g[0], "frame", i);
tc_check((int16_t)r->x, (int16_t)g[1], "x", i);
tc_check((int16_t)r->y, (int16_t)g[2], "y", i);
tc_check((int16_t)r->dir, (int16_t)(int8_t)g[3], "dir", i);
tc_check((int16_t)r->col, (int16_t)(int8_t)g[4], "col", i);
tc_check((int16_t)r->row, (int16_t)(int8_t)g[5], "row", i);
tc_check((int16_t)r->action, (int16_t)g[6], "action", i);
tc_check((int16_t)r->seq,
(int16_t)((uint16_t)g[7] | ((uint16_t)g[8] << 8)), "seq", i);
}
}
/* ---- снятие эталона -------------------------------------------------- */
static void put(char c)
{
if (tc_result.logn < TC_LOGSZ) tc_result.log[tc_result.logn++] = (uint8_t)c;
}
static void put_byte(uint8_t v)
{
static const char hex[] = "0123456789ABCDEF";
put(hex[v >> 4]); put(hex[v & 15]);
}
void sc_dump(const char *name)
{
uint8_t i;
tc_result.magic = TC_MAGIC;
tc_result.total = 1;
tc_result.failed = 1; /* иначе раннер лог не напечатает */
put('#'); put('#'); put('#'); put(' ');
while (*name) put(*name++);
put('\n');
for (i = 0; i < sc_len; i++) {
const sc_rec *r = &sc_trace[i];
put_byte(r->frame);
put_byte(r->x);
put_byte(r->y);
put_byte((uint8_t)r->dir);
put_byte((uint8_t)r->col);
put_byte((uint8_t)r->row);
put_byte(r->action);
put_byte((uint8_t)r->seq);
put_byte((uint8_t)(r->seq >> 8));
put('\n');
}
}
+102
View File
@@ -0,0 +1,102 @@
/*
* scene.h — покадровый сценарный раннер для тестов физики Кида.
*
* Почему покадровый, а не «вызвать функцию и сверить возврат». Порт
* seg005/seg006 — это конечный автомат: диспетчер выбирает
* последовательность, интерпретатор проигрывает её кадр за кадром, физика
* между кадрами двигает персонажа и решает, не пора ли падать. Ошибка
* почти всегда проявляется не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ
* через несколько кадров. Плюс главный риск ближайшего рефакторинга
* (Kid -> Char) — не арифметика, а то, КТО владеет окном Char внутри кадра;
* поймать это можно только прогоном полного кадра, как в главном цикле:
*
* pop_ctrl_tick(); // ввод -> control(): смена последовательности
* kid_tick(); // play_seq: следующий кадр
* pop_phys_tick(); // падение/приземление/стена
*
* Отсюда формат: сцена (комната + стартовая поза) + скрипт ввода, на
* выходе — трасса состояния Кида по кадрам. Сравнение трассы с эталоном
* и есть тест.
*/
#ifndef POP_TESTS_SCENE_H
#define POP_TESTS_SCENE_H
#include <stdint.h>
/* Одна запись трассы: то, чем оперируют решения движка. curr_seq —
* СМЕЩЕНИЕ от SEQTBL_BASE (адрес зависит от базы, смещение — нет). */
typedef struct {
uint8_t frame;
uint8_t x, y;
int8_t dir;
int8_t col, row;
uint8_t action;
uint16_t seq;
} sc_rec;
#define SC_TRACE_MAX 48
extern sc_rec sc_trace[SC_TRACE_MAX];
extern uint8_t sc_len;
/* Кнопки скрипта ввода — битовая маска на кадр. */
#define SC_L 0x01
#define SC_R 0x02
#define SC_U 0x04
#define SC_D 0x08
#define SC_SHIFT 0x10
/* Инициализация окружения набора: таблицы анимации, обнуление состояния.
* Звать один раз в начале прогона. */
void sc_init(void);
/* Задать комнату: fg[30] — коды тайлов по рядам (row*10+col), room —
* номер (нужен trob'ам). Модификаторы обнуляются; ставить их тест может
* сам через tk_modif. Соседи комнаты — стены (изолированная сцена). */
void sc_room(const uint8_t *fg, uint8_t room);
/* Поставить Кида: колонка/ряд задают позицию по центру тайла, как это
* делает do_startpos (seg003) — так сцена описывается в терминах карты, а
* не в пикселях. */
void sc_kid_at(uint8_t col, uint8_t row, int8_t dir);
/* То же, но с точным X: тайл задаёт ряд и пол под ногами, X — фазу внутри
* тайла (нужно, когда исход зависит от того, где именно Кид стоит). */
void sc_kid_at_x(uint8_t col, uint8_t row, uint8_t x, int8_t dir);
/* ---- Персонажи слота Guard (страж/скелет/тень/визирь) ---------------- *
* Физика у них ОБЩАЯ с Кидом (одни функции над Char, разные окна), поэтому
* тестировать её надо на обоих слотах. Поднимаем персонажа руками, как
* enter_guard (seg002:0112): данных уровня под тестом нет.
* charid — CHARID_2_GUARD / CHARID_1_SHADOW / CHARID_4_SKELETON;
* hp — сколько HP выдать (guardhp_max = guardhp_curr = hp). */
void sc_guard_at(uint8_t charid, uint8_t col, uint8_t row, int8_t dir, uint8_t hp);
/* Прогнать n кадров ПЕРСОНАЖА СЛОТА GUARD — порт play_guard_frame без ИИ:
* окно loadshad → load_fram_det_col → play_seq → saveshad, затем
* pop_guard_phys_tick(). ИИ (guards.c, банк 1) в набор не линкуется: он
* решает «куда идти», а тут проверяется «что с ним делает физика». */
void sc_guard_run(uint8_t n);
/* Прогнать n кадров с ЗАЖАТОЙ маской keys, записывая трассу.
* Возвращает число записанных кадров (обрезается по SC_TRACE_MAX). */
uint8_t sc_run(uint8_t keys, uint8_t n);
/* Сбросить трассу (перед новым сценарием). */
void sc_trace_clear(void);
/* Сверить трассу с эталоном: golden — плоский массив по 8 байт на кадр
* (frame,x,y,dir,col,row,action,seq_lo,seq_hi — 9), nrec записей.
* Расхождения уходят в отчёт tcheck с номером кадра и поля. */
void sc_expect(const uint8_t *golden, uint8_t nrec);
#define SC_REC_BYTES 9
/* Снятие эталона. Лог tcheck всего 768 байт — на все сценарии сразу трасс
* не хватит, поэтому за прогон снимается РОВНО ОДИН сценарий (его номер
* задаёт -DSC_RECORD=<n>), а перебором занимается record_golden.py.
* Формат: строка «### имя», дальше по строке на кадр, по два хекс-символа
* на байт. Раннер печатает лог только при провале — sc_dump ставит
* failed=1 намеренно. */
void sc_dump(const char *name);
#endif
+413
View File
@@ -0,0 +1,413 @@
/*
* stubs.c — окружение движка для прогона под ucsim_z80.
*
* Под тестом крутится НАСТОЯЩАЯ физика (pop_map) с настоящим
* интерпретатором последовательностей (pop_kid): это единственный способ
* проверить порт seg005/seg006, потому что вся его суть — в точных
* величинах кадров и seqtbl. А вот всё, что вокруг, в ucsim не существует:
* видеопамять, EMM-страницы, файлы, клавиатура. Здесь они заглушены.
*
* Три категории:
* 1. libc/libbgi (графика, память, файлы, клавиатура) — пустышки. Код
* отрисовки из pop_kid.c линкуется, но не вызывается.
* 2. Соседние модули движка (pop_bg/pop_trob/pop_level/pop_redraw) —
* минимальные модели: room_modif живёт здесь массивом, пометки
* перерисовки и падающие куски ЖУРНАЛИРУЮТСЯ (тест может проверить,
* что физика их дёрнула), уровень отдаёт стартовую комнату.
* 3. Состояние стража — просто данные: страж в тестах физики не тикает,
* но pop_map/pop_ctrl на его поля смотрят.
*
* Клавиатура сделана НЕ пустышкой: `tk_keys` — набор «нажатых» скан-кодов,
* и `kbd_raw_down` отвечает по нему. Так под тест попадают и read_input, и
* read_user_control, то есть весь путь ввода, а не только диспетчер.
*/
#include <stdint.h>
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include "pop_qsave.h" /* из src/: ENGINE_DIR стоит в -I */
/* Сериализаторы приватного состояния входят в те же крупные модули, что и
* физика/ИИ. Сами QuickSave-тесты работают на target с EMM; обычным
* ucsim-сценам нужны только символы, и вызывать их они не должны. */
void pop_qs_bytes(pop_qs_io_t *io, void *p, uint16_t n)
{ (void)io; (void)p; (void)n; }
void pop_qs_u8(pop_qs_io_t *io, uint8_t *p) { (void)io; (void)p; }
void pop_qs_i8(pop_qs_io_t *io, int8_t *p) { (void)io; (void)p; }
void pop_qs_u16(pop_qs_io_t *io, uint16_t *p) { (void)io; (void)p; }
void pop_qs_i16(pop_qs_io_t *io, int *p) { (void)io; (void)p; }
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_bg.h"
#include "pop_map.h"
#include "pop_trob.h"
#include "pop_level.h"
#include "pop_redraw.h"
#include "pop_pace.h"
#include "pop_sfx.h"
#include "_pop_draw.h"
#include "pop_cdraw.h"
#include "stubs.h"
#include "kidblob.h"
/* ---- 1. libc / libbgi ---------------------------------------------- */
uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;
/* ISR-стаб W0-страницы: продукт берёт его АДРЕС и вписывает в страницу.
* Здесь достаточно, чтобы символ существовал. */
void _gfx_w0_isr(void) { }
void gfx_w0_map(uint8_t page) { (void)page; }
void gfx_w0_unmap(void) { }
void gfx_w0_page_prepare(uint8_t page) { (void)page; }
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{ (void)x; (void)y; (void)img; (void)flip; }
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
int skip, int rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
int skip, int rows, uint8_t maxw)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; (void)maxw; }
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
uint8_t skip, uint8_t rows)
{ (void)x; (void)y; (void)img; (void)flip; (void)skip; (void)rows; }
int atlas_load(atlas_t *a, const char *path) { (void)a; (void)path; return -1; }
void atlas_free(atlas_t *a) { (void)a; }
const void *atlas_image(const atlas_t *a, uint8_t idx)
{ (void)a; (void)idx; return 0; }
/* EMM: страниц нет, но блок обязан выделиться — иначе pop_kid_data_load
* сдастся на первом же шаге и молча оставит kdat_ok = 0, а play_seq тогда
* просто ничего не делает (и трасса выходит из одного застывшего кадра). */
uint8_t mem_alloc_pages_bios(uint8_t n) { (void)n; return 1; }
void mem_free_block_bios(uint8_t blk) { (void)blk; }
uint8_t mem_get_page_bios(uint8_t blk, uint8_t idx) { (void)blk; (void)idx; return 1; }
/* Файловая система: единственный «файл» — таблицы анимации. Сделано не
* пустышкой, а рабочим чтением, чтобы pop_kid_data_load отработал целиком
* (включая правку ISR-стаба страницы) и сам взвёл свой kdat_ok — тест не
* лезет во внутренние статики продукта. Данные, по которым потом ходят
* аксессоры, кладёт kid_blob_install по KID_BLOB_ADDR: на устройстве туда
* же их приводит маппинг W0. */
int open(const char *p, int f) { (void)p; (void)f; return 3; }
int read(int fd, void *b, int n)
{
unsigned char *d = (unsigned char *)b;
const unsigned char *s = kid_blob;
unsigned int i, cnt = kid_blob_size;
if (fd != 3) return -1;
if ((unsigned int)n < cnt) cnt = (unsigned int)n;
for (i = 0; i < cnt; i++) d[i] = s[i];
return (int)cnt;
}
int close(int fd) { (void)fd; return 0; }
int bank_load_file(uint8_t page, uint16_t off, const char *path, uint16_t max)
{
(void)page; (void)off; (void)path;
return kid_blob_size <= max ? (int)kid_blob_size : (int)max;
}
/* ---- клавиатура: управляемая тестом --------------------------------- */
uint16_t tk_keys[TK_KEYS_MAX];
int kbd_raw_open(void) { return 0; }
void kbd_raw_close(void) { }
void kbd_raw_sync(void) { }
uint8_t kbd_raw_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == code) return 1;
return 0;
}
uint8_t kbd_raw_any_down(void)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++) if (tk_keys[i]) return 1;
return 0;
}
void tk_keys_clear(void)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++) tk_keys[i] = 0;
}
void tk_key_down(uint16_t code)
{
uint8_t i;
for (i = 0; i < TK_KEYS_MAX; i++)
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
}
/* Темп кадра и звук относятся к окружению реальной машины. Физика и
* управление только оставляют выборки луча и заявки эффектов; host-тесты
* проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */
void pop_beam_sample(void) __preserves_regs(b, c, d, e) { }
void pop_sfx_play(uint8_t id) { (void)id; }
/* Курсор насоса: в тестах звука нет вовсе, значит «ничего не звучит» — и
* автомат вступительной темы первого уровня сразу отпускает управление. */
volatile uint16_t pop_mus_left;
void pop_sfx_stop(void) { }
/* ---- 2. соседние модули движка -------------------------------------- */
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
* зелья (тип). Физика читает и пишет его напрямую через pop_trob_modif. */
uint8_t tk_modif[30];
uint8_t tk_start_room;
uint8_t *pop_trob_modif(uint8_t room) __banked { (void)room; return tk_modif; }
uint8_t pop_level_start_room(void) __banked { return tk_start_room; }
/* Журнал: что физика попросила у соседей. Тест сверяет ФАКТ и параметры
* (какой тайл помечен, где заспавнен кусок) — заглушка-пустышка этого не
* дала бы, а именно такие баги (BUG-GATE-ANIM-1) мы и ловим. */
tk_log_t tk_log;
static void log_put(uint8_t kind, uint8_t a, uint8_t b, uint8_t c)
{
if (tk_log.n >= TK_LOG_MAX) { tk_log.overflow = 1; return; }
tk_log.e[tk_log.n].kind = kind;
tk_log.e[tk_log.n].a = a;
tk_log.e[tk_log.n].b = b;
tk_log.e[tk_log.n].c = c;
tk_log.n++;
}
void tk_log_clear(void) { tk_log.n = 0; tk_log.overflow = 0; }
uint8_t tk_log_count(uint8_t kind)
{
uint8_t i, c = 0;
for (i = 0; i < tk_log.n; i++) if (tk_log.e[i].kind == kind) c++;
return c;
}
void pop_add_trob(uint8_t room, uint8_t tilepos, int8_t type) __banked
{ log_put(TK_TROB, room, tilepos, (uint8_t)type); }
/* Чомперы будит смена ряда (SEQ_UP/SEQ_DOWN, падение, приземление) — в
* хост-тестах физики их состояние не проверяется, достаточно не падать. */
void pop_start_chompers(uint8_t room, int8_t row) __banked
{ (void)room; (void)row; }
void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier,
uint8_t snd) __banked
{ (void)modifier; (void)snd; log_put(TK_BUTTON, room, tilepos, button_type); }
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
{ log_put(TK_REDRAW, tilepos, kind, pages); }
void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) __banked
{ log_put(TK_REDRAW_ABOVE, col, kind, pages); }
void pop_loose_mob_spawn(int row, int col) __banked
{ log_put(TK_MOB_SPAWN, (uint8_t)row, (uint8_t)col, 0); }
void pop_loose_mob_tick(void) __banked { }
void pop_loose_mob_room_changed(void) __banked { }
uint8_t pop_loose_mob_pos(int *col, int *y) __banked
{ (void)col; (void)y; return 0; }
/* Отрисовочные хвосты, которые физика дёргает напрямую. */
void pop_fore_set_clip(int x, int y, int w, int h)
{ (void)x; (void)y; (void)w; (void)h; }
void pop_clip_sprite(int x, int w, int sy_top, int h) __banked
{ (void)x; (void)w; (void)sy_top; (void)h; }
void pop_heal_fast(int x, int y, int w, int h)
{ (void)x; (void)y; (void)w; (void)h; }
/* ---- 3. страж ------------------------------------------------------- *
* Само состояние (Guard, guardhp_*, can_guard_see_kid, is_guard_notice…) и
* pop_take_hp даёт НАСТОЯЩИЙ pop_guard.c — он линкуется в набор. Здесь
* только то, что он тянет из уровня: данных уровня под тестом нет, страж
* поднимается вручную (tk_guard_spawn в scene.c). */
uint8_t pop_current_level; /* pop_level.c под тест не линкуется; sc_init ставит 1 */
/* Цвет стража — подмена палитры, живёт в pop_cdraw (отрисовка, в набор не
* линкуется). Здесь важен только факт вызова из pop_guard_enter. */
void pop_guard_set_palette(uint8_t color) __banked { (void)color; }
/* Габарит последнего нарисованного кадра по слотам (pop_cdraw.c): физика
* читает из него char_width_half. Под тестом никто не рисует, поэтому все
* поля остаются нулями — ровно как было до DRAW-CHAR, когда kid_fp_width()
* возвращал 0. Тест, которому ширина важна, выставляет её сам. */
pop_cdraw_t pop_cd[POP_CH_N];
/* Связи комнат: нужны check_guard_fallout (возрождение скелета в комнате
* снизу). Тест задаёт их через tk_link_down. */
uint8_t tk_link_down;
uint8_t pop_room_link(uint8_t room, uint8_t side)
{ (void)room; return side == 3 ? tk_link_down : 0; }
int8_t pop_level_guard_type(uint8_t level) __banked { (void)level; return 0; }
uint8_t pop_level_guard_hp(uint8_t level) __banked { (void)level; return 3; }
/* Тайлы «уровня» для соседних комнат: pop_map_set_edges забирает fg соседей
* слева/справа сам (кэш на входе в комнату), значит тесту нужен источник.
* Заполняет сам тест — по умолчанию комнат нет и всё стена. */
uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = НОМЕР комнаты (1..TK_ROOMS) */
void pop_level_access_begin(void) { }
void pop_level_access_end(void) { }
uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
{
if (room == 0 || room > TK_ROOMS || tilepos >= 30) return 20; /* стена */
return tk_room_fg[room][tilepos];
}
/* pop_level_tile (то же самое, но с маппингом окна W0 внутри) — её зовёт
* спецсобытие подъёма скелета из guards.c; тому единственному набору, где
* guards.c линкуется (t_mouse), достаточно чтения из tk_room_fg. */
uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
{
return pop_level_tile_raw(room, tilepos);
}
/* Запись в «данные уровня» — у теста это тот же tk_room_fg. Нужна с тех
* пор, как do_pickup правит страницу уровня сразу (BUG-POTION-STRIPE). */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{
if (room == 0 || room > TK_ROOMS || tilepos >= 30) return;
tk_room_fg[room][tilepos] = tile;
}
/* Живое состояние стражей по комнатам — как gstate в pop_level.c, только на
* 8 комнат и без данных уровня: тесту нужно ровно то, на что смотрит
* follow_guard (есть ли в целевой комнате свой страж и не труп ли он).
* tk_guard_room != 0 — комната, в которой «по данным уровня» страж есть. */
uint8_t tk_guard_room;
uint8_t tk_gs_tile[TK_ROOMS]; /* 30 = стража нет (как в pop_level) */
uint16_t tk_gs_seq[TK_ROOMS]; /* != 0 — запомнен ТРУП */
void tk_guard_state_clear(void)
{
uint8_t i;
for (i = 0; i < TK_ROOMS; i++) { tk_gs_tile[i] = 30; tk_gs_seq[i] = 0; }
tk_guard_room = 0;
}
/* ВНИМАНИЕ на форму возврата: результат собирается в res и отдаётся ОДНИМ
* выходом. С `return 1;` прямо в ветке SDCC 4.5 не кладёт единицу в A —
* функция возвращает мусор (docs/bugs/sdcc-z80-ret-const-lost). Без
* __banked баг маскировался: в A случайно оставался ненулевой байт от
* `*tile = 11`, и проверка «истина» проходила. */
uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
uint8_t *color, uint8_t *skill) __banked
{
uint8_t res = 0;
if (room < 1 || room > TK_ROOMS) return 0;
if (tk_gs_tile[room - 1] < 30) { /* запомненный (в т.ч. труп) */
*tile = tk_gs_tile[room - 1]; *dir = 0; *color = 0; *skill = 1;
res = 1;
} else if (room == tk_guard_room) { /* страж из данных уровня */
*tile = 11; *dir = 0; *color = 0; *skill = 1;
res = 1;
}
return res;
}
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
uint8_t skill, uint16_t seq, uint8_t dead)
{
(void)dir; (void)x; (void)skill;
if (room < 1 || room > TK_ROOMS) return;
tk_gs_tile[room - 1] = tile;
tk_gs_seq[room - 1] = dead ? seq : 0;
if (tile >= 30) tk_guard_room = (tk_guard_room == room) ? 0 : tk_guard_room;
}
uint16_t pop_guard_state_seq(uint8_t room)
{ return (room >= 1 && room <= TK_ROOMS) ? tk_gs_seq[room - 1] : 0; }
uint8_t pop_guard_state_x(uint8_t room) { (void)room; return 0; }
/* Таблицы деления на ширину тайла — настоящие (резидент pop_tile.c в тест
* не линкуется). Не заглушка: физика считает по ним колонку, подменять
* нельзя. Значения сгенерированы той же формулой, что в pop_tile.c. */
const int8_t POP_TILE_DIV[256] = {
-5, -5, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4,
-3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7,
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9,
9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14,
};
const uint8_t POP_TILE_MOD[256] = {
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9,10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10,11,12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,
12,13, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13, 0, 1,
};
/* Тайлсет (0 подземелье, 1 дворец) — в приложении его ставит pop_bg_load;
* тесты гоняют физику подземелья, поэтому 0. Не заглушка-функция, а та же
* переменная, что и в резиденте: карта читает её напрямую. */
uint8_t pop_palace;
/* Отражение в зеркале (уровень 4) — чистая графика, в тестах физики её нет.
* pop_map зовёт pop_mirror_draw из pop_check_mirror, поэтому символ нужен. */
void pop_mirror_draw(int clip_top) __banked { (void)clip_top; }
void pop_mirror_heal(void) __banked { }
/* ---- Зеркальный набор персонажей (зелье инверсии) -------------------- *
* В host-тестах графики нет: pop_kboot линкуется ради pop_kid_data_load, а
* его хвост (загрузка *_v.atl) заглушен — проверять там нечего, атласов в
* тесте не существует вовсе. */
uint8_t pop_vflip_ready(void) { return 1; } /* «уже загружено» — грузить не станет */
int pop_vflip_add(const void *orig, const char *path)
{ (void)orig; (void)path; return 0; }
int pop_vflip_take(const void *orig, const void *mirror)
{ (void)orig; (void)mirror; return 0; }
/* Архив ресурсов: на устройстве из него читаются атласы Кида, в тестах
* графики нет вовсе — pop_kboot линкуется только ради pop_kid_data_load,
* а тот по-прежнему ходит в файл (заглушка open/read выше). */
signed char pop_arc_open(void *a, const char *path, unsigned char *tbl,
unsigned char max) __banked
{ (void)a; (void)path; (void)tbl; (void)max; return -1; }
signed char pop_arc_open_id(void *a, unsigned char id, unsigned char *tbl,
unsigned char max) __banked
{ (void)a; (void)id; (void)tbl; (void)max; return -1; }
signed char pop_arc_atlas(void *a, const unsigned char *tbl,
unsigned char idx, void *at) __banked
{ (void)a; (void)tbl; (void)idx; (void)at; return -1; }
void pop_arc_close(void *a) __banked { (void)a; }
uint8_t pop_vflip_page(uint8_t src) { return src; }
void pop_vflip_reset(void) { }
int pop_sword_take(const atlas_t *a) __banked { (void)a; return 0; }
int pop_sword_vflip_take(const atlas_t *m) __banked { (void)m; return 0; }
int pop_guard_vflip_load(void) __banked { return 0; }
+61
View File
@@ -0,0 +1,61 @@
/*
* stubs.h — интерфейс тестового окружения (см. stubs.c).
*
* Что тест может задавать: «нажатые» клавиши (tk_key_down) и модификаторы
* тайлов комнаты (tk_modif). Что может проверять: журнал обращений
* физики к соседним модулям (tk_log).
*/
#ifndef POP_TESTS_STUBS_H
#define POP_TESTS_STUBS_H
#include <stdint.h>
/* ---- клавиатура ------------------------------------------------------ */
#define TK_KEYS_MAX 6
extern uint16_t tk_keys[TK_KEYS_MAX];
void tk_keys_clear(void);
void tk_key_down(uint16_t code);
/* ---- комната --------------------------------------------------------- */
extern uint8_t tk_modif[30]; /* room_modif текущей комнаты */
extern uint8_t tk_start_room; /* что вернёт pop_level_start_room */
/* ---- журнал обращений ------------------------------------------------ */
#define TK_LOG_MAX 24
enum {
TK_TROB = 1, /* a=room b=tilepos c=type */
TK_BUTTON, /* a=room b=tilepos c=type */
TK_REDRAW, /* a=tilepos b=kind c=pages */
TK_REDRAW_ABOVE, /* a=col b=kind c=pages */
TK_MOB_SPAWN /* a=row b=col */
};
typedef struct { uint8_t kind, a, b, c; } tk_log_e;
typedef struct { uint8_t n, overflow; tk_log_e e[TK_LOG_MAX]; } tk_log_t;
extern tk_log_t tk_log;
void tk_log_clear(void);
/* Сколько записей данного вида в журнале (0 = не было). */
uint8_t tk_log_count(uint8_t kind);
/* Состояние стражей по комнатам для follow_guard (см. stubs.c). */
/* Комнат в таблице «уровня». 24 = столько же, сколько в настоящем уровне
* PoP: спецсобытия поздних уровней адресуют комнаты по их РЕАЛЬНЫМ номерам
* (меч тени лежит в 15-й, уход за ним — из 18-й), и обрезать их до восьми
* значило бы тестировать не тот адрес. Цена — 720 байт тестового ОЗУ. */
#define TK_ROOMS 24
extern uint8_t tk_guard_room; /* комната, где страж есть «по данным уровня» */
extern uint8_t tk_gs_tile[TK_ROOMS];
extern uint16_t tk_gs_seq[TK_ROOMS];
void tk_guard_state_clear(void);
/* Связь «вниз» для check_guard_fallout (скелет возрождается в комнате 3). */
extern uint8_t tk_link_down;
/* fg комнат «уровня» — источник для кэша соседей в pop_map_set_edges. */
extern uint8_t tk_room_fg[TK_ROOMS + 1][30]; /* индекс = номер комнаты */
#endif
+135
View File
@@ -0,0 +1,135 @@
/* pop_app — протокол верхнего автомата: допустимые и запрещённые переходы. */
#include "tcheck.h"
#include "pop_app.h"
TC_TEST(boot_to_title)
{
pop_app_reset();
TC_EQ(pop_app_state, POP_APP_BOOT);
TC_TRUE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
TC_EQ(pop_app_state, POP_APP_BUILD_INFO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_BUILD_DONE));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(level_load_roundtrip)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESUME));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_FINISHED));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
}
TC_TEST(rejects_wrong_event)
{
pop_app_reset();
TC_FALSE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_BOOT);
TC_TRUE(pop_app_dispatch(POP_APP_EV_QUIT));
TC_EQ(pop_app_state, POP_APP_QUIT);
TC_FALSE(pop_app_dispatch(POP_APP_EV_BOOT_READY));
}
TC_TEST(future_routes_are_declared)
{
pop_app_reset();
pop_app_state = POP_APP_TITLE;
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
TC_TRUE(pop_app_dispatch(POP_APP_EV_LEVEL_READY));
TC_EQ(pop_app_state, POP_APP_PLAYING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_TIME_EXPIRED));
TC_EQ(pop_app_state, POP_APP_TIME_EXPIRED);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(prelevel_cutscene_route)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_STARTED));
TC_EQ(pop_app_state, POP_APP_CUTSCENE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(demo_routes)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_INTRO_STARTED));
TC_EQ(pop_app_state, POP_APP_INTRO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_CUTSCENE_FINISHED));
TC_EQ(pop_app_state, POP_APP_DEMO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
TC_TRUE(pop_app_dispatch(POP_APP_EV_DEMO_STARTED));
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(intro_skip_starts_new_game)
{
pop_app_reset();
pop_app_dispatch(POP_APP_EV_BOOT_READY);
pop_app_dispatch(POP_APP_EV_BUILD_DONE);
pop_app_dispatch(POP_APP_EV_INTRO_STARTED);
TC_EQ(pop_app_state, POP_APP_INTRO);
TC_TRUE(pop_app_dispatch(POP_APP_EV_NEW_GAME));
TC_EQ(pop_app_state, POP_APP_LEVEL_LOAD);
}
TC_TEST(restart_game_returns_to_intro)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_PAUSE));
TC_EQ(pop_app_state, POP_APP_PAUSE_MENU);
TC_TRUE(pop_app_dispatch(POP_APP_EV_RESTART_INTRO));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
/* Финал уходит в таблицу рекордов: она же дослушивает победную тему и
* гасит экран, как хвост end_sequence (seg001:5F1). */
TC_TEST(ending_to_title_route)
{
pop_app_state = POP_APP_PLAYING;
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_STARTED));
TC_EQ(pop_app_state, POP_APP_ENDING);
TC_TRUE(pop_app_dispatch(POP_APP_EV_ENDING_FINISHED));
TC_EQ(pop_app_state, POP_APP_HALL_OF_FAME);
TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
TC_TEST(hof_route_still_exits_to_title)
{
pop_app_state = POP_APP_HALL_OF_FAME;
TC_TRUE(pop_app_dispatch(POP_APP_EV_HOF_FINISHED));
TC_EQ(pop_app_state, POP_APP_TITLE);
}
void main(void)
{
TC_RUN(boot_to_title);
TC_RUN(level_load_roundtrip);
TC_RUN(rejects_wrong_event);
TC_RUN(future_routes_are_declared);
TC_RUN(prelevel_cutscene_route);
TC_RUN(demo_routes);
TC_RUN(intro_skip_starts_new_game);
TC_RUN(restart_game_returns_to_intro);
TC_RUN(ending_to_title_route);
TC_RUN(hof_route_still_exits_to_title);
}
+197
View File
@@ -0,0 +1,197 @@
/*
* t_cfg — полный codec POP.CFG: заголовок, размер, checksum и payload.
*
* DSS/EMM под host не живут, но pure codec лежит в том же pop_config.c, что
* и target-I/O. Поэтому проверяем весь формат, а не только payload.
*/
#include <stdint.h>
#include <string.h>
#include "tcheck.h"
#include "pop_settings.h"
#include "pop_pace.h" /* POP_SPEED_MODES */
#include "_pop_config.h"
/* Runtime-владельцы нужны только для линковки pop_settings_apply(); codec
* их не вызывает. */
uint8_t pop_speed_mode, pop_cheats, pop_snd_want, pop_mus_want;
void pop_sfx_toggle(void) __banked { pop_snd_want = (uint8_t)!pop_snd_want; }
static void set_all(uint8_t snd, uint8_t spd, uint8_t prof, uint8_t cheats,
uint8_t info, uint16_t flags, uint8_t time_limit,
uint8_t debug_bar)
{
pop_settings.sound_enabled = snd;
pop_settings.speed_mode = spd;
pop_settings.gameplay_profile = prof;
pop_settings.cheats_enabled = cheats;
pop_settings.show_build_info = info;
pop_settings.enhancement_flags = flags;
pop_settings.time_limit_enabled = time_limit;
pop_settings.show_debug_bar = debug_bar;
}
/* ser→parse должен возвращать ровно те же значения (кроме профиля:
* ENHANCED осознанно садится в VANILLA). */
TC_TEST(cfg_roundtrip)
{
uint8_t buf[POP_CFG_SIZE];
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 1, 1, 0xBEEF, 0, 0);
pop_cfg_encode(buf);
memset(&pop_settings, 0, sizeof(pop_settings)); /* стереть */
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
TC_EQ(pop_settings.sound_enabled, 1);
TC_EQ(pop_settings.speed_mode, POP_SPEED_FAST);
TC_EQ(pop_settings.cheats_enabled, 1);
TC_EQ(pop_settings.show_build_info, 1);
TC_EQ(pop_settings.enhancement_flags, 0xBEEF);
TC_EQ(pop_settings.time_limit_enabled, 0);
TC_EQ(pop_settings.show_debug_bar, 0);
}
/* ENHANCED из файла применяется как VANILLA (режим ещё не реализован). */
TC_TEST(cfg_enhanced_downgrades_to_vanilla)
{
uint8_t buf[POP_CFG_SIZE];
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_ENHANCED, 0, 0, 0x0003, 1, 1);
pop_cfg_encode(buf);
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
TC_EQ(pop_settings.gameplay_profile, POP_PROFILE_VANILLA);
/* Остальные поля при этом применяются. */
TC_EQ(pop_settings.enhancement_flags, 0x0003);
}
/* Мусор в ЛЮБОМ поле => отказ без частичного применения. */
TC_TEST(cfg_garbage_rejected_whole)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t i;
for (i = 0; i < POP_CFG_PAYLOAD_SIZE; i++) {
set_all(1, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 1, 0, 0x0001, 1, 1);
pop_cfg_encode(buf);
switch (i) {
case 0: buf[7 + i] = 2; break; /* sound > 1 */
case 1: buf[7 + i] = (uint8_t)POP_SPEED_MODES; break;
case 2: buf[7 + i] = 2; break; /* profile > ENHANCED */
case 3: buf[7 + i] = 2; break; /* cheats > 1 */
case 4: buf[7 + i] = 2; break; /* buildinfo > 1 */
case 7: buf[7 + i] = 2; break; /* timer > 1 */
case 8: buf[7 + i] = 2; break; /* debug bar > 1 */
default: buf[7 + i] = 0xFF; break; /* флаги — любые */
}
/* Изменённый payload обязан иметь корректную checksum: иначе мы
* тестируем checksum, а не диапазон поля. */
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0;
{
uint8_t j;
for (j = 0; j < POP_CFG_PAYLOAD_SIZE; j++)
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= buf[7 + j];
}
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] =
(uint8_t)~buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE];
if (i == 5 || i == 6) { /* flags не валидируются */
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, sizeof(buf)));
continue;
}
memset(&pop_settings, 0, sizeof(pop_settings));
TC_FALSE(pop_cfg_decode(buf, sizeof(buf)));
/* Настройки не тронуты — нули после memset остались нулями. */
TC_EQ(pop_settings.sound_enabled, 0);
TC_EQ(pop_settings.speed_mode, 0);
}
}
TC_TEST(cfg_header_checksum_and_future_tail)
{
uint8_t buf[POP_CFG_MAX_SIZE];
uint8_t i, sum;
set_all(1, POP_SPEED_FAST, POP_PROFILE_VANILLA, 0, 1, 0xA55A, 1, 1);
pop_cfg_encode(buf);
buf[0] = 'X';
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
buf[4] = 2;
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] ^= 1;
TC_FALSE(pop_cfg_decode(buf, POP_CFG_SIZE));
pop_cfg_encode(buf);
TC_FALSE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE - 1)));
/* Будущий v1: два незнакомых байта перед checksum должны пропуститься. */
pop_cfg_encode(buf);
buf[5] = POP_CFG_PAYLOAD_SIZE + 2;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE] = 0x12;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 1] = 0x34;
sum = 0;
for (i = 0; i < POP_CFG_PAYLOAD_SIZE + 2; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 2] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_PAYLOAD_SIZE + 3] = (uint8_t)~sum;
memset(&pop_settings, 0, sizeof(pop_settings));
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_SIZE + 2)));
TC_EQ(pop_settings.enhancement_flags, 0xA55A);
}
/* Созданный до FG4 файл (семь payload-байтов) остаётся валиден и обязан
* включить лимит: это исторический дефолт 60 минут, не значение в _DATA. */
TC_TEST(cfg_legacy_enables_time_limit)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t sum = 0, i;
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 0, 0);
pop_cfg_encode(buf);
buf[5] = POP_CFG_LEGACY_PAYLOAD_SIZE;
for (i = 0; i < POP_CFG_LEGACY_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_LEGACY_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
pop_settings.time_limit_enabled = 0;
TC_TRUE(pop_cfg_decode(buf, POP_CFG_MIN_SIZE));
TC_EQ(pop_settings.time_limit_enabled, 1);
}
/* Файл, записанный ДО тумблера отладочной полосы (восемь payload-байтов),
* обязан остаться валидным и включить полосу: это её исторический дефолт. */
TC_TEST(cfg_legacy_enables_debug_bar)
{
uint8_t buf[POP_CFG_SIZE];
uint8_t sum = 0, i;
set_all(1, POP_SPEED_NORMAL, POP_PROFILE_VANILLA, 1, 0, 0, 1, 0);
pop_cfg_encode(buf);
buf[5] = POP_CFG_TIMELIMIT_PAYLOAD_SIZE;
for (i = 0; i < POP_CFG_TIMELIMIT_PAYLOAD_SIZE; i++) sum ^= buf[7 + i];
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE] = sum;
buf[POP_CFG_HEADER_SIZE + POP_CFG_TIMELIMIT_PAYLOAD_SIZE + 1] = (uint8_t)~sum;
pop_settings.show_debug_bar = 0;
TC_TRUE(pop_cfg_decode(buf, (uint16_t)(POP_CFG_HEADER_SIZE +
POP_CFG_TIMELIMIT_PAYLOAD_SIZE +
POP_CFG_TAIL_SIZE)));
TC_EQ(pop_settings.show_debug_bar, 1);
TC_EQ(pop_settings.time_limit_enabled, 1);
}
TC_TEST(settings_apply_syncs_runtime)
{
set_all(0, POP_SPEED_FASTEST, POP_PROFILE_VANILLA, 0, 0, 0, 1, 1);
pop_snd_want = 1;
pop_settings_apply(1);
TC_EQ(pop_speed_mode, POP_SPEED_FASTEST);
TC_EQ(pop_cheats, 0);
TC_EQ(pop_snd_want, 0);
}
void main(void)
{
TC_RUN(cfg_roundtrip);
TC_RUN(cfg_enhanced_downgrades_to_vanilla);
TC_RUN(cfg_garbage_rejected_whole);
TC_RUN(cfg_header_checksum_and_future_tail);
TC_RUN(cfg_legacy_enables_time_limit);
TC_RUN(cfg_legacy_enables_debug_bar);
TC_RUN(settings_apply_syncs_runtime);
}
+525
View File
@@ -0,0 +1,525 @@
/*
* t_char — физика над Char для РАЗНЫХ ПЕРСОНАЖЕЙ.
*
* Зачем отдельный набор. После GUARD-PHYS физика — один набор функций над
* глобалом `Char`, а кто именно в Char, решают окна loadkid/savekid и
* loadshad/saveshad (как в оригинале, seg006:809). В слоте Guard живёт не
* только страж: тем же кодом ходят скелет (ур. 3), тень (4/5/6/12), визирь
* (13) и мышь (8) — `autocontrol_opponent` (seg002:628) разводит их только
* по ИИ, а play_guard_frame у всех общий. Значит расхождения «у Кида так,
* у прочих иначе» — это ВЕТКИ ПО charid внутри общих функций, и проверять
* их надо именно так: одна сцена, разные персонажи.
*
* t_phys страхует, что перенос ничего не сломал КИДУ (характеризационные
* трассы). Здесь — противоположное: что не-Кид получает СВОЮ ветку.
*
* Проверки точечные (значение поля после N кадров), а не трассы: ветки
* различаются исходом, а не траекторией, и эталон тут только запутал бы.
*/
#include <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "scene.h"
#include "stubs.h"
#define E 0 /* empty */
#define F 1 /* floor */
#define G 4 /* gate (portcullis) */
#define W 20 /* wall */
/* Ровный пол на ряду 1 (как room_flat в t_phys). */
static const uint8_t room_flat[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
};
/* Пол ряда 0 обрывается после колонки 3; ряд 2 — пол. Персонаж, сошедший
* с кромки (0,3), падает ДВА ряда: ровно та высота, на которой ветки
* land() по charid расходятся (seg005:189). */
static const uint8_t room_drop2[30] = {
W, F, F, F, E, E, E, E, E, E,
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, F,
};
/* Решётка ворот в (2,6) — сцена под окклюзию персонажа, стоящего в проёме. */
static const uint8_t room_gate[30] = {
W, E, E, E, E, E, E, E, E, E,
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, G, F, F, F,
};
/* ---- take_hp: кому достаётся урон ----------------------------------- */
TC_TEST(char_take_hp_goes_to_active)
{
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
/* Окно Кида: урон обязан уйти в hitp_delta и не тронуть стража. */
pop_loadkid();
hitp_delta = 0; guardhp_delta = 0;
tc_check(pop_take_hp(1), 0, "kid: не последнее HP", __LINE__);
tc_check(hitp_delta, -1, "kid: hitp_delta", __LINE__);
tc_check(guardhp_delta, 0, "kid: страж не тронут", __LINE__);
pop_savekid();
/* Окно стража: тот же вызов — уже в guardhp_delta. */
pop_loadshad();
hitp_delta = 0; guardhp_delta = 0;
tc_check(pop_take_hp(1), 0, "guard: не последнее HP", __LINE__);
tc_check(guardhp_delta, -1, "guard: guardhp_delta", __LINE__);
tc_check(hitp_delta, 0, "guard: Кид не тронут", __LINE__);
/* Смертельный удар: возврат 1 и дельта на всё оставшееся. */
guardhp_delta = 0;
tc_check(pop_take_hp(100), 1, "guard: смертельный удар", __LINE__);
tc_check(guardhp_delta, -3, "guard: дельта на все HP", __LINE__);
pop_saveshad();
}
/* Уровень 2 чита гасит только мелкий урон Киду. Мгновенная смерть
* (count=100) проходит всегда, а на стража чит не действует вовсе. */
TC_TEST(char_immortal_level2_covers_kid_only)
{
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
pop_immortal = 2;
pop_loadkid();
hitp_delta = 0;
tc_check(pop_take_hp(1), 0, "kid: мелкий урон погашен", __LINE__);
tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__);
tc_check(pop_take_hp(100), 1, "kid: мгновенная смерть проходит", __LINE__);
tc_check(hitp_delta, -3, "kid: смертельная дельта", __LINE__);
pop_savekid();
pop_loadshad();
guardhp_delta = 0;
tc_check(pop_take_hp(100), 1, "guard: чит на него не действует", __LINE__);
pop_saveshad();
pop_immortal = 0;
}
/* ---- do_delta_hp: дельты применяются каждому своему счётчику --------- */
TC_TEST(char_delta_hp_applies_per_character)
{
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
hitp_delta = -1;
guardhp_delta = -2;
pop_kid_hurt = 0;
pop_guard_hurt = 0;
pop_do_delta_hp();
tc_check(hitp_curr, 2, "kid: HP 3-1", __LINE__);
tc_check(guardhp_curr, 1, "guard: HP 3-2", __LINE__);
/* Флаги «брызг» — по одному на персонажа, и только на УРОН. */
tc_check(pop_kid_hurt, 1, "kid: брызги", __LINE__);
tc_check(pop_guard_hurt, 1, "guard: брызги", __LINE__);
/* Плюсовая дельта (зелье) брызг и вспышки давать НЕ должна. */
pop_kid_hurt = 0;
hitp_delta = 1;
pop_do_delta_hp();
tc_check(hitp_curr, 3, "kid: зелье вернуло HP", __LINE__);
tc_check(pop_kid_hurt, 0, "kid: зелье — не урон", __LINE__);
}
/* ---- land(): ветки по charid (seg005:173) --------------------------- */
/* Кид с двух рядов теряет 1 HP и остаётся жив. */
TC_TEST(char_kid_survives_two_row_fall)
{
sc_room(room_drop2, 1);
sc_kid_at(3, 0, 0);
sc_trace_clear();
sc_run(SC_R, 30); /* шагнуть с кромки и упасть */
pop_do_delta_hp();
tc_check(Kid.curr_row, 2, "kid: приземлился на ряд 2", __LINE__);
tc_check(hitp_curr, 2, "kid: −1 HP за два ряда", __LINE__);
tc_check(pop_kid_dead, 0, "kid: жив", __LINE__);
}
/* СТРАЖ с той же высоты гибнет: seg005:190 уводит charid_2_guard прямо в
* take_hp(100). Это не «баланс», а буквальная ветка оригинала. */
TC_TEST(char_guard_dies_from_two_row_fall)
{
sc_room(room_drop2, 1);
sc_kid_at(1, 2, 0); /* Кид в стороне, не мешает */
/* Ставим сразу НАД ПУСТОТОЙ: ИИ в наборе нет, сам с кромки страж не
* сойдёт, а физике довольно того, что под ним не пол. Колонка 5, а не
* 4: решение принимается по ВЕСОВОЙ ТОЧКЕ кадра (dx_weight, seg006),
* а она у стойки стража уходит на 9 px назад — с колонки 4 точка
* попадала бы ещё на пол колонки 3. */
sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3);
sc_guard_run(30);
pop_do_delta_hp();
tc_check(Guard.curr_row, 2, "guard: приземлился на ряд 2", __LINE__);
tc_check(guardhp_curr, 0, "guard: два ряда = смерть", __LINE__);
}
/* ТЕНЬ (charid_1_shadow) с тех же двух рядов не теряет ничего: seg005:189
* уводит её в ветку «упал с одного ряда». */
TC_TEST(char_shadow_survives_two_row_fall)
{
sc_room(room_drop2, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_1_SHADOW, 5, 0, 0, 3);
sc_guard_run(30);
pop_do_delta_hp();
tc_check(Guard.curr_row, 2, "shadow: приземлилась на ряд 2", __LINE__);
tc_check(guardhp_curr, 3, "shadow: HP не тронуто", __LINE__);
}
/* ---- физика слота Guard вообще работает ----------------------------- */
/* До GUARD-PHYS у стража не было ни fall_accel, ни check_action: сойдя с
* кромки, он просто висел в воздухе. Тест ловит именно это. */
TC_TEST(char_guard_falls_at_all)
{
uint8_t y0;
sc_room(room_drop2, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3);
y0 = Guard.y;
sc_guard_run(6);
tc_check(Guard.y > y0, 1, "guard: пошёл вниз", __LINE__);
}
/* Гард по X (seg000:1254): за пределами комнаты физика молчит — именно так
* оригинал не выпускает стража из комнаты. */
TC_TEST(char_guard_physics_gated_by_x)
{
uint8_t y0;
sc_room(room_drop2, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3);
Guard.x = 220; /* правее 211 — вне комнаты */
y0 = Guard.y;
sc_guard_run(6);
tc_check(Guard.y, y0, "guard вне комнаты: физика не трогает", __LINE__);
}
/* check_guard_fallout (seg002:0241): провалившегося ниже комнаты убирают
* совсем — в комнату снизу он не переходит. */
TC_TEST(char_guard_fallout_removes_him)
{
sc_room(room_drop2, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_2_GUARD, 5, 0, 0, 3);
Guard.y = 211;
pop_guard_fallout();
tc_check(Guard.charid, 0, "guard: убран из комнаты", __LINE__);
tc_check(guardhp_curr, 0, "guard: HP обнулены", __LINE__);
}
/* ---- окна Char не путают персонажей ---------------------------------- */
/* Регресс на класс ошибок, ради которого затевался перенос: физика Кида не
* должна трогать стража, и наоборот. Гоняем оба слота в одной сцене. */
TC_TEST(char_windows_do_not_leak)
{
uint8_t gx, gy;
sc_room(room_flat, 1);
sc_kid_at(3, 1, 0);
sc_guard_at(CHARID_2_GUARD, 7, 1, -1, 3);
gx = Guard.x; gy = Guard.y;
sc_run(SC_R, 10); /* Кид бежит вправо */
tc_check(Guard.x, gx, "страж не поехал за Кидом", __LINE__);
tc_check(Guard.y, gy, "страж остался на своей высоте", __LINE__);
tc_check(Guard.charid, CHARID_2_GUARD, "страж не подменён Кидом", __LINE__);
tc_check(Kid.charid, CHARID_0_KID, "Кид не подменён стражем", __LINE__);
}
/* ---- follow_guard: страж идёт ЗА КИДОМ вбок (seg002:0340/039E) ------- *
* Своим ходом персонаж слота Guard комнату не меняет, но когда её меняет
* КИД, exit_room может перенести стража следом. Здесь проверяются все
* условия отбора — они и есть механика. Порт зовётся из enter_room, а
* тестируем прямо pop_guard_follow: сцена комнат в наборе одна. */
/* Базовый случай: бой, Кид ушёл ВЛЕВО, страж прижат к левому краю (x < 91),
* в комнате 2 своего стража нет → идёт следом, X переносится через шов. */
TC_TEST(char_follow_guard_left)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3);
Guard.x = 80; /* < 91: у левого края */
Guard.room = 3;
tc_check(pop_guard_follow(2, 0), 1, "страж пошёл следом", __LINE__);
tc_check(Guard.room, 2, "страж в новой комнате", __LINE__);
tc_check(Guard.x, 80 + 140, "X перенесён через шов (+140)", __LINE__);
/* follow_guard стирает guards_tile ОБЕИХ комнат — иначе страж
* раздвоится: старую поднимут при возврате, новую при входе. */
tc_check(tk_gs_tile[3 - 1], 30, "в старой комнате стража нет", __LINE__);
tc_check(tk_gs_tile[2 - 1], 30, "в новой комнате нет второго", __LINE__);
}
/* Тот же бой, но страж ДАЛЕКО от края (x >= 91) — остаётся. */
TC_TEST(char_follow_guard_needs_edge)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 5, 1, -1, 3);
Guard.x = 120; /* >= 91 */
Guard.room = 3;
tc_check(pop_guard_follow(2, 0), 0, "далеко от края — остаётся", __LINE__);
tc_check(Guard.room, 3, "комната не менялась", __LINE__);
}
/* Порог для ухода ВПРАВО — свой (x >= 165). */
TC_TEST(char_follow_guard_right_threshold)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(8, 1, 0);
sc_guard_at(CHARID_2_GUARD, 7, 1, 0, 3);
Guard.room = 3;
Guard.x = 160; /* < 165 — не идёт */
tc_check(pop_guard_follow(4, 1), 0, "x=160: остаётся", __LINE__);
Guard.x = 170; /* >= 165 — идёт */
tc_check(pop_guard_follow(4, 1), 1, "x=170: идёт следом", __LINE__);
tc_check(Guard.x, 170 - 140, "X перенесён через шов (−140)", __LINE__);
}
/* Не бой — не идёт. Меч в ножнах = страж Кида не преследует. */
TC_TEST(char_follow_guard_needs_sword_drawn)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3);
Guard.x = 80;
Guard.room = 3;
Guard.sword = 0; /* sword_0_sheathed */
tc_check(pop_guard_follow(2, 0), 0, "без меча — остаётся", __LINE__);
}
/* Мёртвый (и умирающий) не идёт никуда. */
TC_TEST(char_follow_guard_dead_stays)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3);
Guard.x = 80;
Guard.room = 3;
Guard.alive = 0; /* alive >= 0 — умирает/мёртв */
tc_check(pop_guard_follow(2, 0), 0, "мёртвый остаётся", __LINE__);
}
/* Вверх и вниз оригинал не пускает никогда (условия leave всегда истинны). */
TC_TEST(char_follow_guard_never_vertical)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3);
Guard.x = 80;
Guard.room = 3;
tc_check(pop_guard_follow(2, 2), 0, "вверх — не идёт", __LINE__);
tc_check(pop_guard_follow(2, 3), 0, "вниз — не идёт", __LINE__);
tc_check(Guard.room, 3, "комната не менялась", __LINE__);
}
/* В целевой комнате СВОЙ живой страж — перехода нет (иначе их станет два). */
TC_TEST(char_follow_guard_blocked_by_own_guard)
{
tk_guard_state_clear();
sc_room(room_flat, 3);
sc_kid_at(1, 1, -1);
sc_guard_at(CHARID_2_GUARD, 2, 1, -1, 3);
Guard.x = 80;
Guard.room = 3;
tk_guard_room = 2; /* в комнате 2 есть свой страж */
tc_check(pop_guard_follow(2, 0), 0, "чужая комната занята", __LINE__);
tc_check(Guard.room, 3, "остался у себя", __LINE__);
/* А если там ТРУП (запомнена последовательность) — идти можно. */
tk_gs_tile[2 - 1] = 11;
tk_gs_seq[2 - 1] = 0x1234;
tc_check(pop_guard_follow(2, 0), 1, "к трупу — можно", __LINE__);
}
/* Тайлы СОСЕДНИХ комнат: колонка персонажа считается из x и на диапазоне
* x 0..255 лежит в −5..13, то есть регулярно уходит за комнату глубже кромки.
* Там get_tile обязан отдавать настоящий тайл соседа — мнимая стена стоила
* BUG-SWORD-GHOST-1 (страж после follow_guard оказывался в col 12, луч
* видимости упирался в неё, и Кид посреди боя прятал меч, а достать обратно
* уже не мог: draw_sword требует can_guard_see_kid >= 2). */
static void char_tiles_resolve_across_rooms(void)
{
static const uint8_t floor30[30] = {
E,E,E,E,E,E,E,E,E,E,
E,E,E,E,E,E,E,E,E,E,
F,F,F,F,F,F,F,F,F,F,
};
uint8_t i;
/* Комнаты-соседи «уровня»: карта заберёт их fg сама (pop_map_set_edges). */
for (i = 0; i < 30; i++) { tk_room_fg[4][i] = F; tk_room_fg[6][i] = F; }
tk_room_fg[4][10 + 5] = W; /* сосед слева: ряд 1, col 5 */
tk_room_fg[6][10 + 2] = W; /* сосед справа: ряд 1, col 2 */
sc_room(floor30, 5);
pop_map_set_edges(4, 6, 0, 0);
tc_check(pop_tile_at(-1, 1), F, "col -1 = col9 соседа слева", __LINE__);
tc_check(pop_tile_at(-5, 1), W, "col -5 = col5 соседа слева", __LINE__);
tc_check(pop_tile_at(10, 1), F, "col 10 = col0 соседа справа", __LINE__);
tc_check(pop_tile_at(12, 1), W, "col 12 = col2 соседа справа", __LINE__);
tc_check(pop_tile_at(13, 1), F, "col 13 = col3 соседа справа", __LINE__);
/* Дальше комнаты соседа оригинал ушёл бы ещё через roomlink; у нас
* кэш на одну комнату в каждую сторону, и это край — стена. */
tc_check(pop_tile_at(20, 1), W, "за комнатой соседа — стена", __LINE__);
tc_check(pop_tile_at(-11, 1), W, "и слева тоже", __LINE__);
/* Соседа нет (край уровня) — стена в обе стороны, как раньше. */
pop_map_set_edges(0, 0, 0, 0);
tc_check(pop_tile_at(12, 1), W, "без соседа справа — стена", __LINE__);
tc_check(pop_tile_at(-1, 1), W, "без соседа слева — стена", __LINE__);
}
/* ---- скелет: падение его не убивает (check_guard_fallout, seg002:252) --- */
/* Если под комнатой лежит комната 3, упавший скелет ВОЗРОЖДАЕТСЯ там же —
* это единственный способ «убрать» его с экрана, убить его нельзя. */
TC_TEST(char_skeleton_reappears_below)
{
sc_room(room_flat, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3);
Guard.room = 5;
Guard.y = 220; /* ниже нижней границы комнаты (211) */
tk_link_down = 3; /* под комнатой 5 — комната 3 */
pop_guard_fallout();
tc_check(Guard.charid, CHARID_4_SKELETON, "скелет остался в слоте", __LINE__);
tc_check(Guard.room, 3, "перенесён в комнату 3", __LINE__);
tc_check(Guard.x, 133, "и на свою позицию", __LINE__);
tc_check(Guard.curr_row, 1, "ряд 1", __LINE__);
tc_check(Guard.alive, -1, "жив", __LINE__);
}
/* А если снизу НЕ комната 3 — обычная судьба выпавшего: слот освобождается. */
TC_TEST(char_skeleton_falls_out_elsewhere)
{
sc_room(room_flat, 1);
sc_kid_at(1, 2, 0);
sc_guard_at(CHARID_4_SKELETON, 5, 1, -1, 3);
Guard.room = 5;
Guard.y = 220;
tk_link_down = 7; /* снизу другая комната */
pop_guard_fallout();
tc_check(Guard.charid, 0, "скелет убран, как обычный выпавший", __LINE__);
}
/* ---- Окклюзия воротами (GATE-FORE-KID) ------------------------------ *
* Персонаж, стоящий В ПРОЁМЕ ворот, обязан уходить ЗА решётку: оригинал
* кладёт её бары в foretable (draw_tile_fore, seg008:0D15). Решение —
* pop_gate_over_char; сама отрисовка живёт в слое фона и в харнесс не
* линкуется, поэтому проверяем именно РЕШЕНИЕ. */
TC_TEST(char_gate_covers_char_standing_in_it)
{
sc_room(room_gate, 1);
sc_kid_at(6, 2, 0); /* ровно на тайле ворот */
pop_loadkid();
TC_TRUE(pop_gate_over_char(&Char));
}
TC_TEST(char_gate_ignores_neighbour_tiles)
{
sc_room(room_gate, 1);
sc_kid_at(5, 2, 0); /* колонка слева от ворот */
pop_loadkid();
TC_EQ(pop_gate_over_char(&Char), 0);
sc_kid_at(7, 2, 0); /* и справа */
pop_loadkid();
TC_EQ(pop_gate_over_char(&Char), 0);
}
TC_TEST(char_gate_ignores_other_room)
{
/* Персонаж той же клетки, но ДРУГОЙ комнаты (страж, оставшийся в
* соседней): его решётка текущей комнаты не касается. */
sc_room(room_gate, 1);
sc_kid_at(6, 2, 0);
pop_loadkid();
Char.room = 5;
TC_EQ(pop_gate_over_char(&Char), 0);
}
TC_TEST(char_gate_never_in_last_column)
{
/* Колонка 9: бары легли бы в колонку 10, то есть в СОСЕДНЮЮ комнату —
* у оригинала это отсекает `Kid.room != room_R`. */
static const uint8_t room_gate9[30] = {
W, E, E, E, E, E, E, E, E, E,
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, G,
};
sc_room(room_gate9, 1);
sc_kid_at(9, 2, 0);
pop_loadkid();
TC_EQ(pop_gate_over_char(&Char), 0);
}
void main(void)
{
sc_init();
TC_RUN(char_take_hp_goes_to_active);
TC_RUN(char_immortal_level2_covers_kid_only);
TC_RUN(char_delta_hp_applies_per_character);
TC_RUN(char_kid_survives_two_row_fall);
TC_RUN(char_guard_dies_from_two_row_fall);
TC_RUN(char_shadow_survives_two_row_fall);
TC_RUN(char_guard_falls_at_all);
TC_RUN(char_guard_physics_gated_by_x);
TC_RUN(char_guard_fallout_removes_him);
TC_RUN(char_windows_do_not_leak);
TC_RUN(char_follow_guard_left);
TC_RUN(char_follow_guard_needs_edge);
TC_RUN(char_follow_guard_right_threshold);
TC_RUN(char_follow_guard_needs_sword_drawn);
TC_RUN(char_follow_guard_dead_stays);
TC_RUN(char_follow_guard_never_vertical);
TC_RUN(char_follow_guard_blocked_by_own_guard);
TC_RUN(char_tiles_resolve_across_rooms);
TC_RUN(char_skeleton_reappears_below);
TC_RUN(char_skeleton_falls_out_elsewhere);
TC_RUN(char_gate_covers_char_standing_in_it);
TC_RUN(char_gate_ignores_neighbour_tiles);
TC_RUN(char_gate_ignores_other_room);
TC_RUN(char_gate_never_in_last_column);
}
@@ -0,0 +1,46 @@
/* pop_cutscene — порядок событий, точная семантика WAIT и прерывание. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_cutscene.h"
static const pop_cs_cmd_t script[] = {
{ POP_CS_PALETTE, 1 }, { POP_CS_SHOW, 7 }, { POP_CS_WAIT, 2 },
{ POP_CS_FLASH, 3 }, { POP_CS_WAIT, 1 }, { POP_CS_FADE_OUT, 0 },
{ POP_CS_END, 0 }
};
TC_TEST(script_events_and_waits)
{
pop_cutscene_t cs;
uint16_t arg;
pop_cutscene_begin(&cs, script);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_PALETTE); TC_EQ(arg, 1);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_SHOW); TC_EQ(arg, 7);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FLASH); TC_EQ(arg, 3);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_NONE);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_FADE_OUT);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
}
TC_TEST(skip_aborts_at_any_wait)
{
pop_cutscene_t cs;
uint16_t arg;
pop_cutscene_begin(&cs, script);
pop_cutscene_tick(&cs, &arg);
pop_cutscene_tick(&cs, &arg);
pop_cutscene_tick(&cs, &arg); /* WAIT(2), ещё есть один кадр */
pop_cutscene_abort(&cs);
TC_EQ(pop_cutscene_tick(&cs, &arg), POP_CS_EVENT_END);
TC_EQ(cs.ended, 1);
}
void main(void)
{
TC_RUN(script_events_and_waits);
TC_RUN(skip_aborts_at_any_wait);
}
+89
View File
@@ -0,0 +1,89 @@
/* pop_demo — запись level 0: смена команд и прерывание любым вводом. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_demo.h"
static int8_t moves[255];
static uint8_t moves_n;
static uint8_t any_key;
static uint8_t fight_ai;
uint8_t hitp_curr;
uint8_t pop_demo_kid_ai(void) __banked
{
return fight_ai;
}
void pop_ctrl_auto_move(int8_t move) __banked
{
moves[moves_n++] = move;
}
uint8_t kbd_raw_any_down(void)
{
return any_key;
}
TC_TEST(replays_sdlpop_move_table)
{
uint8_t done = 0;
moves_n = 0;
fight_ai = 0;
hitp_curr = 4;
pop_demo_begin();
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 1);
TC_EQ(moves[0], 0); /* t=1, запись {0, nothing} */
while (moves_n < 14) {
TC_FALSE(pop_demo_tick());
}
TC_EQ(moves[1], 1); /* t=2, запись {1, forward} */
TC_EQ(moves[11], 1); /* t=12 всё ещё forward */
TC_EQ(moves[12], 0); /* t=13, запись {0x0D, nothing} */
while (!done) done = pop_demo_tick();
TC_EQ(moves_n, 254); /* SDLPoP останавливает demo_time на FE */
TC_EQ(moves[253], -1); /* хвост после записи {0xE9, -1} */
}
TC_TEST(fight_ai_pauses_scripted_moves)
{
moves_n = 0;
fight_ai = 1;
hitp_curr = 4;
pop_demo_begin();
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 0); /* во время боя demo_time не идёт */
fight_ai = 0;
TC_FALSE(pop_demo_tick());
TC_EQ(moves_n, 1);
TC_EQ(moves[0], 0); /* первый scripted тик не потерян */
}
TC_TEST(death_stops_demo_before_combat_ai)
{
moves_n = 0;
fight_ai = 1;
hitp_curr = 0;
pop_demo_begin();
TC_TRUE(pop_demo_tick());
TC_EQ(moves_n, 0);
}
TC_TEST(any_raw_key_interrupts_demo)
{
any_key = 0;
TC_FALSE(pop_demo_user_input());
any_key = 1;
TC_TRUE(pop_demo_user_input());
}
void main(void)
{
TC_RUN(replays_sdlpop_move_table);
TC_RUN(fight_ai_pauses_scripted_moves);
TC_RUN(death_stops_demo_before_combat_ai);
TC_RUN(any_raw_key_interrupts_demo);
}
+52
View File
@@ -0,0 +1,52 @@
/* pop_flow — только канонические pre-level маршруты и ветка scene 12. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_flow.h"
TC_TEST(only_original_levels_have_pre_scenes)
{
uint8_t level;
for (level = 0; level <= 14; level++) {
pop_pre_cutscene_t scene = pop_flow_pre_cutscene(level, 60);
if (level == 2 || level == 6) TC_EQ(scene, POP_PRE_2_6);
else if (level == 4) TC_EQ(scene, POP_PRE_4);
else if (level == 8) TC_EQ(scene, POP_PRE_8);
else if (level == 9) TC_EQ(scene, POP_PRE_9);
else if (level == 12) TC_EQ(scene, POP_PRE_12);
else TC_EQ(scene, POP_PRE_NONE);
}
TC_EQ(pop_flow_pre_cutscene(15, 60), POP_PRE_NONE);
}
TC_TEST(scene_12_short_time_boundary)
{
TC_EQ(pop_flow_pre_cutscene(12, 6), POP_PRE_12);
TC_EQ(pop_flow_pre_cutscene(12, 5), POP_PRE_12_SHORT);
TC_EQ(pop_flow_pre_cutscene(12, 0), POP_PRE_12_SHORT);
}
TC_TEST(level_14_is_ending_without_number)
{
const pop_level_flow_t *flow = pop_flow_level(14);
TC_TRUE(flow != 0);
TC_FALSE(flow->show_level_number);
TC_TRUE(flow->ending_rule);
TC_TRUE(pop_flow_level(15) == 0);
}
TC_TEST(ending_room_is_exact)
{
TC_TRUE(pop_flow_is_ending(14, 5));
TC_FALSE(pop_flow_is_ending(14, 4));
TC_FALSE(pop_flow_is_ending(13, 5));
TC_FALSE(pop_flow_is_ending(15, 5));
}
void main(void)
{
TC_RUN(only_original_levels_have_pre_scenes);
TC_RUN(scene_12_short_time_boundary);
TC_RUN(level_14_is_ending_without_number);
TC_RUN(ending_room_is_exact);
}
+202
View File
@@ -0,0 +1,202 @@
/*
* t_gate.c — ОТКРЫТЫЕ ворота не должны быть «стеной» для осторожного шага.
*
* Наблюдение (пользователь, 2026-08-24): перед ОТКРЫТОЙ решёткой Кид,
* идущий слева направо, делает лишний мелкий шаг — чтобы подойти к краю,
* Shift+вперёд надо нажимать дважды.
*
* Механика: длину осторожного шага выбирает сам safe_step (seg005:0604) по
* числу, которое вернул get_edge_distance — seq 29..42 это «шаг ровно на
* distance пикселей». А get_edge_distance для тайла-стены зовёт
* dist_from_wall_forward, и та у ОРИГИНАЛА первой строкой отвечает −1, если
* тайл — ворота, сквозь которые можно пройти (seg004:0A18:
* `if (tiletype == tiles_4_gate && !can_bump_into_gate()) return -1`).
* Минус единица уводит разбор дальше, где открытые ворота опознаются как
* обычный ПОЛ: distance = 11, полный шаг. Без этой ветки расстояние
* считается до створки, тип края — EDGE_WALL, и шаг выходит коротким.
*
* Проверяем не поведение через кадры, а сам ответ get_edge_distance: он и
* есть источник длины шага, и утверждение «открытая решётка == обычный
* пол» переживёт любую перенастройку последовательностей.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_cdraw.h" /* pop_cd[].fph — высоту кадра ставит отрисовка */
#define F 1 /* floor */
#define G 4 /* gate */
#define W 20 /* wall */
/* Ряд 0: пол до колонки 4, решётка в колонке 5, пол дальше — «коридор с
* решёткой» уровня 1 (комната 5) в чистом виде. */
static const uint8_t room_gate[30] = {
F, F, F, F, F, G, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Тот же ряд, но вместо решётки обычный пол — эталон «свободного» края. */
static const uint8_t room_floor[30] = {
F, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Кид на колонке 4 лицом вправо, решётка (или пол) — следующим тайлом.
* modif — «открытость» ворот: 188 = подняты (проход открыт), 0 = опущены.
* Возврат — distance, тип края остаётся в pop_edge_type(). */
static int edge_at_gate_x(const uint8_t *fg, uint8_t modif, uint8_t x)
{
int d;
sc_room(fg, 1);
tk_modif[5] = modif;
if (x) sc_kid_at_x(4, 0, x, 0 /* лицом вправо */);
else sc_kid_at(4, 0, 0);
/* «Открыты ли ворота» решается сравнением подъёма створки с ВЫСОТОЙ
* КАДРА персонажа (can_bump_into_gate, seg004:0373). В продукте её
* пишет отрисовка (pop_cdraw), которой в наборе нет, — значит задаём
* сами. Порог важен только тем, что 6 (опущенная створка) под
* высотой кадра, а 53 (поднятая) над ней. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41; /* габарит стойки, снят в MAME */
pop_loadkid();
d = pop_edge_distance();
pop_savekid();
return d;
}
static int edge_at_gate(const uint8_t *fg, uint8_t modif)
{
return edge_at_gate_x(fg, modif, 0);
}
/* ОТКРЫТАЯ решётка — не препятствие: и расстояние, и тип края обязаны
* совпасть с обычным полом, иначе осторожный шаг окажется коротким. */
TC_TEST(open_gate_is_not_a_wall)
{
int d_floor, d_gate;
uint8_t t_floor, t_gate;
d_floor = edge_at_gate(room_floor, 0);
t_floor = pop_edge_type();
d_gate = edge_at_gate(room_gate, 188);
t_gate = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ЗАКРЫТАЯ решётка обязана остаться стеной — иначе фикс выше открыл бы
* проход сквозь опущенные ворота. */
/* ЗАКРЫТАЯ решётка обязана остаться стеной: фикс выше не должен открывать
* проход сквозь опущенные ворота. X берём у самой створки — в центре тайла
* она ещё «далеко» и честно классифицируется как пол. */
TC_TEST(closed_gate_is_a_wall)
{
int d = edge_at_gate_x(room_gate, 0, 136);
TC_EQ(pop_edge_type(), EDGE_WALL);
TC_EQ(d < 11, 1);
}
/* ...а ОТКРЫТАЯ на том же X ведёт себя как чистый пол — это и есть длина
* осторожного шага: 11, а не обрезок до створки. */
TC_TEST(open_gate_steps_like_floor)
{
int d_gate, d_floor;
uint8_t t_gate, t_floor;
d_gate = edge_at_gate_x(room_gate, 188, 136);
t_gate = pop_edge_type();
d_floor = edge_at_gate_x(room_floor, 0, 136);
t_floor = pop_edge_type();
TC_EQ(d_gate, d_floor);
TC_EQ(t_gate, t_floor);
}
/* ---- check_gate_push (seg004:0833) --------------------------------- *
* Кид стоит в тайле решётки, решётка опускается — оригинал ВЫТАЛКИВАЕТ его
* из тайла (иначе Кид «в суперпозиции»: створка рисуется то перед ним, то
* за ним, и уйти он может в обе стороны). Прогоняем кадр физики и смотрим,
* сдвинулся ли X. */
static int8_t push_col; /* curr_col, с которым сработал толчок */
static int push_from_dir(uint8_t x, uint8_t modif, int8_t dir)
{
sc_room(room_gate, 1);
tk_modif[5] = modif;
/* Габарит ПОСЛЕДНЕГО НАРИСОВАННОГО кадра — его в продукте пишет
* отрисовка (pop_cdraw), которой в наборе нет. Значения сняты с живой
* сцены в MAME для стойки Кида: ширина 12, высота 41. */
pop_cd[POP_CH_KID].fpw = 12;
pop_cd[POP_CH_KID].fph = 41;
sc_kid_at_x(5, 0, x, dir);
sc_run(0, 1); /* кадр «стоим» — флаги перекрытия за прошлый */
push_col = Kid.curr_col;
sc_run(0, 1); /* ...и за этот: check_gate_push смотрит оба */
return (int)Kid.x - (int)x;
}
static int push_from(uint8_t x, uint8_t modif)
{
return push_from_dir(x, modif, 0);
}
/* СТОРОНА толчка — формула оригинала `Char.x += 5 - (orig_col <= tile_col) * 10`:
* пока тайл ПЕРСОНАЖА и есть решётка, его выталкивает ВЛЕВО (−5); как только
* персонаж отнесён к следующей колонке (решётка стала левым соседом) — ВПРАВО
* (+5). Граница между сторонами, стало быть, ровно там, где меняется
* curr_col, и проверяется здесь на обоих направлениях взгляда.
*
* Решётка — колонка 5, значит push_col == 5 обязан давать 5, push_col == 6
* (+5), а вне зоны касания створки толчка нет вовсе. */
static uint8_t check_side(int8_t dir, uint8_t *nleft, uint8_t *nright)
{
uint8_t x, bad = 0;
for (x = 130; x <= 162; x++) {
int dx = push_from_dir(x, 0, dir);
if (dx == 0) continue;
/* Величина — кратная 5: пока персонаж остаётся в зоне створки,
* толчок повторяется каждый кадр (так же и в оригинале), поэтому
* проверяем СТОРОНУ и шаг, а не одно конкретное число. */
if (push_col == 5) { if (dx >= 0 || dx % 5) bad++; else (*nleft)++; }
else if (push_col == 6) { if (dx <= 0 || dx % 5) bad++; else (*nright)++; }
else bad++; /* толчок вне тайлов решётки — ошибка */
}
return bad;
}
TC_TEST(closing_gate_pushes_kid)
{
uint8_t nleft = 0, nright = 0, bad;
bad = check_side(0, &nleft, &nright); /* лицом вправо */
bad += check_side(-1, &nleft, &nright); /* лицом влево */
TC_EQ(bad, 0);
TC_EQ(nleft > 0, 1); /* обе стороны реально есть */
TC_EQ(nright > 0, 1);
}
/* ОТКРЫТУЮ решётку выталкивание не трогает вовсе (can_bump_into_gate). */
TC_TEST(open_gate_does_not_push)
{
uint8_t x, moved = 0;
for (x = 130; x <= 162; x++)
if (push_from_dir(x, 188, 0) != 0) moved++;
TC_EQ(moved, 0);
}
int main(void)
{
sc_init();
TC_RUN(open_gate_is_not_a_wall);
TC_RUN(closed_gate_is_a_wall);
TC_RUN(open_gate_steps_like_floor);
TC_RUN(closing_gate_pushes_kid);
TC_RUN(open_gate_does_not_push);
return 0;
}
+184
View File
@@ -0,0 +1,184 @@
/*
* t_geom.c — тесты pop_geom.c (геометрия комнаты + PRNG оригинала).
*
* Модуль выбран первым, потому что не тянет за собой ничего: ни графики,
* ни данных уровня, ни клавиатуры — только свой заголовок.
*
* Главный тест здесь — geom_lcg_matches_reference. В pop_geom.c LCG
* оригинала (s = s*214013 + 2531011) написан на ассемблере по схеме
* Горнера ради обхода __mullong, и заявка «бит-в-бит как в SDLPoP» до сих
* пор держалась только на комментарии. Тест сверяет рукописный asm с
* наивной 32-битной формулой на каждом шаге — и по возвращаемому значению,
* и по обеим половинам сида.
*/
#include "tcheck.h"
#include "pop_geom.h"
/* ---- геометрия ------------------------------------------------------ */
TC_TEST(geom_xbump_layout)
{
/* Шаг колонок равномерный и равен TILE_SIZEX — на этом стоит вся
* арифметика колонок (x_bump[col + FIRST_ONSCREEN_COLUMN]). */
uint8_t i;
for (i = 1; i < 20; i++)
TC_EQ(pop_x_bump[i] - pop_x_bump[i - 1], TILE_SIZEX);
/* Центр тайла колонки 0 — та самая величина, которую кладут в Char.x
* do_startpos и pos_guards (seg003). */
TC_EQ(pop_x_bump[0 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 72);
TC_EQ(pop_x_bump[9 + FIRST_ONSCREEN_COLUMN] + TILE_SIZEX, 198);
}
TC_TEST(geom_yland_rows)
{
/* y_land[row + 1]; [0] — ряд «над комнатой». */
TC_EQ(pop_y_land[0], -8);
TC_EQ(pop_y_land[1], 55);
TC_EQ(pop_y_land[2], 118);
TC_EQ(pop_y_land[3], 181);
TC_EQ(pop_y_land[4], 244);
/* Шаг между рядами — TILE_SIZEY. */
TC_EQ(pop_y_land[2] - pop_y_land[1], TILE_SIZEY);
TC_EQ(pop_y_land[3] - pop_y_land[2], TILE_SIZEY);
}
TC_TEST(geom_y_to_row)
{
/* Пол каждого ряда должен давать номер этого ряда. */
TC_EQ(pop_y_to_row(pop_y_land[1]), 0);
TC_EQ(pop_y_to_row(pop_y_land[2]), 1);
TC_EQ(pop_y_to_row(pop_y_land[3]), 2);
/* Ряд над комнатой. */
TC_EQ(pop_y_to_row(pop_y_land[0]), -1);
/* Оборот mod 4: ряд «под комнатой» сворачивается обратно в -1 —
* ровно на это опирается спавн падающего куска у нижней кромки. */
TC_EQ(pop_y_to_row(pop_y_land[4]), -1);
}
/* Цепочка сравнений в pop_y_to_row обязана совпадать с ИСХОДНОЙ формулой
* `(y + 60) / 63 % 4 - 1` во всём рабочем диапазоне — включая усечение
* деления К НУЛЮ для отрицательных. Тест держит инвариант: вызовы
* разбросаны по банкам (pop_map, pop_bg, pop_room), и соблазн написать
* деление «по месту» возвращается. */
TC_TEST(geom_y_to_row_matches_formula)
{
int16_t y;
/* 400..400 с запасом накрывает всё: кусок loose стартует у потолка
* (ряд −1) и улетает ниже поля в комнату снизу. */
for (y = -400; y <= 400; y++) {
int16_t t = (int16_t)(y + 60);
TC_EQ(pop_y_to_row(y), (int8_t)(t / 63 % 4 - 1));
}
}
/* calc_screen_x_coord (seg008:2602) считается в pop_cdraw.c таблицей SCRX7
* (банк 4, сюда не линкуется): хранится x/7 байтом, результат = x + SCRX7[..].
* Таблица сгенерирована питоном, поэтому проверяем два правила, на которых
* держится генерация — ИМЕННО на целевом компиляторе:
* 1) тождество 8x/7 == x + x/7 (7x делится нацело);
* 2) усечение К НУЛЮ, а не к минус бесконечности.
* Округляй SDCC иначе — вся отрицательная половина уехала бы на пиксель, и
* поймалось бы это только глазами на левой кромке комнаты. */
TC_TEST(geom_mul8div7_table_rules)
{
int16_t x;
/* Полный диапазон obj_x по данным кадров: −416..695 (см. шапку scr_x). */
for (x = -448; x <= 703; x++) {
int16_t want = (int16_t)(((x < 0) ? -x : x) * 8 / 7);
if (x < 0) want = (int16_t)-want;
TC_EQ((int16_t)(x * 8 / 7), want); /* усечение к нулю */
TC_EQ((int16_t)(x * 8 / 7), (int16_t)(x + x / 7)); /* тождество */
}
}
/* ---- PRNG: сверка asm-LCG с эталонной формулой ---------------------- */
#if POP_PRANDOM_EXACT
#define LCG_ITERS 128
TC_TEST(geom_lcg_matches_reference)
{
pop_rnd_t s;
unsigned long ref = 12345UL; /* произвольный ненулевой старт */
uint16_t i, got, want;
pop_prandom_set(s, 12345u);
for (i = 0; i < LCG_ITERS; i++) {
got = pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
want = (uint16_t)((uint16_t)(ref >> 16) % 256u);
/* Обрываемся на первом расхождении: иначе одна ошибка в умножении
* забила бы весь буфер отчёта однотипными строками. */
if (got != want ||
s.lo != (uint16_t)ref ||
s.hi != (uint16_t)(ref >> 16)) {
TC_EQ(got, want);
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
break;
}
}
TC_EQ(i, LCG_ITERS); /* прошли всю дистанцию без расхождений */
}
TC_TEST(geom_lcg_seed_zero)
{
/* Ноль — законный сид (кладку сеют номером комнаты + ряд + колонка,
* что вполне даёт 0). У LCG ноль не является неподвижной точкой —
* убеждаемся, что asm это воспроизводит. */
pop_rnd_t s;
unsigned long ref = 0UL;
pop_prandom_set(s, 0u);
pop_prandom(&s, 255u);
ref = ref * 214013UL + 2531011UL;
TC_EQ(s.lo, (uint16_t)ref);
TC_EQ(s.hi, (uint16_t)(ref >> 16));
}
#endif /* POP_PRANDOM_EXACT */
TC_TEST(geom_prandom_range)
{
/* Оба генератора обязаны укладываться в 0..maxv, в том числе когда
* maxv+1 НЕ степень двойки (там другая ветка pop_rnd_fit). */
pop_rnd_t s;
uint16_t i, v, seen_hi = 0;
uint16_t out_of_range = 0;
pop_prandom_set(s, 1u);
for (i = 0; i < 200; i++) {
v = pop_prandom(&s, 4u); /* n = 5, не степень двойки */
if (v > 4) out_of_range++;
if (v == 4) seen_hi = 1;
}
TC_EQ(out_of_range, 0);
TC_TRUE(seen_hi); /* верхняя граница достижима */
pop_prandom_set(s, 7u);
out_of_range = 0;
for (i = 0; i < 64; i++) {
v = pop_prandom(&s, 1u); /* n = 2, маска */
if (v > 1) out_of_range++;
}
TC_EQ(out_of_range, 0);
}
int main(void)
{
TC_RUN(geom_xbump_layout);
TC_RUN(geom_yland_rows);
TC_RUN(geom_y_to_row);
TC_RUN(geom_y_to_row_matches_formula);
TC_RUN(geom_mul8div7_table_rules);
#if POP_PRANDOM_EXACT
TC_RUN(geom_lcg_matches_reference);
TC_RUN(geom_lcg_seed_zero);
#endif
TC_RUN(geom_prandom_range);
return 0;
}
+283
View File
@@ -0,0 +1,283 @@
/*
* 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;
}
+349
View File
@@ -0,0 +1,349 @@
/*
* t_jaffar — спецсобытия УРОВНЯ 13: встреча с Джафаром, победа, выход и
* три исключения loose-механики (падающая гряда плит).
*
* Своего ИИ у Джафара нет — это обычный страж (seg002:085A), поэтому
* проверять тут нечего кроме СОБЫТИЙ и ветвлений по номеру уровня. А
* именно они и опасны: три из них — это `if (level == 13)` посреди чужого
* кода (тряска плит, удар плитой в беге, «страж не встаёт в стойку»), и
* сломать их можно правкой, вообще не думая про 13-й уровень.
*
* Порт: seg002:0544 meet_Jaffar, seg002:0517 Jaffar_exit,
* seg006:1927 on_guard_killed, seg000:1317 check_fall_flo,
* seg007:823/949 исключения animate_loose/loose_make_shake.
*/
#include <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "pop_level.h"
#include "pop_tune.h"
#include "scene.h"
#include "stubs.h"
#define E 0 /* empty */
#define F 1 /* floor */
#define L 11 /* loose */
#define W 20 /* wall */
/* Комната с полом внизу и грядой loose в ряду 2 — она же будет играть роль
* «комнаты сверху» для check_fall_flo. */
static const uint8_t room_loose[30] = {
F, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
F, F, L, L, L, L, L, L, F, F,
};
/* Комната, в которую Кид входит снизу: ровный пол во всех рядах. */
static const uint8_t room_flat[30] = {
F, F, F, F, F, F, F, F, F, F,
F, F, F, F, F, F, F, F, F, F,
F, F, F, F, F, F, F, F, F, F,
};
static uint8_t above_row[10];
static void scene_level13(uint8_t room)
{
pop_guard_reset();
pop_current_level = JAFFAR_LEVEL;
pop_leveldoor_open = 0;
pop_guard_notice_timer = 0;
sc_room(room_flat, room);
sc_kid_at(1, 2, 1);
tk_log_clear();
}
/* ---- meet_Jaffar ----------------------------------------------------- */
TC_TEST(jaffar_notice_timer_set_leaving_room3_right)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = JAFFAR_MEET_ROOM;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, JAFFAR_NOTICE_TIME);
}
/* Дверь уровня уже открыта — значит Джафар мёртв и Кид возвращается.
* Второй форы не даётся. */
TC_TEST(jaffar_notice_not_given_after_victory)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = JAFFAR_MEET_ROOM;
pop_leveldoor_open = 2;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
TC_TEST(jaffar_notice_not_given_from_other_room)
{
scene_level13(JAFFAR_MEET_ROOM);
Kid.room = (uint8_t)(JAFFAR_MEET_ROOM + 1);
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
TC_TEST(jaffar_notice_is_level_gated)
{
scene_level13(JAFFAR_MEET_ROOM);
pop_current_level = 12;
Kid.room = JAFFAR_MEET_ROOM;
pop_meet_jaffar();
TC_EQ(pop_guard_notice_timer, 0);
}
/* Счётчик убывает по кадру и останавливается на нуле. */
TC_TEST(jaffar_notice_timer_counts_down)
{
uint8_t i;
pop_united_shadow = 0;
pop_guard_notice_timer = 3;
for (i = 0; i < 5; i++) pop_frame_timers();
TC_EQ(pop_guard_notice_timer, 0);
}
/* ---- Джафар ЖДЁТ, пока идёт фора ------------------------------------- */
/* Поднять соперника с УБРАННЫМ мечом в одном ряду с Кидом и «показать» его:
* дальше решение принимает autocontrol_guard_inactive. */
static void jaffar_faces_kid(void)
{
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = 0;
can_guard_see_kid = 2;
is_guard_notice = 0;
}
TC_TEST(jaffar_holds_still_while_notice_timer_runs)
{
uint8_t i;
scene_level13(1);
jaffar_faces_kid();
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
for (i = 0; i < 6; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
/* Стойку не сменил: меч так и в ножнах. */
TC_EQ(Guard.sword, 0);
}
/* Фора кончилась — обычное поведение стража возвращается. */
TC_TEST(jaffar_draws_sword_after_notice_timer)
{
uint8_t i;
scene_level13(1);
jaffar_faces_kid();
pop_guard_notice_timer = 0;
for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
TC_EQ(Guard.sword, SWORD_2_DRAWN);
}
/* На прочих уровнях таймер ни на что не влияет: условие в оригинале
* двойное (`level != 13 || timer == 0`), и потерять первую половину легко. */
TC_TEST(jaffar_notice_timer_ignored_on_other_levels)
{
uint8_t i;
scene_level13(1);
pop_current_level = 12;
jaffar_faces_kid();
pop_guard_notice_timer = JAFFAR_NOTICE_TIME;
for (i = 0; i < 12; i++) { can_guard_see_kid = 2; pop_guard_tick(); }
TC_EQ(Guard.sword, SWORD_2_DRAWN);
}
/* ---- победа и выход --------------------------------------------------- */
/* Смерть Джафара не открывает дверь, а ставит флаг «выход разрешён». */
TC_TEST(jaffar_death_opens_the_exit_flag)
{
scene_level13(1);
/* charid у Джафара обычный: своим charid оригинал метит только скелета
* (enter_guard, seg002:196), а визиря отличают спрайты и события. */
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = SWORD_2_DRAWN;
guardhp_curr = 0; /* последнее HP только что снято */
pop_flash_time = 0;
pop_guard_tick();
TC_EQ(pop_leveldoor_open, 2);
TC_EQ(pop_flash_time, JAFFAR_FLASH_TIME);
TC_EQ(pop_flash_color, POP_FLASH_WHITE);
}
/* На других уровнях смерть стража флага не ставит (иначе дверь открывалась
* бы от любого убитого). */
TC_TEST(jaffar_death_flag_is_level_gated)
{
scene_level13(1);
pop_current_level = 12;
sc_guard_at(CHARID_2_GUARD, 6, 2, -1, 6);
Guard.sword = SWORD_2_DRAWN;
guardhp_curr = 0;
pop_guard_tick();
TC_EQ(pop_leveldoor_open, 0);
}
/* Уход ВЛЕВО при поднятом флаге жмёт кнопку комнаты 24. */
TC_TEST(jaffar_exit_presses_button_in_room24)
{
scene_level13(1);
tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15; /* opener */
pop_leveldoor_open = 2;
tk_log_clear();
pop_jaffar_exit();
TC_TRUE(tk_log_count(TK_BUTTON) >= 1);
}
TC_TEST(jaffar_exit_silent_before_victory)
{
scene_level13(1);
tk_room_fg[JAFFAR_EXIT_ROOM][JAFFAR_EXIT_TILEPOS] = 15;
pop_leveldoor_open = 0;
tk_log_clear();
pop_jaffar_exit();
TC_EQ(tk_log_count(TK_BUTTON), 0);
}
/* ---- три исключения loose-механики ----------------------------------- */
/* Сотрясение (приземление Кида) на 13-м уровне плиты НЕ трясёт: иначе оно
* сбило бы им отложенный старт, и гряда посыпалась бы разом. */
TC_TEST(jaffar_knock_does_not_shake_tiles)
{
scene_level13(1);
sc_room(room_loose, 1);
sc_kid_at(1, 2, 1);
Kid.curr_row = 2;
pop_loadkid();
knock = -1; /* KNOCK_DOWN: ряд Кида */
pop_check_knock();
TC_EQ(pop_loose_modif[22], 0);
TC_EQ(pop_loose_modif[27], 0);
}
/* А на прочих уровнях трясёт — контроль, что выключен именно 13-й. */
TC_TEST(jaffar_knock_still_shakes_on_other_levels)
{
scene_level13(1);
pop_current_level = 12;
sc_room(room_loose, 1);
sc_kid_at(1, 2, 1);
Kid.curr_row = 2;
pop_loadkid();
knock = -1;
pop_check_knock();
TC_EQ(pop_loose_modif[22], 0x80);
}
/* Фаза с ОТРИЦАТЕЛЬНЫМ стартом на 13-м уровне не гасится, а досчитывает
* до провала: это и есть падающая гряда. Проверяем на плите ПОЛА (тот же
* код, что у плит-потолков, но без mob-обвязки, которая в набор не
* линкуется). */
TC_TEST(jaffar_negative_phase_counts_through_to_fall)
{
uint8_t i;
scene_level13(1);
sc_room(room_loose, 1);
sc_kid_at(1, 0, 1);
pop_loose_modif[22] = (uint8_t)-4; /* старт «через 4 кадра» */
/* 4 кадра — на выход из отрицательной зоны, дальше 10 — обычный отсчёт;
* 30 с запасом. Ванильная ветка «тряска кончилась» обнулила бы фазу на
* первом же кадре, и плита осталась бы стоять. */
for (i = 0; i < 30; i++) pop_loose_tick();
TC_EQ(pop_tile_at(2, 2), 0); /* провалилась: тайл стал пустым */
}
/* На прочих уровнях та же фаза читается как тряска и гаснет — плита цела. */
TC_TEST(jaffar_negative_phase_is_shake_on_other_levels)
{
uint8_t i;
scene_level13(1);
pop_current_level = 12;
sc_room(room_loose, 1);
sc_kid_at(1, 0, 1);
pop_loose_modif[22] = (uint8_t)-4;
for (i = 0; i < 30; i++) pop_loose_tick();
TC_EQ(pop_tile_at(2, 2), L);
TC_EQ(pop_loose_modif[22], 0);
}
/* ---- check_fall_flo --------------------------------------------------- */
/* Вход в комнату 23: шести плитам ряда 2 комнаты СВЕРХУ раздаётся
* отрицательная фаза, остальным — ничего. */
static void fall_flo_scene(uint8_t room)
{
uint8_t i;
scene_level13(room);
for (i = 0; i < 10; i++) above_row[i] = room_loose[20 + i];
pop_map_set_above(above_row);
pop_map_set_edges(0, 0, 1 /* up */, 0);
pop_map_set_above(above_row);
pop_loose_reset();
}
TC_TEST(jaffar_fall_flo_seeds_the_row_above)
{
uint8_t c, seeded = 0;
fall_flo_scene(POP_LOOSE_TILES_ROOM1);
pop_check_fall_flo();
for (c = 2; c <= 7; c++)
if ((int8_t)pop_ceil_modif[c] < 0) seeded++;
TC_EQ(seeded, 6);
/* Колонки вне диапазона 2..7 не тронуты (там в комнате пол, не loose). */
TC_EQ(pop_ceil_modif[0], 0);
TC_EQ(pop_ceil_modif[9], 0);
}
TC_TEST(jaffar_fall_flo_ignores_other_rooms)
{
uint8_t c;
fall_flo_scene((uint8_t)(POP_LOOSE_TILES_ROOM1 + 1));
pop_check_fall_flo();
for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0);
}
TC_TEST(jaffar_fall_flo_is_level_gated)
{
uint8_t c;
fall_flo_scene(POP_LOOSE_TILES_ROOM2);
pop_current_level = 12;
pop_check_fall_flo();
for (c = 0; c < 10; c++) TC_EQ(pop_ceil_modif[c], 0);
}
/* Повторный вход не перебивает уже идущий отсчёт (иначе гряда «зависала»
* бы, пока игрок бегает туда-сюда). */
TC_TEST(jaffar_fall_flo_does_not_restart_running_countdown)
{
fall_flo_scene(POP_LOOSE_TILES_ROOM2);
pop_ceil_modif[3] = 5; /* плита уже падает */
pop_check_fall_flo();
TC_EQ(pop_ceil_modif[3], 5);
}
void main(void)
{
sc_init();
TC_RUN(jaffar_notice_timer_set_leaving_room3_right);
TC_RUN(jaffar_notice_not_given_after_victory);
TC_RUN(jaffar_notice_not_given_from_other_room);
TC_RUN(jaffar_notice_is_level_gated);
TC_RUN(jaffar_notice_timer_counts_down);
TC_RUN(jaffar_holds_still_while_notice_timer_runs);
TC_RUN(jaffar_draws_sword_after_notice_timer);
TC_RUN(jaffar_notice_timer_ignored_on_other_levels);
TC_RUN(jaffar_death_opens_the_exit_flag);
TC_RUN(jaffar_death_flag_is_level_gated);
TC_RUN(jaffar_exit_presses_button_in_room24);
TC_RUN(jaffar_exit_silent_before_victory);
TC_RUN(jaffar_knock_does_not_shake_tiles);
TC_RUN(jaffar_knock_still_shakes_on_other_levels);
TC_RUN(jaffar_negative_phase_counts_through_to_fall);
TC_RUN(jaffar_negative_phase_is_shake_on_other_levels);
TC_RUN(jaffar_fall_flo_seeds_the_row_above);
TC_RUN(jaffar_fall_flo_ignores_other_rooms);
TC_RUN(jaffar_fall_flo_is_level_gated);
TC_RUN(jaffar_fall_flo_does_not_restart_running_countdown);
}
+178
View File
@@ -0,0 +1,178 @@
/*
* t_mouse — спецсобытие уровня 8: МЫШЬ (do_timers seg003:0545,
* do_mouse seg003:0ABA, autocontrol_mouse seg002:07EB).
*
* Единственный набор, куда линкуется guards.c: проверяется не физика (её
* держат t_phys/t_char), а связка «условие запуска → рождение → ИИ →
* нажатие кнопки → уход». Всё это ветки по charid внутри общего кода
* соперника, и сломать их можно, ничего не трогая в самой мыши.
*
* Кадр соперника здесь полный, с ИИ — pop_guard_tick() + pop_guard_phys_tick(),
* как в главном цикле; sc_guard_run для этого не годится (он намеренно без ИИ).
*/
#include <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "pop_level.h" /* pop_current_level (под тестом — из stubs.c) */
#include "pop_tune.h"
#include "scene.h"
#include "stubs.h"
#define E 0 /* empty */
#define F 1 /* floor */
#define O 15 /* opener */
#define W 20 /* wall */
/* Верхний ряд комнаты 16 в упрощённом виде: сплошной пол и кнопка на
* колонке 7 — там же, где она стоит в данных уровня 8. Провал колонок
* 4..6 намеренно засыпан: он мешает Киду, а не мыши, и в тест на ИИ
* притащил бы падение соперника. */
static const uint8_t room_mouse[30] = {
F, F, F, F, F, F, F, O, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Поставить сцену уровня 8: Кид в комнате 16, дверь уровня открыта. */
static void scene_level8(void)
{
sc_room(room_mouse, POP_MOUSE_ROOM);
sc_kid_at(1, 0, 1);
Kid.room = POP_MOUSE_ROOM;
pop_guard_reset();
pop_current_level = POP_MOUSE_LEVEL;
pop_leveldoor_open = 1;
tk_log_clear();
}
/* Один полный кадр соперника, как в главном цикле sprpop.c. */
static void mouse_frame(void)
{
pop_guard_tick();
pop_guard_phys_tick();
}
/* ---- рождение -------------------------------------------------------- */
TC_TEST(mouse_appears_after_delay)
{
uint16_t i;
scene_level8();
/* Счётчик стартует с 1 («дверь открыта») и растёт по кадру, срабатывая
* на значении POP_MOUSE_DELAY — то есть кадров до мыши на два меньше.
* Прогоняем всё, кроме последнего: слот обязан быть ещё пуст. */
for (i = 0; i < POP_MOUSE_DELAY - 2; i++) pop_check_mouse();
TC_EQ(Guard.charid, 0);
pop_check_mouse(); /* тот самый кадр */
TC_EQ(Guard.charid, CHARID_24_MOUSE);
TC_EQ(Guard.x, POP_MOUSE_START_X);
TC_EQ(Guard.curr_row, 0);
TC_EQ(Guard.direction, -1); /* dir_FF_left */
TC_EQ(guardhp_curr, 1);
TC_EQ(Guard.room, POP_MOUSE_ROOM); /* унаследована от Кида (loadkid) */
}
/* Счётчик тикает ТОЛЬКО пока Кид в комнате мыши: в оригинале это то же
* условие, что и запуск (Char.room == mouse_room), поэтому 12,5 секунды
* отсчитываются с возвращения игрока, а не с открытия двери. */
TC_TEST(mouse_timer_frozen_outside_room)
{
uint16_t i;
scene_level8();
Kid.room = POP_MOUSE_ROOM + 1;
for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse();
TC_EQ(Guard.charid, 0);
TC_EQ(pop_leveldoor_open, 1); /* не сдвинулся ни на кадр */
}
/* Закрытая дверь уровня — мыши нет вовсе (она подсказка на обратном пути). */
TC_TEST(mouse_needs_open_leveldoor)
{
uint16_t i;
scene_level8();
pop_leveldoor_open = 0;
for (i = 0; i < POP_MOUSE_DELAY * 2; i++) pop_check_mouse();
TC_EQ(Guard.charid, 0);
}
/* ---- ИИ: пробежала, нажала, ушла ------------------------------------- */
/* Родить мышь, не гоняя POP_MOUSE_DELAY пустых кадров. */
static void spawn_mouse(void)
{
scene_level8();
pop_leveldoor_open = POP_MOUSE_DELAY - 1;
pop_check_mouse();
}
TC_TEST(mouse_runs_left_and_presses_button)
{
uint8_t i;
spawn_mouse();
TC_EQ(Guard.charid, CHARID_24_MOUSE);
/* Бежит влево: до порога разворота ей ~35 пикселей по 3-5 за кадр. */
for (i = 0; i < 24; i++) mouse_frame();
TC_TRUE(Guard.x < POP_MOUSE_START_X);
/* По дороге наступила на кнопку (0,7) — общий check_press физики
* соперника, тот же, которым кнопки жмут страж и тень. */
TC_TRUE(tk_log_count(TK_BUTTON) >= 1);
}
TC_TEST(mouse_turns_back_and_disappears)
{
uint8_t i;
spawn_mouse();
/* 120 кадров с запасом покрывают путь туда (200→166), стойку seq_107
* (11 кадров) и путь обратно (166→200). */
for (i = 0; i < 120; i++) {
mouse_frame();
if (Guard.direction == DIR_56_NONE) break;
}
TC_EQ(Guard.direction, DIR_56_NONE); /* clear_char: ушла со сцены */
TC_EQ(guardhp_curr, 0);
}
/* ---- ветки по charid у соседнего кода -------------------------------- */
/* Мышь Кида «не видит»: иначе он выхватит на неё меч (seg003:0965). */
TC_TEST(mouse_is_not_seen_by_kid)
{
spawn_mouse();
Guard.curr_row = Kid.curr_row; /* один ряд — условие луча */
pop_check_can_guard_see_kid();
TC_EQ(can_guard_see_kid, 0);
}
/* leave_guard мышь не записывает в данные уровня (seg002:02F5): вернувшись
* в комнату, игрок не должен встретить «стража» с её параметрами.
* Комнату берём из диапазона таблицы заглушки (TK_ROOMS = 8). */
TC_TEST(mouse_is_not_saved_as_room_guard)
{
spawn_mouse();
Guard.room = 1;
tk_gs_tile[0] = 0xEE; /* метка «никто сюда не писал» */
pop_guard_leave();
TC_EQ(tk_gs_tile[0], 0xEE);
}
void main(void)
{
sc_init();
TC_RUN(mouse_appears_after_delay);
TC_RUN(mouse_timer_frozen_outside_room);
TC_RUN(mouse_needs_open_leveldoor);
TC_RUN(mouse_runs_left_and_presses_button);
TC_RUN(mouse_turns_back_and_disappears);
TC_RUN(mouse_is_not_seen_by_kid);
TC_RUN(mouse_is_not_saved_as_room_guard);
}
+380
View File
@@ -0,0 +1,380 @@
/*
* t_phys.c — характеризация физики Кида (pop_map + pop_kid + pop_ctrl).
*
* Назначение набора — СТРАХОВКА ПОД РЕФАКТОРИНГ, а не проверка
* правильности. Эталоны здесь сняты с текущего билда, то есть
* консервируют и текущие баги; правильность по-прежнему проверяется
* против SDLPoP. Смысл в другом: перенос физики с `Kid.` на `Char.`
* обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит
* ровно тот класс ошибок, который там возможен.
*
* Поэтому при осознанном изменении поведения эталон ПЕРЕСНИМАЕТСЯ (см.
* SC_RECORD ниже), а в коммите должно быть написано, почему трасса
* поменялась. Молча обновлённый эталон обесценивает весь набор.
*
* Снятие эталона:
* make -C applications/SprPoP/tests/host t_phys SC_RECORD=1
* Трассы уходят в отчёт tcheck готовыми C-строками — вставить в golden_*.
*/
#include "tcheck.h"
#include "scene.h"
#include "stubs.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_ctrl.h"
#include "pop_guard.h" /* pop_do_delta_hp — урон сводится дельтой */
/* Коды тайлов (pop_map): 0 пусто, 1 пол, 3 колонна, 11 loose, 20 стена. */
#define E 0
#define F 1
#define P 3
#define L 11
#define W 20
/* Ровный пол во всю комнату: базовая сцена для ходьбы/бега/разворота. */
static const uint8_t room_flat[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, F,
W, W, W, W, W, W, W, W, W, W,
};
/* Пол с обрывом после колонки 5: падение с ряда 1 на ряд 2. */
static const uint8_t room_ledge[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, E, E, E, E,
W, F, F, F, F, F, F, F, F, W,
};
/* Проваливающаяся плита на (1,4) — сцена под loose-механику. */
static const uint8_t room_loose[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, L, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* Провал в ТРИ пустых тайла (колонки 2,3,4) — геометрия из BUG-RJUMP-1
* (ур. 2, комнаты 1 и 9). Разбег-прыжок влево с колонки 5 обязан
* доставать до колонки 1; у нас Кид сваливается в провал. */
static const uint8_t room_gap3[30] = {
W, E, E, E, E, E, E, E, E, E,
W, F, E, E, E, F, F, F, F, F,
W, F, F, F, F, F, F, F, F, W,
};
/* SC_RECORD=<n> — снять эталон СЦЕНАРИЯ n (нумерация в порядке вызовов в
* main). По одному за прогон: лог tcheck 768 байт, на все трассы сразу
* его не хватает. Перебором занимается record_golden.py. */
#ifndef SC_RECORD
#define SC_RECORD 0
#endif
/* Снять или сверить — одной строкой в каждом сценарии, чтобы режимы не
* разъезжались. Имя сценария = имя файла эталона. */
#if SC_RECORD
static uint8_t sc_idx;
#define SC_CHECK(g, nm) \
do { if (++sc_idx == (SC_RECORD)) sc_dump(nm); } while (0)
#else
#define SC_CHECK(g, nm) sc_expect((g), (uint8_t)(sizeof(g) / SC_REC_BYTES))
#endif
/* ---- эталоны (сняты SC_RECORD=1) ------------------------------------ *
* В режиме снятия эталонов файлов golden/ ещё нет — и не должно быть:
* первый прогон их и порождает. */
#if !SC_RECORD
static const uint8_t golden_stand[] = {
#include "golden/stand.h"
};
static const uint8_t golden_run_right[] = {
#include "golden/run_right.h"
};
static const uint8_t golden_step_shift[] = {
#include "golden/step_shift.h"
};
static const uint8_t golden_turn[] = {
#include "golden/turn.h"
};
static const uint8_t golden_crouch[] = {
#include "golden/crouch.h"
};
static const uint8_t golden_jump_up[] = {
#include "golden/jump_up.h"
};
static const uint8_t golden_fall_ledge[] = {
#include "golden/fall_ledge.h"
};
static const uint8_t golden_loose[] = {
#include "golden/loose.h"
};
static const uint8_t golden_rjump_gap3[] = {
#include "golden/rjump_gap3.h"
};
#else
/* SC_RECORD: имена нужны только чтобы SC_CHECK() раскрылся. */
#define golden_stand 0
#define golden_run_right 0
#define golden_step_shift 0
#define golden_turn 0
#define golden_crouch 0
#define golden_jump_up 0
#define golden_fall_ledge 0
#define golden_loose 0
#define golden_rjump_gap3 0
#endif
/* ---- сценарии -------------------------------------------------------- */
TC_TEST(phys_stand_is_stable)
{
/* Никакого ввода: Кид на ровном полу обязан стоять неподвижно. Тест
* дешёвый, но ловит самое обидное — «сам поехал» после правки
* геометрии или окна Char. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, -1);
sc_trace_clear();
sc_run(0, 12);
SC_CHECK(golden_stand, "stand");
}
TC_TEST(phys_run_right)
{
sc_room(room_flat, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 24);
SC_CHECK(golden_run_right, "run_right");
}
TC_TEST(phys_step_with_shift)
{
/* Shift + вперёд — осторожный шаг: одна из самых чувствительных к
* distance_to_edge веток. */
sc_room(room_flat, 1);
sc_kid_at(3, 1, 0);
sc_trace_clear();
sc_run(SC_R | SC_SHIFT, 20);
SC_CHECK(golden_step_shift, "step_shift");
}
TC_TEST(phys_turn_around)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_L, 16);
SC_CHECK(golden_turn, "turn");
}
TC_TEST(demo_backward_is_not_screen_mirrored)
{
/* demo_moves уже задаёт forward/backward относительно Кида. На плите
* 0,6 он смотрит вправо, и move=2 обязан начать seq_5 turn, а не
* превратиться через user_control() в seq_1 run вправо к пропасти. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
pop_ctrl_auto_move(0);
pop_ctrl_auto_move(2);
/* SEQTBL_BASE+0x01CB — начало seq_5 в исходном kid_data.bin. */
TC_EQ((uint16_t)(Kid.curr_seq - SEQTBL_BASE), 0x01CB);
}
TC_TEST(phys_crouch_and_stand_up)
{
sc_room(room_flat, 1);
sc_kid_at(4, 1, 0);
sc_trace_clear();
sc_run(SC_D, 8);
sc_run(0, 12);
SC_CHECK(golden_crouch, "crouch");
}
TC_TEST(phys_jump_up_nothing_above)
{
/* Над головой пусто — seq_28; проверяем и смещение по x, которое
* оригинал делает перед прыжком. */
sc_room(room_flat, 1);
sc_kid_at(4, 1, -1);
sc_trace_clear();
sc_run(SC_U, 20);
SC_CHECK(golden_jump_up, "jump_up");
}
TC_TEST(phys_run_off_ledge)
{
/* Бег в обрыв: check_on_floor -> start_fall -> land на ряду 2.
* Сценарий, вокруг которого крутился BUG-FALL-SWORD-1. */
sc_room(room_ledge, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_fall_ledge, "fall_ledge");
}
TC_TEST(phys_loose_floor_breaks)
{
/* Пробежка по loose-плите: тайл обязан затрястись и провалиться, а
* физика — сообщить об этом соседям (trob + перерисовка). */
sc_room(room_loose, 1);
sc_kid_at(1, 1, 0);
sc_trace_clear();
sc_run(SC_R, 34);
SC_CHECK(golden_loose, "loose");
#if !SC_RECORD
/* Журнал заглушек: плита обязана была отвалиться (кусок отрывается —
* pop_loose_mob_spawn) и попросить перерисовку. Это уже не
* характеризация, а содержательная проверка — заглушка-пустышка её не
* дала бы. Пока плита ЖИВЁТ в отрисованной комнате, фазой владеет
* pop_loose_modif, а не trob — поэтому спрашиваем именно про кусок. */
TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0);
TC_TRUE(tk_log_count(TK_REDRAW) > 0);
#endif
}
TC_TEST(phys_loose_survives_room_change)
{
/* GUARD против бага «плита не успевает провалиться»: Кид расшатал плиту
* и ушёл в соседнюю комнату — отсчёт обязан ПЕРЕЖИТЬ смену комнаты.
* В оригинале терять нечего (loose — обычный trob в общем списке, фаза
* в curr_room_modif); у нас фаза живёт в pop_loose_modif[] текущей
* комнаты, и её сдаёт/забирает пара leave_room + reset. */
uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */
uint8_t phase = 3; /* середина отсчёта до провала (1..10) */
/* Саму анимацию проверяет phys_loose_floor_breaks выше; здесь нужна
* ровно передача фазы, поэтому ставим её явно — прогон бы привязал тест
* к тому, на каком кадре Кид добежит до плиты. */
sc_room(room_loose, 1);
pop_loose_modif[pos] = phase;
/* Уход из комнаты: фаза уезжает в room_modif + trob... */
tk_log_clear();
pop_loose_leave_room();
TC_TRUE(tk_log_count(TK_TROB) > 0); /* плита сдана в общий список */
/* ...и возвращается при входе обратно — а не обнуляется, как раньше. */
pop_loose_reset();
TC_EQ(pop_loose_modif[pos], phase);
/* Старт уровня, наоборот, обязан фазу ЗАБЫТЬ (load_level восстанавливает
* уровень целиком) — иначе дрожь протекала бы в следующую жизнь. */
pop_loose_forget();
TC_EQ(pop_loose_modif[pos], 0);
}
TC_TEST(phys_loose_gate_survives_room_change)
{
/* GUARD против гейта холостого хода (loose_any, pop_map.c). Гейт
* пропускает оба цикла pop_loose_tick, пока ни одна фаза не взведена, и
* взводится ПЯТЬЮ местами записи. Четыре из них — взвод дрожи от шага и
* сотрясения — уже прогоняет phys_loose_floor_breaks; пятое (фаза
* ВОССТАНОВЛЕНА входом в комнату) не покрывал никто, а именно оно даёт
* самый тихий из возможных отказов: плита, к которой Кид вернулся,
* застыла бы на полудроже навсегда.
*
* Поэтому проверяем не флаг (он статик модуля), а НАБЛЮДАЕМОЕ следствие:
* после возврата в комнату тик обязан ДВИГАТЬ фазу. */
uint8_t pos = 1 * 10 + 4; /* loose-плита сцены room_loose */
uint8_t phase = 3; /* середина отсчёта до провала (1..10) */
/* Начинаем со снятого гейта: старт уровня забывает все фазы, и ни одно
* место взвода после этого не срабатывает. */
sc_room(room_loose, 1);
pop_loose_forget();
pop_loose_tick(); /* холостой проход — гейт снят */
/* Плита осталась недодрожавшей в room_modif, Кид возвращается в комнату. */
pop_loose_modif[pos] = phase;
pop_loose_leave_room();
pop_loose_reset();
TC_EQ(pop_loose_modif[pos], phase);
/* И вот теперь тик обязан её ДВИНУТЬ, а не пропустить по снятому гейту. */
pop_loose_tick();
TC_EQ(pop_loose_modif[pos], (uint8_t)(phase + 1));
}
TC_TEST(phys_running_jump_over_3tile_gap)
{
/* BUG-RJUMP-1. Разбег влево от колонки 8, дальше Up — разбег-прыжок.
* Ожидание оригинала: перелёт провала в три тайла и посадка на (1,1).
* Сейчас эталон фиксирует НАШЕ поведение (падение в провал) — когда
* баг починим, трасса поменяется осознанно и эталон переснимется. */
sc_room(room_gap3, 1);
sc_kid_at(8, 1, -1);
sc_trace_clear();
sc_run(SC_L, 8); /* разбег */
sc_run(SC_L | SC_U, 26); /* прыжок и посадка */
SC_CHECK(golden_rjump_gap3, "rjump_gap3");
}
/* Падение с ДВУХ рядов: пол только внизу, Кид стартует на кромке ряда 0.
* В обычном падении это −1 HP (land, seg005:173), под пером — ни урона, ни
* разгона. */
static const uint8_t room_drop2[30] = {
W, F, E, E, E, E, E, E, E, E,
W, E, E, E, E, E, E, E, E, E,
W, F, F, F, F, F, F, F, F, W,
};
/* Максимальная скорость падения за прогон и итоговое HP — через сцену не
* видно (в трассу пишется поза), поэтому меряем прямо по Kid. */
static uint8_t drop2_max_fall_y(uint8_t frames)
{
uint8_t i, mx = 0;
for (i = 0; i < frames; i++) {
sc_trace_clear();
sc_run(SC_R, 1);
if ((uint8_t)Kid.fall_y > mx) mx = (uint8_t)Kid.fall_y;
}
return mx;
}
TC_TEST(phys_feather_fall_is_slow_and_harmless)
{
uint8_t mx_normal, mx_feather, hp_normal, hp_feather;
/* (1) как обычно: разгон до FALL_MAX и −1 HP за два ряда */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 0;
mx_normal = drop2_max_fall_y(40);
pop_do_delta_hp(); /* урон копится дельтой, сводит её цикл */
hp_normal = hitp_curr;
/* (2) под зельем медленного падения (seg006:057C: ускорение 1, потолок 4;
* seqtbl уводит приземление в ветку stepfloat — без урона) */
sc_room(room_drop2, 1);
sc_kid_at(1, 0, 0);
pop_feather = 200;
mx_feather = drop2_max_fall_y(40);
pop_do_delta_hp();
hp_feather = hitp_curr;
pop_feather = 0;
TC_TRUE(mx_normal > 4); /* обычное падение разгоняется */
TC_TRUE(mx_feather <= 4); /* под пером потолок скорости — 4 */
TC_TRUE(hp_normal < 3); /* два ряда обычно стоят HP */
TC_EQ(hp_feather, 3); /* под пером — целым */
}
/* ---- точка входа ----------------------------------------------------- */
void main(void)
{
sc_init();
TC_RUN(phys_stand_is_stable);
TC_RUN(phys_run_right);
TC_RUN(phys_step_with_shift);
TC_RUN(phys_turn_around);
TC_RUN(demo_backward_is_not_screen_mirrored);
TC_RUN(phys_crouch_and_stand_up);
TC_RUN(phys_jump_up_nothing_above);
TC_RUN(phys_run_off_ledge);
TC_RUN(phys_loose_floor_breaks);
TC_RUN(phys_loose_survives_room_change);
TC_RUN(phys_loose_gate_survives_room_change);
TC_RUN(phys_running_jump_over_3tile_gap);
TC_RUN(phys_feather_fall_is_slow_and_harmless);
}
+332
View File
@@ -0,0 +1,332 @@
/*
* t_shadow — спецсобытия УРОВНЯ 12: встреча с тенью, общий урон, слияние.
*
* Что здесь проверяется и почему именно тестом, а не прогоном в MAME. Вся
* сцена уровня 12 — это набор УСЛОВИЙ, каждое из которых легко сломать,
* ничего не трогая в самой тени: тайл меча, знак `pop_united_shadow`,
* charid соперника, номер уровня. Проверить их в эмуляторе — значит
* дойти до 12-го уровня и разыграть бой; здесь это шесть вызовов.
*
* Порт: seg002:0064 check_shadow, seg002:0536 sword_disappears,
* seg006:2033 check_killed_shadow, seg000:1518 do_delta_hp,
* seg003:0735 timers, seg006:1063 «появление плит».
*
* Сам ИИ тени (autocontrol_shadow_level12) сюда НЕ вынесен: он решает
* «куда идти», и его исход — траектория, то есть предмет прогона со
* сценой и эталоном, а не одиночной проверки. Набор линкует guards.c
* (как t_mouse), поэтому добавить такой сценарий можно здесь же.
*/
#include <stdint.h>
#include "tcheck.h"
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h"
#include "pop_geom.h"
#include "pop_state.h"
#include "pop_level.h"
#include "pop_tune.h"
#include "scene.h"
#include "stubs.h"
#define E 0 /* empty */
#define F 1 /* floor */
#define W 20 /* wall */
#define S 22 /* sword */
/* Комната 15 в объёме, который нужен событию: меч в (0,1), пол под ним. */
static const uint8_t room15[30] = {
F, S, F, F, F, F, F, F, E, E,
W, W, W, W, W, W, W, W, F, F,
W, W, W, W, W, W, W, W, W, W,
};
/* Комната 2: верхний ряд с ПРОПАСТЬЮ в колонках 4..6 — те самые плиты,
* которые появляются под ногами после слияния. */
static const uint8_t room2[30] = {
F, F, F, F, E, E, E, F, F, F,
W, W, W, W, W, W, W, W, W, W,
W, W, W, W, W, W, W, W, W, W,
};
/* Общая точка входа: уровень 12, слот соперника пуст, слияния не было. */
static void scene_level12(void)
{
uint8_t i;
pop_guard_reset();
pop_current_level = SHADOW_FIGHT_LEVEL;
pop_united_shadow = 0;
pop_shadow_init = 0;
for (i = 0; i < 30; i++) tk_room_fg[SHADOW_FIGHT_ROOM][i] = room15[i];
tk_log_clear();
}
/* ---- check_shadow: когда тень поднимается ---------------------------- */
/* Меч ещё лежит — тени нет. Условие ОБРАТНО уровню 5 (там тень приходит,
* пока зелье на месте), и перепутать их — самая вероятная ошибка порта. */
TC_TEST(shadow12_waits_until_sword_taken)
{
scene_level12();
TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0);
TC_EQ(Guard.charid, 0);
}
/* Меч подобран — тень падает в комнату (init_shad_12 + seq_7_fall). */
TC_TEST(shadow12_rises_when_sword_gone)
{
scene_level12();
tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F;
TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 1);
TC_EQ(Guard.charid, CHARID_1_SHADOW);
TC_EQ(Guard.x, 0x51);
/* y = 232 — НИЖЕ поля (192), и это не ошибка данных: поле беззнаковое,
* тень «падает» из-под комнаты и появляется сверху. А curr_row при
* этом РОВНО ИЗ ТАБЛИЦЫ (0), а не пересчитанный из y (тот дал бы −1):
* do_init_shad копирует семь полей как есть и ряд не трогает. Обе
* величины закреплены тестом — «починить» их выглядит соблазнительно. */
TC_EQ(Guard.y, 0xE8);
TC_EQ(Guard.curr_row, 0);
TC_EQ(Guard.direction, 0); /* dir_0_right */
TC_EQ(Guard.room, SHADOW_FIGHT_ROOM);
TC_EQ(guardhp_curr, 4);
}
/* После слияния тень не поднимается больше НИКОГДА (флаг ушёл в −1). */
TC_TEST(shadow12_never_rises_after_uniting)
{
scene_level12();
tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS] = F;
pop_united_shadow = -1;
TC_EQ(pop_check_shadow(SHADOW_FIGHT_ROOM), 0);
TC_EQ(Guard.charid, 0);
}
/* В чужой комнате событие не срабатывает — там поднимается обычный страж
* (возврат 0 = «занимайся слотом сам»). */
TC_TEST(shadow12_other_room_leaves_slot_alone)
{
scene_level12();
TC_EQ(pop_check_shadow((uint8_t)(SHADOW_FIGHT_ROOM - 1)), 0);
TC_EQ(Guard.charid, 0);
}
/* ---- sword_disappears ------------------------------------------------ */
TC_TEST(shadow12_sword_vanishes_leaving_room18)
{
scene_level12();
Kid.room = SWORD_GONE_ROOM;
tk_modif[SHADOW_SWORD_TILEPOS] = 5; /* блеск меча */
pop_sword_disappears();
TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], F);
/* Модификатор гасится вместе с тайлом: ненулевой modif рисует на полу
* «фальшивый» узор (та же причина, что у поднятого зелья). */
TC_EQ(tk_modif[SHADOW_SWORD_TILEPOS], 0);
}
TC_TEST(shadow12_sword_stays_leaving_other_room)
{
scene_level12();
Kid.room = (uint8_t)(SWORD_GONE_ROOM - 1);
pop_sword_disappears();
TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S);
}
/* На других уровнях комната 18 — обычная комната. */
TC_TEST(shadow12_sword_event_is_level_gated)
{
scene_level12();
pop_current_level = 11;
Kid.room = SWORD_GONE_ROOM;
pop_sword_disappears();
TC_EQ(tk_room_fg[SHADOW_FIGHT_ROOM][SHADOW_SWORD_TILEPOS], S);
}
/* ---- таймер вспышки -------------------------------------------------- */
/* Счётчик досчитывает до нуля и уходит в −1 — «уже слились». Ноль как
* конечное состояние был бы неотличим от «слияния не было». */
TC_TEST(shadow12_timer_ends_at_minus_one)
{
pop_united_shadow = 3;
pop_frame_timers(); TC_EQ(pop_united_shadow, 2);
pop_frame_timers(); TC_EQ(pop_united_shadow, 1);
pop_frame_timers(); TC_EQ(pop_united_shadow, -1);
pop_frame_timers(); TC_EQ(pop_united_shadow, -1); /* и больше не меняется */
}
TC_TEST(shadow12_timer_idle_when_zero)
{
pop_united_shadow = 0;
pop_frame_timers();
TC_EQ(pop_united_shadow, 0);
}
/* ---- общий урон (do_delta_hp) ---------------------------------------- */
static void hp_scene(uint8_t charid, uint8_t level)
{
scene_level12();
pop_current_level = level;
Guard.charid = charid;
guardhp_curr = guardhp_max = 4;
hitp_beg_lev = 3;
pop_kid_hp_reset();
guardhp_delta = -1;
hitp_delta = 0;
}
/* Ранил тень — ранил себя: урон КОПИРУЕТСЯ, а не делится. */
TC_TEST(shadow12_damage_is_shared)
{
hp_scene(CHARID_1_SHADOW, SHADOW_FIGHT_LEVEL);
pop_do_delta_hp();
TC_EQ(guardhp_curr, 3);
TC_EQ(hitp_curr, 2);
}
TC_TEST(shadow12_damage_not_shared_with_guard)
{
hp_scene(CHARID_2_GUARD, SHADOW_FIGHT_LEVEL);
pop_do_delta_hp();
TC_EQ(guardhp_curr, 3);
TC_EQ(hitp_curr, 3);
}
/* Тень уровней 4/5/6 боевой не бывает, но проверка обязана быть по УРОВНЮ,
* а не только по charid: иначе безобидная тень уровня 5 делилась бы HP. */
TC_TEST(shadow12_damage_not_shared_on_other_level)
{
hp_scene(CHARID_1_SHADOW, 5);
pop_do_delta_hp();
TC_EQ(guardhp_curr, 3);
TC_EQ(hitp_curr, 3);
}
/* ---- check_killed_shadow --------------------------------------------- */
static void kill_scene(uint8_t charid, int8_t kid_alive, int8_t guard_alive)
{
scene_level12();
Guard.charid = charid;
Guard.alive = guard_alive;
guardhp_curr = guardhp_max = 4;
hitp_beg_lev = 3;
pop_kid_hp_reset();
Kid.charid = CHARID_0_KID;
Kid.alive = kid_alive;
guardhp_delta = hitp_delta = 0;
pop_flash_time = 0;
pop_immortal = 0;
}
/* Тень мертва — Киду отсчитывается 100 урона, то есть гарантированно всё. */
TC_TEST(shadow12_killing_shadow_kills_kid)
{
kill_scene(CHARID_1_SHADOW, -1, 0);
pop_check_killed_shadow();
TC_EQ(hitp_delta, -3);
TC_TRUE(pop_flash_time != 0);
TC_EQ(pop_flash_color, POP_FLASH_WHITE);
}
/* И симметрично: смерть Кида добивает тень. */
TC_TEST(shadow12_killing_kid_kills_shadow)
{
kill_scene(CHARID_1_SHADOW, 0, -1);
pop_check_killed_shadow();
TC_EQ(guardhp_delta, -4);
}
/* Со стражем связи нет — условие оригинала `(charid | charid) == shadow`
* проходит только для пары Кид/тень. */
TC_TEST(shadow12_kill_link_ignores_guard)
{
kill_scene(CHARID_2_GUARD, -1, 0);
pop_check_killed_shadow();
TC_EQ(hitp_delta, 0);
TC_EQ(pop_flash_time, 0);
}
/* Оба живы — событие молчит (иначе бой убивал бы обоих в первом же кадре). */
TC_TEST(shadow12_kill_link_silent_while_both_alive)
{
kill_scene(CHARID_1_SHADOW, -1, -1);
pop_check_killed_shadow();
TC_EQ(hitp_delta, 0);
TC_EQ(guardhp_delta, 0);
}
/* ---- «появление плит» ------------------------------------------------ */
/* Сцена: Кид стоит в комнате 2 на колонке 4 верхнего ряда — прямо над
* пропастью. До слияния он обязан провалиться, после — достроить пол. */
static void floors_scene(void)
{
uint8_t i;
scene_level12();
for (i = 0; i < 30; i++) tk_room_fg[2][i] = room2[i];
sc_room(room2, 2);
sc_kid_at(4, 0, 1);
sc_trace_clear();
}
TC_TEST(shadow12_floors_appear_after_uniting)
{
floors_scene();
pop_united_shadow = -1;
sc_run(0, 2);
/* Не упал: действие осталось стоечным, ряд тот же. */
TC_EQ(Kid.curr_row, 0);
TC_TRUE(Kid.action != 4); /* actions_4_in_freefall */
/* Плита появилась И в карте комнаты, И в данных уровня. */
TC_EQ(pop_tile_at(4, 0), 1);
TC_EQ(tk_room_fg[2][4], 1);
/* Перерисованы две клетки — сама и соседняя справа, на обеих страницах. */
TC_TRUE(tk_log_count(TK_REDRAW) >= 2);
}
/* До слияния пропасть остаётся пропастью. */
TC_TEST(shadow12_floors_hidden_before_uniting)
{
floors_scene();
pop_united_shadow = 0;
sc_run(0, 2);
TC_EQ(pop_tile_at(4, 0), 0);
TC_TRUE(Kid.curr_row != 0 || Kid.action == 4);
}
/* Событие привязано к КОМНАТЕ: в комнате 3 того же уровня пола нет. */
TC_TEST(shadow12_floors_only_in_their_rooms)
{
floors_scene();
pop_united_shadow = -1;
Kid.room = 3;
sc_run(0, 2);
TC_EQ(pop_tile_at(4, 0), 0);
}
void main(void)
{
sc_init();
TC_RUN(shadow12_waits_until_sword_taken);
TC_RUN(shadow12_rises_when_sword_gone);
TC_RUN(shadow12_never_rises_after_uniting);
TC_RUN(shadow12_other_room_leaves_slot_alone);
TC_RUN(shadow12_sword_vanishes_leaving_room18);
TC_RUN(shadow12_sword_stays_leaving_other_room);
TC_RUN(shadow12_sword_event_is_level_gated);
TC_RUN(shadow12_timer_ends_at_minus_one);
TC_RUN(shadow12_timer_idle_when_zero);
TC_RUN(shadow12_damage_is_shared);
TC_RUN(shadow12_damage_not_shared_with_guard);
TC_RUN(shadow12_damage_not_shared_on_other_level);
TC_RUN(shadow12_killing_shadow_kills_kid);
TC_RUN(shadow12_killing_kid_kills_shadow);
TC_RUN(shadow12_kill_link_ignores_guard);
TC_RUN(shadow12_kill_link_silent_while_both_alive);
TC_RUN(shadow12_floors_appear_after_uniting);
TC_RUN(shadow12_floors_hidden_before_uniting);
TC_RUN(shadow12_floors_only_in_their_rooms);
}
+116
View File
@@ -0,0 +1,116 @@
/* pop_timer — логические тики, pause/unlimited, читы и QuickSave state. */
#include <stdint.h>
#include "tcheck.h"
#include "pop_timer.h"
/* Минимальный memory-stream вместо EMM. pop_timer_qsave использует ровно
* u8/u16, поэтому этот stub одновременно проверяет его endian-контракт. */
static uint8_t stream[3];
static uint8_t stream_pos;
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value)
{
if (io->reading) {
if (io->apply) *value = stream[stream_pos];
} else {
stream[stream_pos] = *value;
}
++stream_pos;
}
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
{
uint8_t lo, hi;
if (io->reading) {
lo = stream[stream_pos++]; hi = stream[stream_pos++];
if (io->apply) *value = (uint16_t)(lo | ((uint16_t)hi << 8));
} else {
stream[stream_pos++] = (uint8_t)*value;
stream[stream_pos++] = (uint8_t)(*value >> 8);
}
}
TC_TEST(timer_starts_and_only_runs_when_allowed)
{
pop_timer_new_game();
TC_EQ(pop_timer_minutes, POP_TIMER_START_MINUTES);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(0, 1)); /* Settings: unlimited */
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(1, 0)); /* pause/HDD/cutscene */
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
pop_timer_minutes = 2;
pop_timer_ticks = 2;
TC_FALSE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 2);
TC_EQ(pop_timer_ticks, 1);
TC_FALSE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 1);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
}
TC_TEST(timer_expiry_is_one_shot)
{
pop_timer_minutes = 1;
pop_timer_ticks = 1;
TC_TRUE(pop_timer_tick(1, 1));
TC_EQ(pop_timer_minutes, 0);
TC_EQ(pop_timer_ticks, POP_TIMER_START_TICKS);
TC_FALSE(pop_timer_tick(1, 1));
TC_FALSE(pop_timer_tick(0, 1));
}
TC_TEST(timer_cheats_match_sdlpop_rules)
{
pop_timer_minutes = 5;
pop_timer_cheat_subtract();
TC_EQ(pop_timer_minutes, 4);
pop_timer_minutes = 1;
pop_timer_cheat_subtract();
TC_EQ(pop_timer_minutes, 1); /* minus не создаёт expiry */
pop_timer_minutes = 0;
pop_timer_cheat_add();
TC_EQ(pop_timer_minutes, 1);
pop_timer_minutes = 0xFF;
pop_timer_cheat_add();
TC_EQ(pop_timer_minutes, 0xFF); /* не сворачиваемся в 0 */
}
TC_TEST(timer_qsave_restores_exact_state_and_rejects_bad_tick)
{
pop_qs_io_t io;
pop_timer_minutes = 42;
pop_timer_ticks = 103;
stream_pos = 0;
io.reading = io.apply = io.failed = 0;
pop_timer_qsave(&io);
TC_EQ(stream_pos, 3);
pop_timer_minutes = 7;
pop_timer_ticks = 400;
stream_pos = 0;
io.reading = io.apply = 1; io.failed = 0;
pop_timer_qsave(&io);
TC_FALSE(io.failed);
TC_EQ(pop_timer_minutes, 42);
TC_EQ(pop_timer_ticks, 103);
stream[0] = 9; stream[1] = 0; stream[2] = 0; /* tick 0 — impossible */
pop_timer_minutes = 33;
pop_timer_ticks = 444;
stream_pos = 0; io.failed = 0;
pop_timer_qsave(&io);
TC_TRUE(io.failed);
TC_EQ(pop_timer_minutes, 33);
TC_EQ(pop_timer_ticks, 444);
}
void main(void)
{
TC_RUN(timer_starts_and_only_runs_when_allowed);
TC_RUN(timer_expiry_is_one_shot);
TC_RUN(timer_cheats_match_sdlpop_rules);
TC_RUN(timer_qsave_restores_exact_state_and_rejects_bad_tick);
}
+95
View File
@@ -0,0 +1,95 @@
/*
* 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);
}
int main(void)
{
sc_init();
TC_RUN(wall_stops_undershot_jump);
return 0;
}