from pathlib import Path
import pytest
from emulator.rendering.attribute_table import (
BYTES_PER_ROW,
TABLE_SIZE,
get_attribute_palette_id,
)
def pack_attribute_byte(
topleft: int,
topright: int,
bottomleft: int,
bottomright: int,
) -> int:
"""
Pack four 2-bit palette IDs into one attribute byte.
This helper mirrors the documentation formula and makes the test examples
easier to read.
"""
return (
(bottomright << 6)
| (bottomleft << 4)
| (topright << 2)
| (topleft << 0)
)
def make_attribute_table() -> bytearray:
"""Create a blank 64-byte attribute table."""
return bytearray(TABLE_SIZE)
def test_attribute_table_file_exists():
"""
Objective:
Keep attribute-table bit decoding separate from nametable rendering.
"""
assert Path("emulator/rendering/attribute_table.py").exists()
def test_attribute_table_declares_size_constants():
"""
Objective:
Name the visible nametable attribute-table geometry.
"""
assert TABLE_SIZE == 64
assert BYTES_PER_ROW == 8
def test_get_attribute_palette_id_function_exists():
"""
Objective:
Expose one helper for asking which background palette a tile coordinate uses.
"""
assert callable(get_attribute_palette_id)
def test_all_zero_attribute_table_selects_palette_zero_everywhere():
"""
Objective:
If all attribute bytes are zero, every quadrant selects palette ID 0.
"""
attribute_table = bytes(make_attribute_table())
assert get_attribute_palette_id(attribute_table, tile_x=0, tile_y=0) == 0
assert get_attribute_palette_id(attribute_table, tile_x=3, tile_y=3) == 0
assert get_attribute_palette_id(attribute_table, tile_x=31, tile_y=29) == 0
def test_top_left_quadrant_reads_bits_zero_and_one():
"""
Objective:
Tiles in the top-left 2x2 quadrant read bits 0-1 of the attribute byte.
Example byte:
top-left = 1
other quadrants = 0
binary: 0b00_00_00_01
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(
topleft=1,
topright=0,
bottomleft=0,
bottomright=0,
)
assert get_attribute_palette_id(bytes(attribute_table), 0, 0) == 1
assert get_attribute_palette_id(bytes(attribute_table), 1, 0) == 1
assert get_attribute_palette_id(bytes(attribute_table), 0, 1) == 1
assert get_attribute_palette_id(bytes(attribute_table), 1, 1) == 1
def test_top_right_quadrant_reads_bits_two_and_three():
"""
Objective:
Tiles in the top-right 2x2 quadrant read bits 2-3 of the attribute byte.
Example byte:
top-right = 2
binary contribution: 0b00_00_10_00
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(
topleft=0,
topright=2,
bottomleft=0,
bottomright=0,
)
assert get_attribute_palette_id(bytes(attribute_table), 2, 0) == 2
assert get_attribute_palette_id(bytes(attribute_table), 3, 0) == 2
assert get_attribute_palette_id(bytes(attribute_table), 2, 1) == 2
assert get_attribute_palette_id(bytes(attribute_table), 3, 1) == 2
def test_bottom_left_quadrant_reads_bits_four_and_five():
"""
Objective:
Tiles in the bottom-left 2x2 quadrant read bits 4-5 of the attribute byte.
This protects against a common bug: accidentally treating bottom-left like
top-left or bottom-right.
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(
topleft=0,
topright=0,
bottomleft=3,
bottomright=0,
)
assert get_attribute_palette_id(bytes(attribute_table), 0, 2) == 3
assert get_attribute_palette_id(bytes(attribute_table), 1, 2) == 3
assert get_attribute_palette_id(bytes(attribute_table), 0, 3) == 3
assert get_attribute_palette_id(bytes(attribute_table), 1, 3) == 3
def test_bottom_right_quadrant_reads_bits_six_and_seven():
"""
Objective:
Tiles in the bottom-right 2x2 quadrant read bits 6-7 of the attribute byte.
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(
topleft=0,
topright=0,
bottomleft=0,
bottomright=2,
)
assert get_attribute_palette_id(bytes(attribute_table), 2, 2) == 2
assert get_attribute_palette_id(bytes(attribute_table), 3, 2) == 2
assert get_attribute_palette_id(bytes(attribute_table), 2, 3) == 2
assert get_attribute_palette_id(bytes(attribute_table), 3, 3) == 2
def test_one_attribute_byte_can_store_four_different_palette_ids():
"""
Objective:
One attribute byte can select four different palette IDs for its four 2x2 tile
quadrants.
Packed example:
top-left = 0
top-right = 1
bottom-left = 2
bottom-right = 3
Byte layout:
0b11_10_01_00
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(
topleft=0,
topright=1,
bottomleft=2,
bottomright=3,
)
assert attribute_table[0] == 0b11_10_01_00
assert get_attribute_palette_id(bytes(attribute_table), 0, 0) == 0
assert get_attribute_palette_id(bytes(attribute_table), 2, 0) == 1
assert get_attribute_palette_id(bytes(attribute_table), 0, 2) == 2
assert get_attribute_palette_id(bytes(attribute_table), 2, 2) == 3
def test_neighboring_attribute_byte_starts_at_tile_x_four():
"""
Objective:
Since each attribute byte covers 4 tiles horizontally, tile_x=4 uses the next
attribute byte in the same attribute-table row.
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(0, 0, 0, 0)
attribute_table[1] = pack_attribute_byte(2, 0, 0, 0)
assert get_attribute_palette_id(bytes(attribute_table), 0, 0) == 0
assert get_attribute_palette_id(bytes(attribute_table), 4, 0) == 2
assert get_attribute_palette_id(bytes(attribute_table), 5, 1) == 2
def test_next_attribute_table_row_starts_at_tile_y_four():
"""
Objective:
Since each attribute byte covers 4 tiles vertically, tile_y=4 uses the next
attribute-table row.
"""
attribute_table = make_attribute_table()
attribute_table[0] = pack_attribute_byte(0, 0, 0, 0)
attribute_table[BYTES_PER_ROW] = pack_attribute_byte(3, 0, 0, 0)
assert get_attribute_palette_id(bytes(attribute_table), 0, 0) == 0
assert get_attribute_palette_id(bytes(attribute_table), 0, 4) == 3
assert get_attribute_palette_id(bytes(attribute_table), 1, 5) == 3
def test_attribute_table_must_be_64_bytes():
"""
Objective:
Attribute table decoding expects the 64-byte attribute area, not the full
1024-byte nametable page and not the 960-byte tile-ID area.
"""
with pytest.raises(ValueError, match="Attribute table must be 64 bytes"):
get_attribute_palette_id(bytes([0x00] * 63), tile_x=0, tile_y=0)