Show test codetest_310_background_opaque_mask.py from pathlib import Path
import pytest
from emulator.rendering.nametable_renderer import (
BACKGROUND_HEIGHT,
BACKGROUND_WIDTH,
NAMETABLE_SIZE,
BackgroundOpaqueMask,
build_background_opaque_mask,
)
from tests.chapter_09_sprite_rendering.test_304_render_one_sprite_8x8 import encode_chr_tile
def make_pattern_table_with_tiles (*tiles: bytes ) -> bytes :
"""Build a 4096-byte pattern table with provided tiles at the front."""
pattern_table = bytearray (0x1000 )
for tile_index, tile in enumerate (tiles):
start = tile_index * 16
pattern_table[start:start + 16 ] = tile
return bytes (pattern_table)
def make_tile_with_pixel (x: int , y: int , color_index: int ) -> bytes :
"""Create one CHR tile with a single configured pixel."""
grid = [[0 for _ in range (8 )] for _ in range (8 )]
grid[y][x] = color_index
return encode_chr_tile(grid)
def test_background_opaque_mask_type_alias_is_list_of_bool_runtime_shape ():
"""
Objective:
BackgroundOpaqueMask is intentionally a simple list[bool], not a dataclass.
"""
mask: BackgroundOpaqueMask = [False , True ]
assert mask == [False , True ]
def test_build_background_opaque_mask_has_one_entry_per_screen_pixel ():
"""
Objective:
The mask is per pixel, not per tile.
"""
pattern_table = make_pattern_table_with_tiles(make_tile_with_pixel(0 , 0 , 0 ))
nametable = bytes ([0 ] * NAMETABLE_SIZE)
mask = build_background_opaque_mask(pattern_table, nametable)
assert len (mask) == BACKGROUND_WIDTH * BACKGROUND_HEIGHT
assert len (mask) == 256 * 240
def test_background_color_index_zero_is_not_opaque ():
"""
Objective:
CHR color index 0 represents background transparency/backdrop for priority
decisions, so the mask entry should be False.
"""
pattern_table = make_pattern_table_with_tiles(make_tile_with_pixel(0 , 0 , 0 ))
nametable = bytes ([0 ] * NAMETABLE_SIZE)
mask = build_background_opaque_mask(pattern_table, nametable)
assert mask[0 ] is False
def test_background_color_indexes_one_two_three_are_opaque ():
"""
Objective:
CHR color indexes 1, 2, and 3 are non-transparent background pixels.
"""
tile_1 = make_tile_with_pixel(0 , 0 , 1 )
tile_2 = make_tile_with_pixel(0 , 0 , 2 )
tile_3 = make_tile_with_pixel(0 , 0 , 3 )
pattern_table = make_pattern_table_with_tiles(tile_1, tile_2, tile_3)
for tile_index in [0 , 1 , 2 ]:
nametable = bytes ([tile_index] + [0 ] * (NAMETABLE_SIZE - 1 ))
mask = build_background_opaque_mask(pattern_table, nametable)
assert mask[0 ] is True
def test_background_opaque_mask_maps_tile_pixel_to_screen_pixel ():
"""
Objective:
The mask index uses screen coordinates:
mask_index = screen_y * BACKGROUND_WIDTH + screen_x
"""
tile = make_tile_with_pixel(x=3 , y=4 , color_index=1 )
pattern_table = make_pattern_table_with_tiles(tile)
nametable = bytes ([0 ] * NAMETABLE_SIZE)
mask = build_background_opaque_mask(pattern_table, nametable)
expected_index = 4 * BACKGROUND_WIDTH + 3
assert mask[expected_index] is True
def test_background_opaque_mask_maps_later_nametable_tile_to_correct_screen_area ():
"""
Objective:
Nametable tile coordinates use 32 tiles per row and each tile covers 8x8 pixels.
"""
tile = make_tile_with_pixel(x=2 , y=1 , color_index=1 )
pattern_table = make_pattern_table_with_tiles(tile)
nametable = bytearray ([0 ] * NAMETABLE_SIZE)
tile_x = 5
tile_y = 7
nametable[tile_y * 32 + tile_x] = 0
mask = build_background_opaque_mask(pattern_table, bytes (nametable))
screen_x = tile_x * 8 + 2
screen_y = tile_y * 8 + 1
expected_index = screen_y * BACKGROUND_WIDTH + screen_x
assert mask[expected_index] is True
def test_build_background_opaque_mask_rejects_short_nametable ():
"""
Objective:
The helper expects the 960 visible tile bytes of one nametable.
"""
pattern_table = make_pattern_table_with_tiles(make_tile_with_pixel(0 , 0 , 1 ))
with pytest.raises(ValueError, match ="Nametable must be 960 bytes" ):
build_background_opaque_mask(pattern_table, bytes ([0 ] * (NAMETABLE_SIZE - 1 )))
def test_background_opaque_mask_does_not_need_attribute_or_palette_data ():
"""
Objective:
Opacity depends on CHR color index only, not palette selection.
"""
source = Path("emulator/rendering/nametable_renderer.py" ).read_text()
function_source = source[source.index("def build_background_opaque_mask" ) :]
assert "attribute_table" not in function_source
assert "palette_ram" not in function_source
def test_background_opaque_mask_does_not_import_pygame_or_sprite_renderer ():
"""
Objective:
The mask is pure background-rendering data. It should not depend on pygame or
sprite rendering.
"""
source = Path("emulator/rendering/nametable_renderer.py" ).read_text()
assert "import pygame" not in source
assert "sprite_renderer" not in sourceTest functions (9)
test_background_opaque_mask_type_alias_is_list_of_bool_runtime_shapetest_build_background_opaque_mask_has_one_entry_per_screen_pixeltest_background_color_index_zero_is_not_opaquetest_background_color_indexes_one_two_three_are_opaquetest_background_opaque_mask_maps_tile_pixel_to_screen_pixeltest_background_opaque_mask_maps_later_nametable_tile_to_correct_screen_areatest_build_background_opaque_mask_rejects_short_nametabletest_background_opaque_mask_does_not_need_attribute_or_palette_datatest_background_opaque_mask_does_not_import_pygame_or_sprite_renderer