From 3fe083331f6aede636d50baff0bd2d2fa64b81b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Wed, 5 Aug 2026 12:32:55 +0300 Subject: [PATCH] =?UTF-8?q?tests-host:=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=85=D0=B0=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=81=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B5?= =?UTF-8?q?=D0=B2=20=D0=9A=D0=B8=D0=B4=D0=B0=20(=D1=84=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=20+=20=D0=B7=D0=B0=D1=86=D0=B5=D0=BF)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверять физику Кида глазами в MAME дорого и ненадёжно: ошибка почти всегда не в одной функции, а в РАСХОЖДЕНИИ ТРАЕКТОРИИ через несколько кадров. Харнесс гоняет тот же кадр, что и главный цикл (pop_ctrl_tick -> kid_tick -> pop_phys_tick -> pop_loose_tick), и сравнивает трассу состояния с эталоном. - scene.c/.h — раннер: комната + стартовая поза + скрипт ввода -> трасса; sc_kid_at_x задаёт точный X (исход часто зависит от фазы внутри тайла). - stubs.c/.h — libc/libbgi/соседние модули; read() реально отдаёт kid_data.bin (иначе kdat_ok=0 и play_seq молчит — трасса замирает). - t_phys.c — 9 характеризующих сценариев, 1723 сверки (golden/). - t_grab.c — окно зацепа: существует, достижимо коротким шагом, не зависит от рисунка нажатий. - record_golden.py — снятие эталона по одному сценарию за прогон. - testkit/host-tests.mk — CODE_LOC настраиваемый, EXTRA_INC/EXTRA_CFLAGS. Именно харнесс дал доказательство, что физика зацепа у нас верна, и тем самым перевёл поиск BUG-GRAB-1 на клавиатуру. Co-Authored-By: Claude Opus 5 --- applications/PoP/roomtest/tests-host/Makefile | 42 +++ .../PoP/roomtest/tests-host/README.md | 99 +++++-- .../PoP/roomtest/tests-host/bank_stub.s | 25 ++ .../PoP/roomtest/tests-host/gen_kid_blob.py | 46 ++++ .../PoP/roomtest/tests-host/golden/crouch.h | 22 ++ .../roomtest/tests-host/golden/fall_ledge.h | 36 +++ .../PoP/roomtest/tests-host/golden/jump_up.h | 22 ++ .../PoP/roomtest/tests-host/golden/loose.h | 36 +++ .../roomtest/tests-host/golden/rjump_gap3.h | 36 +++ .../roomtest/tests-host/golden/run_right.h | 26 ++ .../PoP/roomtest/tests-host/golden/stand.h | 14 + .../roomtest/tests-host/golden/step_shift.h | 22 ++ .../PoP/roomtest/tests-host/golden/turn.h | 18 ++ .../PoP/roomtest/tests-host/kidblob.h | 18 ++ .../PoP/roomtest/tests-host/record_golden.py | 72 +++++ applications/PoP/roomtest/tests-host/scene.c | 163 ++++++++++++ applications/PoP/roomtest/tests-host/scene.h | 88 +++++++ applications/PoP/roomtest/tests-host/stubs.c | 196 ++++++++++++++ applications/PoP/roomtest/tests-host/stubs.h | 43 +++ applications/PoP/roomtest/tests-host/t_grab.c | 159 +++++++++++ applications/PoP/roomtest/tests-host/t_phys.c | 248 ++++++++++++++++++ testkit/host-tests.mk | 12 +- 22 files changed, 1421 insertions(+), 22 deletions(-) create mode 100644 applications/PoP/roomtest/tests-host/bank_stub.s create mode 100644 applications/PoP/roomtest/tests-host/gen_kid_blob.py create mode 100644 applications/PoP/roomtest/tests-host/golden/crouch.h create mode 100644 applications/PoP/roomtest/tests-host/golden/fall_ledge.h create mode 100644 applications/PoP/roomtest/tests-host/golden/jump_up.h create mode 100644 applications/PoP/roomtest/tests-host/golden/loose.h create mode 100644 applications/PoP/roomtest/tests-host/golden/rjump_gap3.h create mode 100644 applications/PoP/roomtest/tests-host/golden/run_right.h create mode 100644 applications/PoP/roomtest/tests-host/golden/stand.h create mode 100644 applications/PoP/roomtest/tests-host/golden/step_shift.h create mode 100644 applications/PoP/roomtest/tests-host/golden/turn.h create mode 100644 applications/PoP/roomtest/tests-host/kidblob.h create mode 100644 applications/PoP/roomtest/tests-host/record_golden.py create mode 100644 applications/PoP/roomtest/tests-host/scene.c create mode 100644 applications/PoP/roomtest/tests-host/scene.h create mode 100644 applications/PoP/roomtest/tests-host/stubs.c create mode 100644 applications/PoP/roomtest/tests-host/stubs.h create mode 100644 applications/PoP/roomtest/tests-host/t_grab.c create mode 100644 applications/PoP/roomtest/tests-host/t_phys.c diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index bf39a89..3bad08c 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -3,6 +3,48 @@ TESTKIT := $(abspath $(CURDIR)/../../../../testkit) ENGINE_DIR := $(abspath $(CURDIR)/..) +# t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации +# абсолютными адресами от KD_DATA_OFF (0x100) — там, куда на устройстве +# мапится страница W0. Значит блоб kid_data.bin лежит по 0x100, и код +# набора обязан начинаться выше его конца (0x100 + 3720 = 0x0F88). +CODE_LOC := 0x1000 + +# Заголовки 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) + OBJS_geom := build/eng_pop_geom.rel +OBJS_phys := build/eng_pop_geom.rel build/eng_pop_kid.rel \ + build/eng_pop_map.rel build/eng_pop_ctrl.rel \ + build/eng_pop_state.rel \ + build/stubs.rel build/scene.rel build/kid_blob.rel \ + build/bank_stub.rel + +# Пробный набор (разведка, без эталонов) линкуется тем же составом. +OBJS_grab := $(OBJS_phys) + include $(TESTKIT)/host-tests.mk + +# --- t_phys: блоб таблиц анимации ------------------------------------ +KID_BIN := $(abspath $(CURDIR)/../../poc/res/kid/kid_data.bin) + +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) diff --git a/applications/PoP/roomtest/tests-host/README.md b/applications/PoP/roomtest/tests-host/README.md index a42fec7..5ca4259 100644 --- a/applications/PoP/roomtest/tests-host/README.md +++ b/applications/PoP/roomtest/tests-host/README.md @@ -14,6 +14,7 @@ make -C applications/PoP/roomtest/tests-host t_geom | набор | модуль | что проверяет | |-------|--------|---------------| | `t_geom` | `pop_geom.c` | геометрия комнаты (`x_bump`, `y_land`, `y_to_row`) и PRNG оригинала | +| `t_phys` | `pop_map.c` + `pop_kid.c` + `pop_ctrl.c` | покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал | Главный тест в `t_geom` — `geom_lcg_matches_reference`. LCG оригинала (`s = s*214013 + 2531011`) написан в `pop_geom.c` на ассемблере по схеме @@ -22,25 +23,83 @@ make -C applications/PoP/roomtest/tests-host t_geom asm с наивной 32-битной формулой на 128 шагах — и по возвращаемому значению, и по обеим половинам сида. +## t_phys: характеризация, а не проверка правильности + +Набор существует ради **страховки под рефакторинг**. Эталоны сняты с +текущего билда, то есть консервируют и текущие баги — правильность +по-прежнему проверяется сверкой с `SDLPoP/src/`. Ценность в другом: +ближайший крупный шаг (перенос физики с `Kid.` на `Char.`, чтобы её мог +переиспользовать страж) обязан быть **поведение-сохраняющим**, и «трасса до +== трасса после» ловит ровно тот класс ошибок, который там возможен. + +Поэтому эталон **переснимается только осознанно**, и в коммите должно быть +написано, что и почему поменялось. Молча обновлённый эталон обесценивает +весь набор. + +``` +python3 record_golden.py # переснять все трассы +python3 record_golden.py 7 9 # только сценарии 7 и 9 +``` + +Трасса — по одной записи на кадр: `frame, x, y, dir, col, row, action` и +смещение `curr_seq` от `SEQTBL_BASE`. Кадр гоняется ровно в том порядке, +что и в главном цикле `roomtest.c`: + +``` +pop_ctrl_tick(); // ввод -> control(): смена последовательности +kid_tick(); // play_seq: следующий кадр +pop_phys_tick(); // падение/приземление/стена +pop_loose_tick(); // досчёт тряски и снятие провалившейся плиты +``` + +Покадровость здесь принципиальна: порт seg005/seg006 — конечный автомат, и +ошибка почти всегда проявляется не в одной функции, а в расхождении +траектории через несколько кадров. Плюс главный риск переноса на `Char` — +не арифметика, а то, **кто владеет окном `Char` внутри кадра**; поймать это +можно только прогоном полного кадра. + +Проверка, что харнесс воспроизводит устройство: сценарий `stand` даёт +`frame=15, x=114, y=118, curr_seq=0x19A3` — те же значения, что читаются из +`_Kid` в живом MAME. + +### Как это заработало под ucsim + +Три шва, каждый закрыт без правок продуктового кода: + +1. **Таблицы анимации.** `pop_kid.c` ходит по ним абсолютными адресами от + `KD_DATA_OFF` (0x100) — там, куда на устройстве их приводит маппинг W0. + В тесте `kid_data.bin` просто кладётся по 0x100 (`gen_kid_blob.py`), а + `gfx_w0_map` заглушен. Следствие: код набора линкуется с + `--code-loc 0x1000` (`CODE_LOC` в Makefile), чтобы не налезть на блоб. +2. **Окружение.** `stubs.c` — libc/libbgi (графика, память, файлы), соседние + модули (`pop_bg`/`pop_trob`/`pop_level`/`pop_redraw`) и состояние стража. + Заглушки не пустые там, где это меняет смысл: файловое чтение реально + отдаёт `kid_data.bin` (иначе `pop_kid_data_load` тихо сдаётся, `kdat_ok` + остаётся нулём и `play_seq` не делает ничего — трасса выходит из одного + застывшего кадра), клавиатура отвечает по набору «нажатых» скан-кодов + (так под тест попадают `read_input` и `read_user_control`, а не только + диспетчер), а обращения к соседям **журналируются** (`tk_log`) — можно + проверять «плита отвалилась и попросила перерисовку», а не только + координаты. +3. **`__banked`.** Модули физики помечены как банковые, SDCC генерирует на + них трамплин `___sdcc_bcall_ehl`, которому нужны `set_bank`/`get_bank`. + В тесте память плоская — `bank_stub.s` отдаёт нулевой банк и пустое + переключение, соблюдая контракт по регистрам. + +Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные +переключения банков, тайминги кадра, настоящая клавиатура. + ## Что нужно, чтобы двинуться дальше -Следующие на очереди модули (`pop_trob`, `pop_level`, `pop_map`, логика -падающих плит) упираются в два шва, и оба затрагивают продукт: - -1. **Доступ к странице уровня.** `pop_level.c` ходит по абсолютным адресам - (`(uint8_t *)(LVL_DATA_OFF + …)` после `gfx_w0_map`) — в тестах это - обращение в никуда. Нужен макрос вида `W0PTR(off)`: на таргете как - сейчас, в тестах — смещение в обычном массиве. -2. **Журналирующий рендерер** вместо `pop_bg`/`pop_gdraw` — не заглушка, а - запись вызовов, чтобы проверять «какой тайл помечен к перерисовке, каким - кодом, с каким счётчиком страниц». `BUG-GATE-ANIM-1` был ровно такой - формы. - -После этого `bug_closed.md` превращается в готовый список регрессионных -кейсов: у каждой записи там есть симптом и ожидаемое поведение. - -Отдельно стоит завести тест на `BUG-LOOSE-2` — он до сих пор помечен в -`bug_list.md` как непроверенный именно потому, что гонку «уйти из комнаты -раньше, чем долетит плита» через мост MAME воспроизвести не удалось. На -уровне логики это несколько строк: заспавнить кусок, сменить комнату, -тикать до приземления, проверить щебень в данных уровня. +- **Доступ к странице уровня.** `pop_level.c` ходит по абсолютным адресам + (`(uint8_t *)(LVL_DATA_OFF + …)` после `gfx_w0_map`). Для `pop_map` это не + потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ через `pop_map_set`, — + но набор на сам `pop_level`/`pop_trob` в это упрётся. Решение то же, что + для `kid_data`: положить страницу по нужному адресу и поднять `CODE_LOC`. +- **Сценарии из `bug_closed.md`.** У каждой закрытой записи есть симптом и + ожидаемое поведение — готовый список регрессионных кейсов, который стоит + переносить в `t_phys` по мере надобности. +- **BUG-LOOSE-2** (гонка «уйти из комнаты раньше, чем долетит плита») — + через мост MAME воспроизвести не удалось, а на уровне логики это + несколько строк: заспавнить кусок, сменить комнату, тикать до + приземления, проверить щебень. diff --git a/applications/PoP/roomtest/tests-host/bank_stub.s b/applications/PoP/roomtest/tests-host/bank_stub.s new file mode 100644 index 0000000..5e7921a --- /dev/null +++ b/applications/PoP/roomtest/tests-host/bank_stub.s @@ -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 diff --git a/applications/PoP/roomtest/tests-host/gen_kid_blob.py b/applications/PoP/roomtest/tests-host/gen_kid_blob.py new file mode 100644 index 0000000..1974d6b --- /dev/null +++ b/applications/PoP/roomtest/tests-host/gen_kid_blob.py @@ -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)} Б)") diff --git a/applications/PoP/roomtest/tests-host/golden/crouch.h b/applications/PoP/roomtest/tests-host/golden/crouch.h new file mode 100644 index 0000000..62fdebf --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/crouch.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/fall_ledge.h b/applications/PoP/roomtest/tests-host/golden/fall_ledge.h new file mode 100644 index 0000000..450567d --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/fall_ledge.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/jump_up.h b/applications/PoP/roomtest/tests-host/golden/jump_up.h new file mode 100644 index 0000000..98ddae4 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/jump_up.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/loose.h b/applications/PoP/roomtest/tests-host/golden/loose.h new file mode 100644 index 0000000..d1df6d1 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/loose.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/rjump_gap3.h b/applications/PoP/roomtest/tests-host/golden/rjump_gap3.h new file mode 100644 index 0000000..d2c141c --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/rjump_gap3.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/run_right.h b/applications/PoP/roomtest/tests-host/golden/run_right.h new file mode 100644 index 0000000..96c4fd6 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/run_right.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/stand.h b/applications/PoP/roomtest/tests-host/golden/stand.h new file mode 100644 index 0000000..8610c7c --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/stand.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/step_shift.h b/applications/PoP/roomtest/tests-host/golden/step_shift.h new file mode 100644 index 0000000..c6c0891 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/step_shift.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/golden/turn.h b/applications/PoP/roomtest/tests-host/golden/turn.h new file mode 100644 index 0000000..66f5dbd --- /dev/null +++ b/applications/PoP/roomtest/tests-host/golden/turn.h @@ -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, diff --git a/applications/PoP/roomtest/tests-host/kidblob.h b/applications/PoP/roomtest/tests-host/kidblob.h new file mode 100644 index 0000000..11f58aa --- /dev/null +++ b/applications/PoP/roomtest/tests-host/kidblob.h @@ -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 diff --git a/applications/PoP/roomtest/tests-host/record_golden.py b/applications/PoP/roomtest/tests-host/record_golden.py new file mode 100644 index 0000000..b21db80 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/record_golden.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +""" +record_golden.py — снять эталонные трассы для t_phys. + +Зачем скрипт, а не один прогон. Отчёт tcheck ограничен 768 байтами лога, а +трасс на все сценарии сильно больше. Поэтому набор умеет снимать РОВНО +ОДИН сценарий за прогон (-DSC_RECORD=), а перебор делает этот скрипт: +собирает набор 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()) diff --git a/applications/PoP/roomtest/tests-host/scene.c b/applications/PoP/roomtest/tests-host/scene.c new file mode 100644 index 0000000..1b333fb --- /dev/null +++ b/applications/PoP/roomtest/tests-host/scene.c @@ -0,0 +1,163 @@ +/* + * scene.c — реализация покадрового раннера (см. scene.h). + */ +#include +#include + +#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]; +/* Кромки шва: соседей у сцены нет, поэтому по краям стена (тайл 20). */ +static const uint8_t sc_edge[6] = { 20, 20, 20, 20, 20, 20 }; + +void sc_init(void) +{ + kid_blob_install(); /* таблицы туда, куда смотрит pop_kid */ + pop_kid_data_load("KID\\KID_DATA.BIN"); + 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, sc_edge, sc_edge); + 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); + 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.x = x; /* тайл задаёт ряд, X — фазу внутри тайла */ + pop_determine_col(); /* колонку пересчитать под новый X */ + pop_kid_hp_reset(); +} + +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); + + /* Порядок ровно как в главном цикле roomtest.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'); + } +} diff --git a/applications/PoP/roomtest/tests-host/scene.h b/applications/PoP/roomtest/tests-host/scene.h new file mode 100644 index 0000000..5dbe3a0 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/scene.h @@ -0,0 +1,88 @@ +/* + * 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 + +/* Одна запись трассы: то, чем оперируют решения движка. 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); + +/* Прогнать 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=), а перебором занимается record_golden.py. + * Формат: строка «### имя», дальше по строке на кадр, по два хекс-символа + * на байт. Раннер печатает лог только при провале — sc_dump ставит + * failed=1 намеренно. */ +void sc_dump(const char *name); + +#endif diff --git a/applications/PoP/roomtest/tests-host/stubs.c b/applications/PoP/roomtest/tests-host/stubs.c new file mode 100644 index 0000000..0e84a06 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/stubs.c @@ -0,0 +1,196 @@ +/* + * 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 +#include +#include +#include +#include +#include + +#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_draw.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_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; } + +/* ---- клавиатура: управляемая тестом --------------------------------- */ + +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; +} + +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; } +} + +/* ---- 2. соседние модули движка -------------------------------------- */ + +/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота), + * зелья (тип). Физика читает и пишет его напрямую через pop_trob_modif. */ +uint8_t tk_modif[30]; +uint8_t tk_start_room; + +uint8_t *pop_trob_modif(uint8_t room) { (void)room; return tk_modif; } +uint8_t pop_level_start_room(void) { 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) +{ log_put(TK_TROB, room, tilepos, (uint8_t)type); } + +void pop_trigger_button(uint8_t room, uint8_t tilepos, + uint8_t button_type, uint8_t modifier) +{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); } + +void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) +{ log_put(TK_REDRAW, tilepos, kind, pages); } + +void pop_set_redraw_above(uint8_t col, uint8_t kind, uint8_t pages) +{ 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) __banked +{ (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. состояние стража -------------------------------------------- */ + +pop_char_t Guard; +kframe pop_gframe; +int8_t can_guard_see_kid; +int8_t guard_refrac, kid_sword_strike; +int8_t offguard, holding_sword; diff --git a/applications/PoP/roomtest/tests-host/stubs.h b/applications/PoP/roomtest/tests-host/stubs.h new file mode 100644 index 0000000..c08e645 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/stubs.h @@ -0,0 +1,43 @@ +/* + * stubs.h — интерфейс тестового окружения (см. stubs.c). + * + * Что тест может задавать: «нажатые» клавиши (tk_key_down) и модификаторы + * тайлов комнаты (tk_modif). Что может проверять: журнал обращений + * физики к соседним модулям (tk_log). + */ +#ifndef POP_TESTS_STUBS_H +#define POP_TESTS_STUBS_H + +#include + +/* ---- клавиатура ------------------------------------------------------ */ +#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); + +#endif diff --git a/applications/PoP/roomtest/tests-host/t_grab.c b/applications/PoP/roomtest/tests-host/t_grab.c new file mode 100644 index 0000000..4efac36 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_grab.c @@ -0,0 +1,159 @@ +/* + * 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); + } +} + +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); + return 0; +} diff --git a/applications/PoP/roomtest/tests-host/t_phys.c b/applications/PoP/roomtest/tests-host/t_phys.c new file mode 100644 index 0000000..dc0f391 --- /dev/null +++ b/applications/PoP/roomtest/tests-host/t_phys.c @@ -0,0 +1,248 @@ +/* + * t_phys.c — характеризация физики Кида (pop_map + pop_kid + pop_ctrl). + * + * Назначение набора — СТРАХОВКА ПОД РЕФАКТОРИНГ, а не проверка + * правильности. Эталоны здесь сняты с текущего билда, то есть + * консервируют и текущие баги; правильность по-прежнему проверяется + * против SDLPoP. Смысл в другом: перенос физики с `Kid.` на `Char.` + * обязан быть поведение-сохраняющим, и «трасса до == трасса после» ловит + * ровно тот класс ошибок, который там возможен. + * + * Поэтому при осознанном изменении поведения эталон ПЕРЕСНИМАЕТСЯ (см. + * SC_RECORD ниже), а в коммите должно быть написано, почему трасса + * поменялась. Молча обновлённый эталон обесценивает весь набор. + * + * Снятие эталона: + * make -C applications/PoP/roomtest/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" + +/* Коды тайлов (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 (нумерация в порядке вызовов в + * 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(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) и попросить перерисовку. Это уже не + * характеризация, а содержательная проверка — заглушка-пустышка её не + * дала бы. Через trob loose у нас НЕ идёт (только пики/ворота), у него + * свой pop_loose_modif — поэтому спрашиваем именно про кусок. */ + TC_TRUE(tk_log_count(TK_MOB_SPAWN) > 0); + TC_TRUE(tk_log_count(TK_REDRAW) > 0); +#endif +} + +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"); +} + +/* ---- точка входа ----------------------------------------------------- */ + +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(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_running_jump_over_3tile_gap); +} diff --git a/testkit/host-tests.mk b/testkit/host-tests.mk index bae7b07..c3bb831 100644 --- a/testkit/host-tests.mk +++ b/testkit/host-tests.mk @@ -29,9 +29,17 @@ BUILD := build # --code-loc: стартовый код занимает 13 байт по 0x0000, код с 0x0200. # --data-loc 0x8000: данные заведомо выше кода. +# +# CODE_LOC переопределяется набором, если ему нужна нижняя память под +# данные: так делает t_phys в roomtest — он кладёт по 0x100 страницу +# таблиц анимации, которую на устройстве туда приводит маппинг W0. +# EXTRA_INC — заголовки, которых нет ни в testkit, ни в ENGINE_DIR +# (например libc/libbgi, когда модуль под тестом их включает). +CODE_LOC ?= 0x0200 +EXTRA_INC ?= CFLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size -I$(TESTKIT) \ - $(if $(ENGINE_DIR),-I$(ENGINE_DIR)) -LDFLAGS := -mz80 --no-std-crt0 --code-loc 0x0200 --data-loc 0x8000 + $(if $(ENGINE_DIR),-I$(ENGINE_DIR)) $(EXTRA_INC) $(EXTRA_CFLAGS) +LDFLAGS := -mz80 --no-std-crt0 --code-loc $(CODE_LOC) --data-loc 0x8000 SUITES := $(patsubst t_%.c,%,$(notdir $(wildcard t_*.c))) COMMON := $(BUILD)/crt0_ucsim.rel $(BUILD)/tcheck.rel