295. Ручной вывод фона main через pygame
Добавление ручного вывода фона через pygame в main_only_background.py.
Урок 295 из 356 · tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py
Файл для создания/обновления в корневой папке
main_only_background.pyЗачем нужен этот шаг
core_validator.py доказывает, что эмулятор может загрузить локальный ROM и выполнять кадры без pygame. main_only_background.py — это исторический визуальный ручной запускатель, отображающий только фон: он должен использовать pygame для отображения фонового Framebuffer, создаваемого эмулятором после каждого кадра.
Рекомендуемый порядок работы
Начните с копирования рабочей структуры из core_validator.py, затем добавьте только недостающие части, связанные с pygame/отображением:
- импортировать pygame
- импортировать draw_framebuffer из tools.show_framebuffer
- определить SCALE
- создать начальный framebuffer для размеров окна
- открыть окно pygame
- обрабатывать события pygame.QUIT
- после каждого шага кадра отрисовывать фоновый framebuffer
- отрисовать framebuffer и обновить экран
- вызвать pygame.quit() в блоке finally
Важная граница
pygame разрешён в main_only_background.py, поскольку это ручная/фронтенд точка входа. pygame не должен импортироваться модулями ядра эмулятора.
Важное юридическое правило/правило для тестов
Репозиторий учебного курса не должен включать коммерческие файлы ROM. Автоматические тесты не должны требовать MarioBros.nes или открывать реальное окно pygame.
Эталонный хеш, использованный при разработке курса [Mario Bros. (World).nes]:
MD5 5d7bcc400a2fb5fa27346da345d3bb62 MarioBros.nes
SHA1 314b6e46e814f955b52ac954f67dab849582fe77Этот хеш является лишь ручным ориентиром. Тесты не должны требовать этот файл или этот точный хеш, поскольку у пользователей могут быть другие легальные дампы/ревизии.
Пример предполагаемой реализации
from pathlib import Path
import pygame
from emulator.bus.cpu_bus import CpuBus
from emulator.cartridge.cartridge import Cartridge
from emulator.console import Console
from emulator.cpu.cpu import CPU
from tools.show_framebuffer import draw_framebuffer
ROM_PATH = Path("MarioBros.nes")
debug_mode = False
SCALE = 3
def main() -> None:
if not ROM_PATH.exists():
raise FileNotFoundError(
"MarioBros.nes not found. Provide your own legal local copy. "
"This file is intentionally not included in the tutorial repository."
)
cartridge = Cartridge.from_ines_bytes(ROM_PATH.read_bytes())
cpu_bus = CpuBus(cartridge=cartridge)
cpu = CPU(cpu_bus)
console = Console(cpu=cpu, ppu=cpu_bus.ppu)
cpu.reset()
framebuffer = console.render_background_framebuffer()
print(f"Loaded {ROM_PATH}")
print(f"CPU reset PC = ${cpu.pc:04X}")
print("Starting frame loop. Close the window or press Ctrl+C to stop.")
pygame.init()
try:
window = pygame.display.set_mode(
(framebuffer.width * SCALE, framebuffer.height * SCALE)
)
pygame.display.set_caption("NES Background")
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
executed = console.step_until_next_frame()
framebuffer = console.render_background_framebuffer()
draw_framebuffer(window, framebuffer, SCALE)
pygame.display.flip()
if debug_mode:
print(
f"frame={console.ppu.frame} "
f"pc=${cpu.pc:04X} "
f"instructions={executed}"
)
except KeyboardInterrupt:
print("Stopped by user.")
finally:
pygame.quit()
if __name__ == "__main__":
main()Ручная команда
uv run python main_only_background.pyОжидаемое ручное поведение
main_only_background.py открывает окно pygame и отображает текущий фоновый framebuffer. Окно может выглядеть неполным, поскольку спрайты в этом историческом запускателе не реализованы. Закройте окно или нажмите Ctrl+C, чтобы остановить.
Пример ожидаемой картинки, приблизительно
+------------------------------+
| |
| MARIO BROS. |
| |
| 1 PLAYER GAME A |
| 1 PLAYER GAME B |
| 2 PLAYER GAME A |
| 2 PLAYER GAME B |
| |
| background is shown |
| sprites are missing |
| |
+------------------------------+Через 30 секунд — 1 минуту вы также должны увидеть фон/раскладку, похожую на классический уровень Mario Bros. 1983 года. Спрайты всё ещё отсутствуют, но фоновая сцена должна создавать ощущение, что эмулятор ожил:
+------------------------------+
| I-0000 TOP-0000 II-0000 |
| |
| ==== ==== |
|== ==|
| |
| ────────────── |
|───── ─────|
| |
| |
| ─────────── POW ─────────── |
|==== ====|
|------------------------------|
+------------------------------+Это лишь приблизительный ASCII-набросок. Важный ручной сигнал в том, что тайлы фона/заголовка/уровня появляются и меняются со временем. Отсутствие движущихся персонажей/врагов ожидаемо, пока не реализован рендеринг спрайтов.
Замечание о производительности
Ручной запускатель на pygame может сейчас казаться медленным. Это ожидаемо на данном этапе. Текущий вспомогательный метод draw_framebuffer намеренно прост и отрисовывает множество масштабированных прямоугольников средствами Python. В будущем можно заменить его более быстрым способом передачи framebuffer, но этот шаг сосредоточен на ожидаемом визуальном результате и границах архитектуры, а не на скорости.
Почему этот тест не вызывает main()
main_only_background.py открывает реальное окно pygame и выполняет ручной цикл. Автоматические тесты должны оставаться конечными и проверять только структуру.
Вне рамок этого шага
- оптимизация быстрой передачи framebuffer
- сопоставление клавиатуры/геймпада pygame
- рендеринг спрайтов
- проверка точных визуальных пикселей из коммерческого ROM
- вызов main() из pytest
Запустить этот урок
uv run pytest tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py -v