McRogueFace/tests/unit/shade_sprite_test.py

190 lines
6.5 KiB
Python

"""Unit tests for shade_sprite module."""
import mcrfpy
import sys
import os
# Add project root to path so shade_sprite can be imported
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(script_dir))
if project_root not in sys.path:
sys.path.insert(0, project_root)
from shade_sprite import (
AnimatedSprite, CharacterAssembler, Direction,
PUNY_29, PUNY_24, CREATURE_RPGMAKER, SLIME,
ALL_FORMATS, detect_format, AnimFrame, AnimDef,
)
errors = []
def test(name, condition, msg=""):
if not condition:
errors.append(f"FAIL: {name} - {msg}")
print(f" FAIL: {name} {msg}")
else:
print(f" PASS: {name}")
# ---- Format definitions ----
print("=== Format Definitions ===")
test("PUNY_29 dimensions", PUNY_29.tile_w == 32 and PUNY_29.tile_h == 32)
test("PUNY_29 columns", PUNY_29.columns == 29)
test("PUNY_29 rows", PUNY_29.rows == 8)
test("PUNY_29 directions", PUNY_29.directions == 8)
test("PUNY_29 has 10 animations", len(PUNY_29.animations) == 10,
f"got {len(PUNY_29.animations)}: {list(PUNY_29.animations.keys())}")
test("PUNY_29 idle is looping", PUNY_29.animations["idle"].loop)
test("PUNY_29 death no chain", PUNY_29.animations["death"].chain_to is None)
test("PUNY_29 slash chains to idle", PUNY_29.animations["slash"].chain_to == "idle")
test("PUNY_24 dimensions", PUNY_24.tile_w == 32 and PUNY_24.tile_h == 32)
test("PUNY_24 columns", PUNY_24.columns == 24)
test("PUNY_24 has 8 animations", len(PUNY_24.animations) == 8,
f"got {len(PUNY_24.animations)}: {list(PUNY_24.animations.keys())}")
test("CREATURE_RPGMAKER tile", CREATURE_RPGMAKER.tile_w == 24 and CREATURE_RPGMAKER.tile_h == 24)
test("CREATURE_RPGMAKER 4-dir", CREATURE_RPGMAKER.directions == 4)
test("SLIME columns", SLIME.columns == 15)
test("SLIME 1-dir", SLIME.directions == 1)
test("ALL_FORMATS count", len(ALL_FORMATS) == 4)
# ---- Format detection ----
print("\n=== Format Detection ===")
test("detect 928x256 -> PUNY_29", detect_format(928, 256) is PUNY_29)
test("detect 768x256 -> PUNY_24", detect_format(768, 256) is PUNY_24)
test("detect 480x32 -> SLIME", detect_format(480, 32) is SLIME)
test("detect 72x96 -> CREATURE_RPGMAKER", detect_format(72, 96) is CREATURE_RPGMAKER)
test("detect unknown -> None", detect_format(100, 100) is None)
# ---- Direction ----
print("\n=== Direction ===")
test("Direction.S == 0", Direction.S == 0)
test("Direction.N == 4", Direction.N == 4)
test("Direction.E == 6", Direction.E == 6)
# 8-dir row mapping
test("8-dir S -> row 0", PUNY_29.direction_row(Direction.S) == 0)
test("8-dir N -> row 4", PUNY_29.direction_row(Direction.N) == 4)
test("8-dir E -> row 6", PUNY_29.direction_row(Direction.E) == 6)
# 4-dir mapping
test("4-dir S -> row 0", CREATURE_RPGMAKER.direction_row(Direction.S) == 0)
test("4-dir W -> row 1", CREATURE_RPGMAKER.direction_row(Direction.W) == 1)
test("4-dir E -> row 2", CREATURE_RPGMAKER.direction_row(Direction.E) == 2)
test("4-dir N -> row 3", CREATURE_RPGMAKER.direction_row(Direction.N) == 3)
test("4-dir SW -> row 0 (rounds to S)", CREATURE_RPGMAKER.direction_row(Direction.SW) == 0)
# ---- Sprite index calculation ----
print("\n=== Sprite Index ===")
# PUNY_29: row * 29 + col
test("PUNY_29 col=0 S -> 0", PUNY_29.sprite_index(0, Direction.S) == 0)
test("PUNY_29 col=5 S -> 5", PUNY_29.sprite_index(5, Direction.S) == 5)
test("PUNY_29 col=0 N -> 116", PUNY_29.sprite_index(0, Direction.N) == 4 * 29)
test("PUNY_29 col=5 E -> 6*29+5", PUNY_29.sprite_index(5, Direction.E) == 6 * 29 + 5)
# SLIME: always row 0 (1-dir)
test("SLIME col=3 any dir -> 3", SLIME.sprite_index(3, Direction.E) == 3)
# ---- AnimatedSprite (with mock sprite) ----
print("\n=== AnimatedSprite ===")
# Create a real mcrfpy.Sprite with a from_bytes texture for testing
tex_data = bytes([128, 128, 128, 255] * (768 * 256))
tex = mcrfpy.Texture.from_bytes(tex_data, 768, 256, 32, 32)
sprite = mcrfpy.Sprite(texture=tex, pos=(0, 0))
anim = AnimatedSprite(sprite, PUNY_24, Direction.S)
test("AnimatedSprite created", anim is not None)
test("AnimatedSprite starts with idle", anim.animation_name == "idle")
test("AnimatedSprite direction is S", anim.direction == Direction.S)
# Play walk
anim.play("walk")
test("play walk", anim.animation_name == "walk")
test("walk frame 0", anim.frame_index == 0)
# Tick through first frame (walk frame 0 is col 1, 200ms)
anim.tick(100) # half of 200ms
test("tick 100ms stays on frame 0", anim.frame_index == 0)
anim.tick(100) # now 200ms total -> advance to frame 1
test("tick 200ms advances to frame 1", anim.frame_index == 1)
# Sprite index should reflect col 2 (walk frame 1), row 0 (S)
expected_idx = 0 * 24 + 2 # row=0, col=2
test("sprite_index after advance",
sprite.sprite_index == expected_idx,
f"got {sprite.sprite_index}, expected {expected_idx}")
# Direction change
anim.set_direction(Direction.E)
test("direction changed to E", anim.direction == Direction.E)
# Same column but different row
expected_idx = 6 * 24 + 2 # row=6 (E), col=2
test("sprite_index after dir change",
sprite.sprite_index == expected_idx,
f"got {sprite.sprite_index}, expected {expected_idx}")
# One-shot animation
anim.play("slash")
test("slash starts at frame 0", anim.frame_index == 0)
# Slash: 4 frames of 100ms each, then chains to idle
anim.tick(100)
anim.tick(100)
anim.tick(100)
test("slash frame 3 after 300ms", anim.frame_index == 3)
anim.tick(100) # finish last frame -> chain to idle
test("slash chains to idle", anim.animation_name == "idle")
# Death animation (no chain)
anim.play("death")
anim.tick(100) # frame 0
anim.tick(100) # frame 1 (last frame for PUNY_24 death)
# death has chain_to=None, so it stays finished
# Wait for last frame to expire
anim.tick(1000) # way past duration
test("death finished", anim.finished)
test("death stays on last frame", anim.frame_index == 1)
# Invalid animation name
try:
anim.play("nonexistent")
test("invalid anim raises KeyError", False, "no exception")
except KeyError:
test("invalid anim raises KeyError", True)
# ---- CharacterAssembler (basic) ----
print("\n=== CharacterAssembler ===")
asm = CharacterAssembler(PUNY_24)
test("assembler created", asm is not None)
# Empty build should fail
try:
asm.build()
test("empty build raises", False, "no exception")
except ValueError:
test("empty build raises ValueError", True)
# ---- Summary ----
print()
if errors:
print(f"FAILED: {len(errors)} tests failed")
for e in errors:
print(f" {e}")
sys.exit(1)
else:
print("All tests passed!")
sys.exit(0)