1169 lines
46 KiB
Python
1169 lines
46 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
from app.core.colors import custom_random_color_choices
|
|
from app.config.xml_mapping import load_config
|
|
from app.core.pattern_engine import PatternEngine
|
|
from app.core.types import PatternParameters
|
|
from app.patterns.base import PatternContext
|
|
from app.patterns.builtin import SnakePattern
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SAMPLE_MAPPING = ROOT / "sample_data" / "infinity_mirror_mapping_clean.xml"
|
|
RANDOM_EFFECT_COLORS = {
|
|
(255, 30, 30),
|
|
(255, 107, 0),
|
|
(255, 188, 0),
|
|
(178, 255, 0),
|
|
(0, 255, 76),
|
|
(0, 219, 255),
|
|
(0, 81, 255),
|
|
(178, 0, 255),
|
|
}
|
|
|
|
|
|
def _matches_scaled_random_effect_color(color: tuple[int, int, int]) -> bool:
|
|
if color == (0, 0, 0):
|
|
return False
|
|
for base in RANDOM_EFFECT_COLORS:
|
|
scales = [channel / reference for channel, reference in zip(color, base) if channel > 1 and reference > 0]
|
|
if not scales:
|
|
continue
|
|
scale = sum(scales) / len(scales)
|
|
if all(abs(channel - (reference * scale)) <= 3.0 for channel, reference in zip(color, base)):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _matches_scaled_choice_color(color: tuple[int, int, int], choices: set[tuple[int, int, int]]) -> bool:
|
|
if color == (0, 0, 0):
|
|
return False
|
|
for base in choices:
|
|
scales = [channel / reference for channel, reference in zip(color, base) if channel > 1 and reference > 0]
|
|
if not scales:
|
|
continue
|
|
scale = sum(scales) / len(scales)
|
|
if all(abs(channel - (reference * scale)) <= 3.0 for channel, reference in zip(color, base)):
|
|
return True
|
|
return False
|
|
|
|
|
|
class PatternEngineTests(unittest.TestCase):
|
|
def test_sample_mapping_tile_segments_follow_counterclockwise_band(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
tile = config.sorted_tiles()[0]
|
|
|
|
self.assertEqual(
|
|
[(segment.side, segment.start_channel, segment.led_count, segment.reverse) for segment in tile.segments],
|
|
[
|
|
("left", 1, 27, False),
|
|
("bottom", 82, 27, False),
|
|
("right", 163, 27, True),
|
|
("top", 244, 25, True),
|
|
],
|
|
)
|
|
|
|
def test_engine_renders_all_tiles_and_leds(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
frame = engine.render_frame(config, "checker", PatternParameters(), timestamp=1.23)
|
|
|
|
self.assertEqual(len(frame.tiles), 18)
|
|
first_tile = frame.tiles["r1c1"]
|
|
self.assertEqual(len(first_tile.led_pixels), 4)
|
|
self.assertEqual(sum(len(colors) for colors in first_tile.led_pixels.values()), 106)
|
|
|
|
def test_utility_single_tile_only_lights_selected_tile(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
frame = engine.render_frame(
|
|
config,
|
|
"solid",
|
|
PatternParameters(),
|
|
utility_mode="single_tile",
|
|
selected_tile_id="r2c3",
|
|
timestamp=2.0,
|
|
)
|
|
|
|
lit_tiles = [tile_id for tile_id, tile in frame.tiles.items() if tile.fill_color.to_8bit_tuple() != (0, 0, 0)]
|
|
self.assertEqual(lit_tiles, ["r2c3"])
|
|
|
|
def test_solid_pattern_hits_selected_color_and_led_output(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
frame = engine.render_frame(config, "solid", params, timestamp=3.0)
|
|
first_tile = frame.tiles["r1c1"]
|
|
|
|
self.assertEqual(first_tile.fill_color.to_8bit_tuple(), (0, 0, 255))
|
|
first_segment = next(iter(first_tile.led_pixels.values()))
|
|
self.assertTrue(first_segment)
|
|
self.assertEqual(first_segment[0].to_8bit_tuple(), (0, 0, 255))
|
|
|
|
def test_new_low_res_patterns_are_registered(self) -> None:
|
|
engine = PatternEngine()
|
|
descriptors = {descriptor.pattern_id: descriptor for descriptor in engine.descriptors()}
|
|
ids = set(descriptors)
|
|
|
|
self.assertTrue({"scan", "scan_dual", "saw", "sweep", "two_dots", "wave_line", "stopwatch", "snake", "strobe", "sparkle"}.issubset(ids))
|
|
self.assertIn("checker_mode", descriptors["checker"].supported_parameters)
|
|
self.assertIn("angle", descriptors["scan"].supported_parameters)
|
|
self.assertIn("tempo_multiplier", descriptors["scan"].supported_parameters)
|
|
self.assertIn("on_width", descriptors["scan"].supported_parameters)
|
|
self.assertIn("off_width", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("speed", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("step_size", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("band_thickness", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("flip_horizontal", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("flip_vertical", descriptors["scan"].supported_parameters)
|
|
self.assertNotIn("level_mode", descriptors["scan"].supported_parameters)
|
|
self.assertIn("strobe_mode", descriptors["strobe"].supported_parameters)
|
|
self.assertNotIn("level_mode", descriptors["strobe"].supported_parameters)
|
|
self.assertNotIn("randomness", descriptors["strobe"].supported_parameters)
|
|
self.assertIn("pixel_group_size", descriptors["strobe"].supported_parameters)
|
|
self.assertIn("strobe_duty_cycle", descriptors["strobe"].supported_parameters)
|
|
self.assertIn("center_pulse_mode", descriptors["center_pulse"].supported_parameters)
|
|
self.assertNotIn("symmetry", descriptors["center_pulse"].supported_parameters)
|
|
self.assertIn("stopwatch_mode", descriptors["stopwatch"].supported_parameters)
|
|
self.assertIn("tempo_multiplier", descriptors["stopwatch"].supported_parameters)
|
|
self.assertNotIn("level_mode", descriptors["stopwatch"].supported_parameters)
|
|
self.assertNotIn("randomness", descriptors["sparkle"].supported_parameters)
|
|
self.assertIn("strobe_duty_cycle", descriptors["sparkle"].supported_parameters)
|
|
self.assertNotIn("symmetry", descriptors["checker"].supported_parameters)
|
|
|
|
def test_checker_diagonal_split_reaches_preview_metadata_and_leds(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
checker_mode="diagonal",
|
|
color_mode="dual",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = engine.render_frame(config, "checker", params, timestamp=0.0)
|
|
tile = frame.tiles["r1c1"]
|
|
diagonal_split = tile.metadata.get("diagonal_split")
|
|
|
|
self.assertIsInstance(diagonal_split, dict)
|
|
self.assertEqual(diagonal_split["orientation"], "backslash")
|
|
self.assertEqual(
|
|
frame.tiles["r1c1"].metadata["diagonal_split"]["orientation"],
|
|
frame.tiles["r2c1"].metadata["diagonal_split"]["orientation"],
|
|
)
|
|
self.assertEqual(
|
|
frame.tiles["r1c1"].metadata["diagonal_split"]["orientation"],
|
|
frame.tiles["r1c2"].metadata["diagonal_split"]["orientation"],
|
|
)
|
|
led_colors = {
|
|
color.to_8bit_tuple()
|
|
for segment_pixels in tile.led_pixels.values()
|
|
for color in segment_pixels
|
|
}
|
|
self.assertIn((255, 255, 255), led_colors)
|
|
self.assertIn((0, 0, 0), led_colors)
|
|
|
|
def test_checkerd_flips_diagonal_instead_of_shifting_colors(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
checker_mode="checkerd",
|
|
color_mode="dual",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame_0 = PatternEngine().render_frame(config, "checker", params, timestamp=0.0)
|
|
frame_1 = PatternEngine().render_frame(config, "checker", params, timestamp=1.01)
|
|
split_0 = frame_0.tiles["r1c1"].metadata["diagonal_split"]
|
|
split_1 = frame_1.tiles["r1c1"].metadata["diagonal_split"]
|
|
|
|
self.assertNotEqual(split_0["orientation"], split_1["orientation"])
|
|
self.assertEqual(split_0["color_a"].to_8bit_tuple(), split_1["color_a"].to_8bit_tuple())
|
|
self.assertEqual(split_0["color_b"].to_8bit_tuple(), split_1["color_b"].to_8bit_tuple())
|
|
self.assertEqual(
|
|
frame_0.tiles["r1c1"].metadata["diagonal_split"]["orientation"],
|
|
frame_0.tiles["r2c1"].metadata["diagonal_split"]["orientation"],
|
|
)
|
|
self.assertEqual(
|
|
frame_0.tiles["r1c1"].metadata["diagonal_split"]["orientation"],
|
|
frame_0.tiles["r1c2"].metadata["diagonal_split"]["orientation"],
|
|
)
|
|
|
|
def test_checkerd_mono_off_half_reaches_true_black(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
checker_mode="checkerd",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "checker", params, timestamp=0.0)
|
|
led_colors = {
|
|
color.to_8bit_tuple()
|
|
for segment_pixels in frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment_pixels
|
|
}
|
|
|
|
self.assertIn((0, 0, 0), led_colors)
|
|
self.assertNotIn((71, 71, 71), led_colors)
|
|
self.assertIn((255, 255, 255), led_colors)
|
|
|
|
def test_arrow_pattern_matches_discrete_double_chevron_layout(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
block_size=1.0,
|
|
direction="left_to_right",
|
|
color_mode="dual",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = engine.render_frame(config, "arrow", params, timestamp=0.0)
|
|
active_tiles = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if tile.fill_color.to_8bit_tuple() == (255, 255, 255)
|
|
}
|
|
|
|
self.assertEqual(active_tiles, {"r1c1", "r1c4", "r2c2", "r2c5", "r3c1", "r3c4"})
|
|
|
|
def test_row_chase_advances_one_clean_row_at_a_time(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
block_size=1.0,
|
|
direction="top_to_bottom",
|
|
color_mode="dual",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected = [
|
|
{"r1c1", "r1c2", "r1c3", "r1c4", "r1c5", "r1c6"},
|
|
{"r2c1", "r2c2", "r2c3", "r2c4", "r2c5", "r2c6"},
|
|
{"r3c1", "r3c2", "r3c3", "r3c4", "r3c5", "r3c6"},
|
|
{"r1c1", "r1c2", "r1c3", "r1c4", "r1c5", "r1c6"},
|
|
]
|
|
|
|
for timestamp, active_row in zip((0.0, 1.01, 2.01, 3.01), expected):
|
|
frame = PatternEngine().render_frame(config, "row_chase", params, timestamp=timestamp)
|
|
active_tiles = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if tile.fill_color.to_8bit_tuple() != (0, 0, 0)
|
|
}
|
|
self.assertEqual(active_tiles, active_row)
|
|
|
|
def test_wave_line_matches_requested_3x3_diagonal_cycle(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
direction="left_to_right",
|
|
color_mode="dual",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected_frames = [
|
|
{"r1c1", "r1c5", "r2c2", "r2c4", "r2c6", "r3c3"},
|
|
{"r1c2", "r1c6", "r2c1", "r2c3", "r2c5", "r3c4"},
|
|
{"r1c3", "r2c2", "r2c4", "r2c6", "r3c1", "r3c5"},
|
|
{"r1c4", "r2c1", "r2c3", "r2c5", "r3c2", "r3c6"},
|
|
]
|
|
|
|
for timestamp, expected_active in zip((0.0, 1.01, 2.01, 3.01), expected_frames):
|
|
frame = PatternEngine().render_frame(config, "wave_line", params, timestamp=timestamp)
|
|
active = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if tile.fill_color.to_8bit_tuple() == (255, 255, 255)
|
|
}
|
|
self.assertEqual(active, expected_active)
|
|
self.assertEqual(len(active), 6)
|
|
active_rows = {int(tile_id[1]) for tile_id in active}
|
|
self.assertEqual(active_rows, {1, 2, 3})
|
|
|
|
def test_wave_line_mono_inactive_tiles_can_fall_fully_black(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
direction="left_to_right",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "wave_line", params, timestamp=0.0)
|
|
self.assertEqual(frame.tiles["r1c2"].fill_color.to_8bit_tuple(), (0, 0, 0))
|
|
|
|
def test_diagonal_scan_uses_triangle_half_steps(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
direction="left_to_right",
|
|
scan_style="line",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected_frames = [
|
|
{("r3c1", "b")},
|
|
{("r3c1", "a")},
|
|
{("r2c1", "b"), ("r3c2", "b")},
|
|
{("r2c1", "a"), ("r3c2", "a")},
|
|
]
|
|
|
|
for timestamp, expected_active in zip((0.0, 1.01, 2.01, 3.01), expected_frames):
|
|
frame = PatternEngine().render_frame(config, "diagonal_scan", params, timestamp=timestamp)
|
|
active_halves: set[tuple[str, str]] = set()
|
|
for tile_id, tile in frame.tiles.items():
|
|
split = tile.metadata.get("diagonal_split")
|
|
self.assertIsInstance(split, dict)
|
|
self.assertEqual(split["orientation"], "backslash")
|
|
if split["color_a"].to_8bit_tuple() != (0, 0, 0):
|
|
active_halves.add((tile_id, "a"))
|
|
if split["color_b"].to_8bit_tuple() != (0, 0, 0):
|
|
active_halves.add((tile_id, "b"))
|
|
self.assertEqual(active_halves, expected_active)
|
|
|
|
def test_diagonal_scan_bands_fill_the_wall_with_moving_diagonal_stripes(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
direction="left_to_right",
|
|
scan_style="bands",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected_frames = [
|
|
("baxb.a", "xbaxb.", "xx.axb"),
|
|
("xbaxb.", "axbaxb", ".axbax"),
|
|
("axbaxb", ".axbax", "b.axba"),
|
|
(".axbax", "b.axba", "xb.axb"),
|
|
]
|
|
|
|
for timestamp, expected_rows in zip((0.0, 1.01, 2.01, 3.01), expected_frames):
|
|
frame = PatternEngine().render_frame(config, "diagonal_scan", params, timestamp=timestamp)
|
|
rows: list[str] = []
|
|
for row in range(1, 4):
|
|
chars: list[str] = []
|
|
for col in range(1, 7):
|
|
split = frame.tiles[f"r{row}c{col}"].metadata["diagonal_split"]
|
|
active_a = split["color_a"].to_8bit_tuple() != (0, 0, 0)
|
|
active_b = split["color_b"].to_8bit_tuple() != (0, 0, 0)
|
|
chars.append("x" if active_a and active_b else "a" if active_a else "b" if active_b else ".")
|
|
rows.append("".join(chars))
|
|
self.assertEqual(tuple(rows), expected_rows)
|
|
|
|
def test_diagonal_scan_bands_keep_parallel_stripes_in_vertical_mode(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
direction="top_to_bottom",
|
|
scan_style="bands",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected_frames = [
|
|
("baxb.a", "xbaxb.", "xx.axb"),
|
|
("xbaxb.", "axbaxb", ".axbax"),
|
|
("axbaxb", ".axbax", "b.axba"),
|
|
]
|
|
|
|
for timestamp, expected_rows in zip((0.0, 1.01, 2.01), expected_frames):
|
|
frame = PatternEngine().render_frame(config, "diagonal_scan", params, timestamp=timestamp)
|
|
rows: list[str] = []
|
|
for row in range(1, 4):
|
|
chars: list[str] = []
|
|
for col in range(1, 7):
|
|
split = frame.tiles[f"r{row}c{col}"].metadata["diagonal_split"]
|
|
self.assertEqual(split["orientation"], "backslash")
|
|
active_a = split["color_a"].to_8bit_tuple() != (0, 0, 0)
|
|
active_b = split["color_b"].to_8bit_tuple() != (0, 0, 0)
|
|
chars.append("x" if active_a and active_b else "a" if active_a else "b" if active_b else ".")
|
|
rows.append("".join(chars))
|
|
self.assertEqual(tuple(rows), expected_rows)
|
|
|
|
def test_scan_225_degree_line_crosses_the_wall_instead_of_sticking_to_one_corner(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
angle=225.0,
|
|
scan_style="line",
|
|
on_width=1.0,
|
|
off_width=1.0,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "scan", params, timestamp=5.01)
|
|
active = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if tile.fill_color.to_8bit_tuple() != (0, 0, 0)
|
|
}
|
|
|
|
self.assertGreaterEqual(len(active), 5)
|
|
self.assertEqual({tile_id[1] for tile_id in active}, {"1", "2", "3"})
|
|
self.assertIn("r1c3", active)
|
|
self.assertIn("r3c1", active)
|
|
|
|
def test_scan_dual_direction_changes_which_side_is_leading(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
left_to_right = PatternParameters(
|
|
brightness=1.0,
|
|
direction="left_to_right",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
right_to_left = PatternParameters(
|
|
brightness=1.0,
|
|
direction="right_to_left",
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame_ltr = PatternEngine().render_frame(config, "scan_dual", left_to_right, timestamp=1.23)
|
|
frame_rtl = PatternEngine().render_frame(config, "scan_dual", right_to_left, timestamp=1.23)
|
|
ltr_profile = [frame_ltr.tiles[f"r1c{col}"].fill_color.to_8bit_tuple()[0] for col in range(1, 7)]
|
|
rtl_profile = [frame_rtl.tiles[f"r1c{col}"].fill_color.to_8bit_tuple()[0] for col in range(1, 7)]
|
|
|
|
self.assertEqual(ltr_profile, list(reversed(rtl_profile)))
|
|
self.assertNotEqual(ltr_profile, rtl_profile)
|
|
|
|
def test_saw_mono_can_drop_fully_to_black(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
direction="right_to_left",
|
|
color_mode="mono",
|
|
primary_color="#FFAA00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "saw", params, timestamp=0.0)
|
|
fills = {tile.fill_color.to_8bit_tuple() for tile in frame.tiles.values()}
|
|
|
|
self.assertIn((0, 0, 0), fills)
|
|
self.assertTrue(any(color != (0, 0, 0) for color in fills))
|
|
|
|
def test_center_pulse_expands_from_middle_outward(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame_center = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.0)
|
|
active_center = {
|
|
tile_id
|
|
for tile_id, tile in frame_center.tiles.items()
|
|
if max(tile.fill_color.to_8bit_tuple()) >= 248
|
|
}
|
|
self.assertEqual(active_center, {"r2c3", "r2c4"})
|
|
|
|
frame_ring = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.76)
|
|
active_ring = {
|
|
tile_id
|
|
for tile_id, tile in frame_ring.tiles.items()
|
|
if max(tile.fill_color.to_8bit_tuple()) >= 248
|
|
}
|
|
self.assertEqual(active_ring, {"r1c3", "r1c4", "r2c2", "r2c5", "r3c3", "r3c4"})
|
|
|
|
frame_outer = PatternEngine().render_frame(config, "center_pulse", params, timestamp=1.52)
|
|
active_outer = {
|
|
tile_id
|
|
for tile_id, tile in frame_outer.tiles.items()
|
|
if max(tile.fill_color.to_8bit_tuple()) >= 248
|
|
}
|
|
self.assertEqual(active_outer, {"r1c2", "r1c5", "r2c1", "r2c6", "r3c2", "r3c5"})
|
|
|
|
def test_center_pulse_reverse_starts_at_outermost_ring(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
center_pulse_mode="reverse",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.0)
|
|
active_tiles = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if max(tile.fill_color.to_8bit_tuple()) >= 248
|
|
}
|
|
|
|
self.assertEqual(active_tiles, {"r1c1", "r1c6", "r3c1", "r3c6"})
|
|
|
|
def test_center_pulse_dual_mode_inactive_tiles_can_fall_to_true_black(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="dual",
|
|
primary_color="#FF00FF",
|
|
secondary_color="#220044",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.0)
|
|
|
|
self.assertEqual(frame.tiles["r1c1"].fill_color.to_8bit_tuple(), (0, 0, 0))
|
|
self.assertTrue(all(color.to_8bit_tuple() == (0, 0, 0) for color in frame.tiles["r1c1"].led_pixels["1 - 81 LED 27"]))
|
|
|
|
def test_center_pulse_outline_only_uses_inner_rectangle_segments_first(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
center_pulse_mode="outline",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.0)
|
|
active_tiles = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if any(color.to_8bit_tuple() != (0, 0, 0) for segment in tile.led_pixels.values() for color in segment)
|
|
}
|
|
center_tile = frame.tiles["r2c3"]
|
|
top = {color.to_8bit_tuple() for color in center_tile.led_pixels["244 - 318 LED 25"]}
|
|
bottom = {color.to_8bit_tuple() for color in center_tile.led_pixels["82 - 162 LED 27"]}
|
|
left = {color.to_8bit_tuple() for color in center_tile.led_pixels["1 - 81 LED 27"]}
|
|
right = {color.to_8bit_tuple() for color in center_tile.led_pixels["163 - 243 LED 27"]}
|
|
|
|
self.assertEqual(active_tiles, {"r2c2", "r2c3", "r2c4", "r2c5"})
|
|
self.assertEqual(top, {(255, 255, 255)})
|
|
self.assertEqual(bottom, {(255, 255, 255)})
|
|
self.assertEqual(left, {(0, 0, 0)})
|
|
self.assertEqual(right, {(0, 0, 0)})
|
|
|
|
def test_center_pulse_outline_reverse_starts_with_outer_wall_perimeter(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
center_pulse_mode="outline_reverse",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "center_pulse", params, timestamp=0.0)
|
|
active_tiles = {
|
|
tile_id
|
|
for tile_id, tile in frame.tiles.items()
|
|
if any(color.to_8bit_tuple() != (0, 0, 0) for segment in tile.led_pixels.values() for color in segment)
|
|
}
|
|
|
|
self.assertEqual(
|
|
active_tiles,
|
|
{
|
|
"r1c1",
|
|
"r1c2",
|
|
"r1c3",
|
|
"r1c4",
|
|
"r1c5",
|
|
"r1c6",
|
|
"r2c1",
|
|
"r2c6",
|
|
"r3c1",
|
|
"r3c2",
|
|
"r3c3",
|
|
"r3c4",
|
|
"r3c5",
|
|
"r3c6",
|
|
},
|
|
)
|
|
|
|
def test_strobe_random_pixels_reaches_single_leds_inside_one_tile(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
strobe_mode="random_pixels",
|
|
strobe_duty_cycle=0.5,
|
|
color_mode="mono",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "strobe", params, timestamp=0.0)
|
|
first_tile = frame.tiles["r1c1"]
|
|
led_colors = {
|
|
color.to_8bit_tuple()
|
|
for segment_pixels in first_tile.led_pixels.values()
|
|
for color in segment_pixels
|
|
}
|
|
|
|
self.assertIn((0, 0, 0), led_colors)
|
|
self.assertIn((0, 0, 255), led_colors)
|
|
|
|
def test_strobe_random_pixels_can_group_multiple_leds_into_one_block(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
strobe_mode="random_pixels",
|
|
pixel_group_size=3.0,
|
|
strobe_duty_cycle=0.5,
|
|
color_mode="mono",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "strobe", params, timestamp=0.0)
|
|
first_segment = frame.tiles["r1c1"].led_pixels["1 - 81 LED 27"]
|
|
|
|
for index in range(1, len(first_segment)):
|
|
if first_segment[index].to_8bit_tuple() != first_segment[index - 1].to_8bit_tuple():
|
|
self.assertEqual(index % 3, 0)
|
|
|
|
def test_strobe_random_leds_can_change_inside_a_pixel_group(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
strobe_mode="random_leds",
|
|
pixel_group_size=3.0,
|
|
strobe_duty_cycle=0.5,
|
|
color_mode="mono",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "strobe", params, timestamp=0.0)
|
|
transition_indices = [
|
|
index
|
|
for segment in frame.tiles["r1c1"].led_pixels.values()
|
|
for index in range(1, len(segment))
|
|
if segment[index].to_8bit_tuple() != segment[index - 1].to_8bit_tuple()
|
|
]
|
|
|
|
self.assertTrue(transition_indices)
|
|
self.assertTrue(any(index % 3 != 0 for index in transition_indices))
|
|
|
|
def test_strobe_random_pixels_density_follows_duty_cycle(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
low = PatternParameters(
|
|
brightness=1.0,
|
|
strobe_mode="random_pixels",
|
|
strobe_duty_cycle=0.08,
|
|
color_mode="mono",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
high = PatternParameters(
|
|
brightness=1.0,
|
|
strobe_mode="random_pixels",
|
|
strobe_duty_cycle=0.9,
|
|
color_mode="mono",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
low_frame = PatternEngine().render_frame(config, "strobe", low, timestamp=0.0)
|
|
high_frame = PatternEngine().render_frame(config, "strobe", high, timestamp=0.0)
|
|
low_lit = sum(
|
|
1
|
|
for segment in low_frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
if color.to_8bit_tuple() != (0, 0, 0)
|
|
)
|
|
high_lit = sum(
|
|
1
|
|
for segment in high_frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
if color.to_8bit_tuple() != (0, 0, 0)
|
|
)
|
|
|
|
self.assertGreater(high_lit, low_lit)
|
|
|
|
def test_strobe_global_returns_directly_to_black_even_with_smoothing_enabled(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
engine = PatternEngine()
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
fade=0.35,
|
|
strobe_mode="global",
|
|
strobe_duty_cycle=0.5,
|
|
color_mode="mono",
|
|
primary_color="#FFFFFF",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
on_frame = engine.render_frame(config, "strobe", params, timestamp=0.0, tempo_bpm=120.0)
|
|
off_frame = engine.render_frame(config, "strobe", params, timestamp=0.07, tempo_bpm=120.0)
|
|
|
|
self.assertEqual(on_frame.tiles["r1c1"].fill_color.to_8bit_tuple(), (255, 255, 255))
|
|
self.assertEqual(off_frame.tiles["r1c1"].fill_color.to_8bit_tuple(), (0, 0, 0))
|
|
self.assertTrue(
|
|
all(
|
|
color.to_8bit_tuple() == (0, 0, 0)
|
|
for segment in off_frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
)
|
|
)
|
|
|
|
def test_sparkle_random_colors_changes_hue_between_buckets(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
strobe_duty_cycle=0.72,
|
|
color_mode="random_colors",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
pattern = PatternEngine().registry.get("sparkle")
|
|
samples_a = pattern.render(PatternContext(config=config, params=params, time_s=0.0, tempo_bpm=60.0, tempo_phase=0.0))
|
|
samples_b = pattern.render(PatternContext(config=config, params=params, time_s=1.01, tempo_bpm=60.0, tempo_phase=1.01))
|
|
colors_a = {sample.fill_color.to_8bit_tuple() for sample in samples_a.values() if sample.fill_color.to_8bit_tuple() != (0, 0, 0)}
|
|
colors_b = {sample.fill_color.to_8bit_tuple() for sample in samples_b.values() if sample.fill_color.to_8bit_tuple() != (0, 0, 0)}
|
|
|
|
self.assertTrue(colors_a)
|
|
self.assertTrue(colors_b)
|
|
self.assertTrue(all(_matches_scaled_random_effect_color(color) for color in colors_a))
|
|
self.assertTrue(all(_matches_scaled_random_effect_color(color) for color in colors_b))
|
|
self.assertNotEqual(colors_a, colors_b)
|
|
|
|
def test_sparkle_density_can_fall_back_to_black(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
strobe_duty_cycle=0.08,
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "sparkle", params, timestamp=0.0)
|
|
fills = {tile.fill_color.to_8bit_tuple() for tile in frame.tiles.values()}
|
|
|
|
self.assertIn((0, 0, 0), fills)
|
|
self.assertTrue(any(color != (0, 0, 0) for color in fills))
|
|
|
|
def test_random_colors_break_up_same_axis_tiles(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="random_colors",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
|
|
frame = PatternEngine().render_frame(config, "column_gradient", params, timestamp=0.0)
|
|
column_two = {
|
|
frame.tiles[f"r{row}c2"].fill_color.to_8bit_tuple()
|
|
for row in range(1, 4)
|
|
}
|
|
unique_fills = {tile.fill_color.to_8bit_tuple() for tile in frame.tiles.values()}
|
|
|
|
self.assertGreater(len(column_two), 1)
|
|
self.assertGreater(len(unique_fills), 6)
|
|
|
|
def test_custom_random_colors_follow_user_selected_color_family(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="custom_random",
|
|
primary_color="#FF0000",
|
|
secondary_color="#00FF00",
|
|
)
|
|
|
|
pattern = PatternEngine().registry.get("sparkle")
|
|
samples = pattern.render(PatternContext(config=config, params=params, time_s=0.0, tempo_bpm=60.0, tempo_phase=0.0))
|
|
visible_colors = {sample.fill_color.to_8bit_tuple() for sample in samples.values() if sample.fill_color.to_8bit_tuple() != (0, 0, 0)}
|
|
allowed_colors = {color.to_8bit_tuple() for color in custom_random_color_choices("#FF0000", "#00FF00")}
|
|
|
|
self.assertTrue(visible_colors)
|
|
self.assertTrue(all(_matches_scaled_choice_color(color, allowed_colors) for color in visible_colors))
|
|
self.assertGreaterEqual(len(visible_colors), 2)
|
|
|
|
def test_legacy_palette_names_map_to_new_event_palettes(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
legacy = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="palette",
|
|
palette="Steel Bloom",
|
|
)
|
|
canonical = PatternParameters(
|
|
brightness=1.0,
|
|
color_mode="palette",
|
|
palette="Magenta Drive",
|
|
)
|
|
|
|
legacy_frame = PatternEngine().render_frame(config, "solid", legacy, timestamp=0.0)
|
|
canonical_frame = PatternEngine().render_frame(config, "solid", canonical, timestamp=0.0)
|
|
|
|
self.assertEqual(
|
|
legacy_frame.tiles["r1c1"].fill_color.to_8bit_tuple(),
|
|
canonical_frame.tiles["r1c1"].fill_color.to_8bit_tuple(),
|
|
)
|
|
|
|
def test_stopwatch_sync_counts_leds_up_and_back_down(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
stopwatch_mode="sync",
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
expected_counts = {
|
|
0.0: 1,
|
|
0.06: 2,
|
|
0.11: 3,
|
|
5.31: 106,
|
|
5.36: 105,
|
|
}
|
|
|
|
for timestamp, expected in expected_counts.items():
|
|
frame = PatternEngine().render_frame(config, "stopwatch", params, timestamp=timestamp)
|
|
lit_counts = {
|
|
sum(1 for segment in tile.led_pixels.values() for color in segment if color.to_8bit_tuple() != (0, 0, 0))
|
|
for tile in frame.tiles.values()
|
|
}
|
|
self.assertEqual(lit_counts, {expected})
|
|
|
|
frame = PatternEngine().render_frame(config, "stopwatch", params, timestamp=0.0)
|
|
first_tile = frame.tiles["r1c1"]
|
|
segment_lit_counts = {
|
|
segment_name: sum(1 for color in colors if color.to_8bit_tuple() != (0, 0, 0))
|
|
for segment_name, colors in first_tile.led_pixels.items()
|
|
}
|
|
self.assertEqual(segment_lit_counts["1 - 81 LED 27"], 1)
|
|
self.assertEqual(segment_lit_counts["82 - 162 LED 27"], 0)
|
|
self.assertEqual(segment_lit_counts["163 - 243 LED 27"], 0)
|
|
self.assertEqual(segment_lit_counts["244 - 318 LED 25"], 0)
|
|
|
|
def test_stopwatch_does_not_skip_full_strip_at_low_preview_fps(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
tempo_multiplier=8.0,
|
|
stopwatch_mode="sync",
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
engine = PatternEngine()
|
|
counts = []
|
|
for index in range(40):
|
|
frame = engine.render_frame(config, "stopwatch", params, timestamp=index / 23.0, tempo_bpm=120.0)
|
|
counts.append(
|
|
sum(
|
|
1
|
|
for segment in frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
if color.to_8bit_tuple() != (0, 0, 0)
|
|
)
|
|
)
|
|
|
|
self.assertIn(106, counts)
|
|
|
|
def test_stopwatch_keeps_counterclockwise_channel_order(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
stopwatch_mode="sync",
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame_bottom = PatternEngine().render_frame(config, "stopwatch", params, timestamp=1.81)
|
|
bottom = frame_bottom.tiles["r1c1"].led_pixels["82 - 162 LED 27"]
|
|
bottom_lit = [index for index, color in enumerate(bottom) if color.to_8bit_tuple() != (0, 0, 0)]
|
|
self.assertEqual(bottom_lit, list(range(10)))
|
|
|
|
frame_right = PatternEngine().render_frame(config, "stopwatch", params, timestamp=2.81)
|
|
right = frame_right.tiles["r1c1"].led_pixels["163 - 243 LED 27"]
|
|
right_lit = [index for index, color in enumerate(right) if color.to_8bit_tuple() != (0, 0, 0)]
|
|
self.assertEqual(right_lit, list(range(3)))
|
|
|
|
def test_stopwatch_tempo_multiplier_speeds_up_fill(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
slow = PatternParameters(
|
|
brightness=1.0,
|
|
tempo_multiplier=1.0,
|
|
stopwatch_mode="sync",
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
fast = PatternParameters(
|
|
brightness=1.0,
|
|
tempo_multiplier=4.0,
|
|
stopwatch_mode="sync",
|
|
color_mode="mono",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
slow_frame = PatternEngine().render_frame(config, "stopwatch", slow, timestamp=1.0)
|
|
fast_frame = PatternEngine().render_frame(config, "stopwatch", fast, timestamp=1.0)
|
|
slow_count = sum(
|
|
1
|
|
for segment in slow_frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
if color.to_8bit_tuple() != (0, 0, 0)
|
|
)
|
|
fast_count = sum(
|
|
1
|
|
for segment in fast_frame.tiles["r1c1"].led_pixels.values()
|
|
for color in segment
|
|
if color.to_8bit_tuple() != (0, 0, 0)
|
|
)
|
|
|
|
self.assertGreater(fast_count, slow_count)
|
|
|
|
def test_stopwatch_random_mode_offsets_tiles_and_changes_cycle_colors(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
step_size=1.0,
|
|
stopwatch_mode="random",
|
|
color_mode="random_colors",
|
|
primary_color="#00FF00",
|
|
secondary_color="#000000",
|
|
)
|
|
|
|
frame_a = PatternEngine().render_frame(config, "stopwatch", params, timestamp=0.0)
|
|
frame_b = PatternEngine().render_frame(config, "stopwatch", params, timestamp=212.01)
|
|
|
|
counts = [
|
|
sum(1 for segment in tile.led_pixels.values() for color in segment if color.to_8bit_tuple() != (0, 0, 0))
|
|
for tile in frame_a.tiles.values()
|
|
]
|
|
self.assertGreater(len(set(counts)), 1)
|
|
|
|
colors_a = [
|
|
next((color.to_8bit_tuple() for segment in tile.led_pixels.values() for color in segment if color.to_8bit_tuple() != (0, 0, 0)), None)
|
|
for tile in frame_a.tiles.values()
|
|
]
|
|
colors_b = [
|
|
next((color.to_8bit_tuple() for segment in tile.led_pixels.values() for color in segment if color.to_8bit_tuple() != (0, 0, 0)), None)
|
|
for tile in frame_b.tiles.values()
|
|
]
|
|
self.assertTrue({color for color in colors_a if color is not None}.issubset(RANDOM_EFFECT_COLORS))
|
|
self.assertTrue({color for color in colors_b if color is not None}.issubset(RANDOM_EFFECT_COLORS))
|
|
self.assertNotEqual(colors_a, colors_b)
|
|
|
|
def test_snake_eats_apple_keeps_length_and_respawns_new_apple(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
snake = SnakePattern()
|
|
snake._shape = (config.logical_display.rows, config.logical_display.cols)
|
|
snake._snake = [(0, 0), (1, 0), (2, 0), (2, 1)]
|
|
snake._direction = (0, 1)
|
|
snake._apple = (0, 1)
|
|
snake._last_time_s = 0.0
|
|
snake._step_progress = 0.0
|
|
snake._blink_until_time = None
|
|
|
|
before = snake.render(PatternContext(config=config, params=params, time_s=0.0))
|
|
self.assertEqual(before["r1c2"].fill_color.to_8bit_tuple(), (255, 0, 0))
|
|
|
|
after = snake.render(PatternContext(config=config, params=params, time_s=0.46))
|
|
self.assertEqual(snake._snake[0], (0, 1))
|
|
self.assertEqual(len(snake._snake), 4)
|
|
self.assertNotEqual(snake._apple, (0, 1))
|
|
self.assertIsNotNone(snake._apple)
|
|
self.assertNotIn(snake._apple, snake._snake)
|
|
self.assertIsNotNone(snake._blink_until_time)
|
|
self.assertNotEqual(after["r1c2"].fill_color.to_8bit_tuple(), (255, 0, 0))
|
|
|
|
settled = snake.render(PatternContext(config=config, params=params, time_s=0.62))
|
|
self.assertEqual(settled["r1c2"].fill_color.to_8bit_tuple(), (0, 0, 255))
|
|
|
|
def test_snake_uses_single_body_color_and_secondary_for_apple(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
snake = SnakePattern()
|
|
snake._shape = (config.logical_display.rows, config.logical_display.cols)
|
|
snake._snake = [(0, 3), (0, 2), (0, 1), (0, 0)]
|
|
snake._direction = (0, 1)
|
|
snake._apple = (1, 1)
|
|
snake._last_time_s = 0.0
|
|
snake._step_progress = 0.0
|
|
snake._blink_until_time = None
|
|
|
|
samples = snake.render(PatternContext(config=config, params=params, time_s=0.0))
|
|
self.assertEqual(samples["r1c1"].fill_color.to_8bit_tuple(), (0, 0, 255))
|
|
self.assertEqual(samples["r1c2"].fill_color.to_8bit_tuple(), (0, 0, 255))
|
|
self.assertEqual(samples["r2c2"].fill_color.to_8bit_tuple(), (255, 0, 0))
|
|
|
|
def test_snake_prefers_forward_motion_until_it_has_to_turn(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
snake = SnakePattern()
|
|
snake._shape = (config.logical_display.rows, config.logical_display.cols)
|
|
snake._snake = [(1, 2), (1, 1), (1, 0), (0, 0)]
|
|
snake._direction = (0, 1)
|
|
snake._apple = (1, 5)
|
|
snake._last_time_s = 0.0
|
|
snake._step_progress = 0.0
|
|
snake._blink_until_time = None
|
|
|
|
snake.render(PatternContext(config=config, params=params, time_s=0.46))
|
|
self.assertEqual(snake._snake[0], (1, 3))
|
|
self.assertEqual(snake._direction, (0, 1))
|
|
|
|
def test_snake_tempo_changes_step_rate(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
low_params = PatternParameters(
|
|
brightness=1.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
high_params = PatternParameters(
|
|
brightness=1.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
low_snake = SnakePattern()
|
|
high_snake = SnakePattern()
|
|
for snake in (low_snake, high_snake):
|
|
snake._shape = (config.logical_display.rows, config.logical_display.cols)
|
|
snake._snake = [(1, 2), (1, 1), (1, 0), (0, 0)]
|
|
snake._direction = (0, 1)
|
|
snake._apple = (1, 5)
|
|
snake._last_time_s = 0.0
|
|
snake._step_progress = 0.0
|
|
snake._blink_until_time = None
|
|
|
|
low_snake.render(PatternContext(config=config, params=low_params, time_s=0.35, tempo_bpm=12.0))
|
|
high_snake.render(PatternContext(config=config, params=high_params, time_s=0.35, tempo_bpm=240.0))
|
|
|
|
self.assertEqual(low_snake._snake[0], (1, 2))
|
|
self.assertEqual(high_snake._snake[0], (1, 5))
|
|
|
|
def test_snake_does_not_catch_up_after_long_idle_gap(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=4.0,
|
|
randomness=0.0,
|
|
color_mode="dual",
|
|
primary_color="#0000FF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
snake = SnakePattern()
|
|
snake._shape = (config.logical_display.rows, config.logical_display.cols)
|
|
snake._snake = [(1, 2), (1, 1), (1, 0), (0, 0)]
|
|
snake._direction = (0, 1)
|
|
snake._apple = (1, 5)
|
|
snake._last_time_s = 0.0
|
|
snake._step_progress = 0.0
|
|
snake._blink_until_time = None
|
|
|
|
snake.render(PatternContext(config=config, params=params, time_s=2.0))
|
|
|
|
self.assertEqual(snake._snake[0], (1, 2))
|
|
self.assertEqual(snake._direction, (0, 1))
|
|
|
|
def test_snake_free_run_eventually_reaches_apple(self) -> None:
|
|
config = load_config(SAMPLE_MAPPING)
|
|
params = PatternParameters(
|
|
brightness=1.0,
|
|
speed=1.0,
|
|
randomness=0.35,
|
|
color_mode="dual",
|
|
primary_color="#4D7CFF",
|
|
secondary_color="#FF0000",
|
|
)
|
|
snake = SnakePattern()
|
|
ate_apple = False
|
|
previous_length = None
|
|
|
|
for index in range(20):
|
|
snake.render(PatternContext(config=config, params=params, time_s=index * 0.46))
|
|
if previous_length is None:
|
|
previous_length = len(snake._snake)
|
|
if snake._blink_until_time is not None:
|
|
ate_apple = True
|
|
break
|
|
|
|
self.assertTrue(ate_apple)
|
|
self.assertEqual(len(snake._snake), previous_length)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|