295. 手动 main pygame 背景显示

为 main_only_background.py 添加手动 pygame 背景显示。

295 / 356 · tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py

在根目录创建/更新的文件

main_only_background.py

为什么需要这一步

core_validator.py 证明了模拟器可以在不使用 pygame 的情况下启动本地 ROM 并逐帧执行。main_only_background.py 是历史版本的仅背景可视化手动运行器:它应使用 pygame 在每帧之后显示模拟器生成的背景 Framebuffer。

推荐的工作流程

先复制 core_validator.py 中可用的结构,然后仅添加缺少的 pygame/显示部分:

  • 导入 pygame
  • 从 tools.show_framebuffer 导入 draw_framebuffer
  • 定义 SCALE
  • 创建初始帧缓冲区以确定窗口尺寸
  • 打开 pygame 窗口
  • 处理 pygame.QUIT 事件
  • 每帧执行后渲染背景帧缓冲区
  • 绘制帧缓冲区并刷新显示
  • 在 finally 中调用 pygame.quit()

重要边界

main_only_background.py 中允许使用 pygame,因为它是手动/前端入口点。模拟器核心模块不得导入 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 窗口并显示当前背景帧缓冲区。由于此历史运行器尚未实现精灵渲染,窗口可能看起来不完整。关闭窗口或按 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 绘制大量缩放的矩形。未来的优化可以用更快的帧缓冲区上传路径替换它,但本步骤聚焦于预期视觉输出和架构边界,而非速度。

为什么本测试不调用 main()

main_only_background.py 会打开真实的 pygame 窗口并运行手动循环。自动化测试必须保持有限执行,且仅应检查结构。

超出本步骤范围

  • 快速帧缓冲区上传优化
  • pygame 键盘/控制器映射
  • 精灵渲染
  • 验证商业 ROM 的精确视觉像素
  • 从 pytest 调用 main()

运行本课

uv run pytest tests/chapter_08_manual_main/test_295_manual_main_pygame_background_display.py -v