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.pyWhy 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 $4016This 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.nesReference hash used during tutorial development [Mario Bros. (World).nes]:
MD5 5d7bcc400a2fb5fa27346da345d3bb62 MarioBros.nes
SHA1 314b6e46e814f955b52ac954f67dab849582fe77This 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.pyExpected 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