294. Manual core validator rom loop

Add a manual core_validator.py ROM boot loop.

Lesson 294 of 356 · tests/chapter_08_manual_main/test_294_manual_core_validator_rom_loop.py

File to create on root folder

core_validator.py

Why this step exists

The emulator now has enough startup-survival behavior to make a manual developer entry point useful:

iNES parsing
Mapper000/NROM
CPU reset vector
Console frame stepping
APU/audio no-op for out-of-scope audio
OAMDMA $4014
controller port 1 through $4016

This step makes core_validator.py a manual place to try a local ROM and keep the emulator running frame by frame without pygame.

Why core_validator.py instead of main.py? main.py is reserved for the visual pygame runner. core_validator.py remains a small non-visual runner that is useful when debugging the emulator core without a window or frontend event loop.

Important legal/testing rule

The tutorial repository must not include commercial ROM files. Automated tests must not require MarioBros.nes. A developer who wants to manually run Mario Bros. must provide their own legal local copy:

MarioBros.nes

Reference hash used during tutorial development [Mario Bros. (World).nes]:

MD5 5d7bcc400a2fb5fa27346da345d3bb62  MarioBros.nes
SHA1 314b6e46e814f955b52ac954f67dab849582fe77

This hash is only a manual reference. Tests must not require this file or this exact hash because users may have different legal dumps/revisions.

Suggested implementation example

from pathlib import Path

from emulator.bus.cpu_bus import CpuBus
from emulator.cartridge.cartridge import Cartridge
from emulator.console import Console
from emulator.cpu.cpu import CPU


ROM_PATH = Path("MarioBros.nes")
debug_mode = True


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()

    print(f"Loaded {ROM_PATH}")
    print(f"CPU reset PC = ${cpu.pc:04X}")
    print("Starting frame loop. Press Ctrl+C to stop.")

    try:
        while True:
            executed = console.step_until_next_frame()

            if debug_mode:
                print(
                    f"frame={console.ppu.frame} "
                    f"pc=${cpu.pc:04X} "
                    f"instructions={executed}"
                )
    except KeyboardInterrupt:
        print("

Stopped by user.")

if __name__ == "__main__":
    main()

Manual command

uv run python core_validator.py

Expected manual behavior

This command does not exit by itself. It keeps stepping frames until the developer presses Ctrl+C. That is expected because core_validator.py is a manual long-running ROM execution tool, not an automated test.

Why this test does not call main()

core_validator.py is a manual infinite loop. Automated tests must stay finite and must not require a local commercial ROM file. These tests inspect structure only.

Why debug_mode is only checked for existence: debug_mode is a manual control knob. Future tutorial steps may set it to True or False depending on what is being taught. Tests should not freeze that value.

Out of scope

  • pygame display
  • keyboard mapping
  • committed ROM fixtures
  • asserting core_validator.py produces correct gameplay
  • calling main() from pytest

Run this lesson

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