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:
@@ -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)
|
||||
@@ -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,
|
||||
@@ -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())
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user