218. Mapper000 nrom

create

Lesson 218 of 356 · tests/chapter_02_rom_loading/test_218_mapper000_nrom.py

create emulator/cartridge/mapper000.py::Mapper000.read_prg for NROM CPU mapping.

Why this step exists

NROM-128 mirrors one 16 KiB bank across $8000-$FFFF; NROM-256 maps 32 KiB directly. Keeping this translation in the mapper prevents Cartridge from becoming hardware behavior. It uses the PRG and CHR payloads exposed by lesson 217.

Suggested implementation at this lesson boundary

from dataclasses import dataclass

PRG_ROM_START = 0x8000
PRG_ROM_END = 0xFFFF
NROM_128_SIZE = 16 * 1024
NROM_256_SIZE = 32 * 1024
CHR_ROM_SIZE = 8 * 1024


@dataclass(frozen=True)
class Mapper000:
    prg_rom: bytes
    chr_rom: bytes

    def read_prg(self, addr: int) -> int:
        if not (PRG_ROM_START <= addr <= PRG_ROM_END):
            raise ValueError(
                f"Address out of PRG ROM range: {addr:04X}"
            )
        if len(self.prg_rom) == NROM_128_SIZE:
            offset = (addr - PRG_ROM_START) % NROM_128_SIZE
        elif len(self.prg_rom) == NROM_256_SIZE:
            offset = addr - PRG_ROM_START
        else:
            raise ValueError(
                "Mapper000 supports only 16KB or 32KB PRG ROM"
            )
        return self.prg_rom[offset]

Invariants: accept only CPU $8000-$FFFF and exactly 16 or 32 KiB PRG; mirror only the 16 KiB case; retain PRG/CHR constructor order. A common mistake is indexing prg_rom with the CPU address directly or implementing the translation on Cartridge. read_chr, PPU routing, CHR RAM, writes, mirroring, and bank switching are out of scope for this step.

Out of scope for this step

1. Lesson 219 adds `read_chr` for the PPU-side CHR range.
2. Lesson 220 adds mapper selection.
3. PPU routing, CHR RAM, writes, mirroring, and bank switching come later.

Run this lesson

uv run pytest tests/chapter_02_rom_loading/test_218_mapper000_nrom.py -v