DRAW-CHAR. Отрисовка персонажа сведена к одному набору функций над Char — как физика после GUARD-PHYS. В оригинале add_kid_to_objtable (seg008:22F0) и add_guard_to_objtable (seg008:2324) имеют идентичное тело и различаются окном (loadkid/loadshad), набором спрайтов и типом объекта, а redraw_at_char/redraw_at_char2 гейтов по charid не имеют вовсе. pop_gdraw.c -> pop_cdraw.c: pop_char_draw/heal/fore(who), слот POP_CH_KID / POP_CH_OPP; состояние слотов pop_cd[] в _DATA — читается из любого банка без трамплина. Проход окклюзии тоже один (pop_fore_over_char), pop_fore_over_kid больше нет. Починилось само (расхождения, которые и были ценой дублирования): у соперника не было clip_char; у Кида не было клипа полем 192 и ветки брызг «мёртв/падение»; char_width_half СТРАЖА считался по спрайту КИДА. Замер: _CODE 24 881 -> 20 524 (куча 2023 -> 6333), BANK2 -265, итого -3.2 КБ. Проверено пользователем в MAME; циан-полоса профиля подросла — оптимизация заведена отдельной задачей DRAW-COST. MEM-BANK2, шаг 1: общие «листья» слоя фона в РЕЗИДЕНТ (pop_tile.c/.h). Ограничение платформы: писучие данные банка лежат в _DATA и видны всем, а const-таблицы — в странице банка, из другого банка их не прочитать; трамплин же выбирается объявлением, то есть __banked на листе бьёт и по горячим вызывающим (654 такта). W1 замаплено всегда — оттуда обе половины зовут листья прямым call и читают таблицы напрямую. MEM-BANK2, шаг 2: дедуп внутри банка. wall_pattern 808 -> 394 и wall_rnd 786 -> 654: четыре ветки по виду стены отличались только набором кусков и числами в одной серии prandom — сведены к таблицам WP_PARTS и WR_RULE, порядок вызовов prandom сохранён дословно. Заодно: kid_seq_off больше не static const в kid_data.h (230 Б мёртвой копии в каждом из 9 модулей) — генератор pop_extract_kid_data.py отдаёт макро-инициализатор, массив определяет один pop_kid.c. Итог: BANK2 14 815 -> 11 942 (72.9 %, свободно 4442 Б), _CODE 22 556, куча 4301 Б. tests-host зелёные (65/39/53/1723/1); в MAME комната 1 совпала с дорефакторным снимком попиксельно (0 из 227 520), комната 3 — та же раскладка кладки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tests-host — модульные тесты движка roomtest
Прогоняются под ucsim_z80, обвязка общая — testkit/ (там же объяснение,
почему под z80, а не хостовым gcc, и грабли).
make host-tests # из корня, вместе с остальными
make -C applications/PoP/roomtest/tests-host # только эти
make -C applications/PoP/roomtest/tests-host t_geom
Что покрыто
| набор | модуль | что проверяет |
|---|---|---|
t_geom |
pop_geom.c |
геометрия комнаты (x_bump, y_land, y_to_row) и PRNG оригинала |
t_phys |
pop_map.c + pop_kid.c + pop_ctrl.c |
покадровые трассы физики Кида: стойка, бег, шаг с Shift, разворот, присед, прыжок вверх, падение с кромки, loose-плита, разбег-прыжок через провал |
t_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. Кадр гоняется ровно в том порядке,
что и в главном цикле 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
Три шва, каждый закрыт без правок продуктового кода:
- Таблицы анимации.
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), чтобы не налезть на блоб. - Окружение.
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) — можно проверять «плита отвалилась и попросила перерисовку», а не только координаты. __banked. Модули физики помечены как банковые, SDCC генерирует на них трамплин___sdcc_bcall_ehl, которому нужныset_bank/get_bank. В тесте память плоская —bank_stub.sотдаёт нулевой банк и пустое переключение, соблюдая контракт по регистрам.
Что харнесс НЕ покрывает и остаётся за MAME: отрисовка и окклюзия, реальные переключения банков, тайминги кадра, настоящая клавиатура.
Что нужно, чтобы двинуться дальше
- Доступ к странице уровня.
pop_level.cходит по абсолютным адресам ((uint8_t *)(LVL_DATA_OFF + …)послеgfx_w0_map). Дляpop_mapэто не потребовалось — карту комнаты в него ИНЪЕКТИРУЮТ черезpop_map_set, — но набор на самpop_level/pop_trobв это упрётся. Решение то же, что дляkid_data: положить страницу по нужному адресу и поднятьCODE_LOC. - Сценарии из
bug_closed.md. У каждой закрытой записи есть симптом и ожидаемое поведение — готовый список регрессионных кейсов, который стоит переносить вt_physпо мере надобности. - BUG-LOOSE-2 (гонка «уйти из комнаты раньше, чем долетит плита») — через мост MAME воспроизвести не удалось, а на уровне логики это несколько строк: заспавнить кусок, сменить комнату, тикать до приземления, проверить щебень.