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