import hashlib from pathlib import Path import numpy as np import pytest from spriteforge.backends import BackendContext, create_backend from spriteforge.backends.procedural import generator_names from spriteforge.manifest import AssetSpec from spriteforge.postprocess import build_palette, encode_frames, normalize_asset def spec(generator:str,**params)->AssetSpec: return AssetSpec(backend="procedural",generator=generator,params=params,dirs=1,seed=123) @pytest.mark.parametrize("name",["floor_tile","wall_tile","decal"]) def test_generators_are_deterministic_and_nonempty(name): backend=create_backend("procedural");context=BackendContext(Path.cwd(),"test") a=backend.generate(spec(name),context);b=backend.generate(spec(name),context) assert np.array_equal(a.frames[0].rgba,b.frames[0].rgba) assert np.count_nonzero(a.frames[0].rgba[...,3])>0 assert a.frames[0].rgba.dtype==np.uint8 def test_seed_changes_noise(): backend=create_backend("procedural");context=BackendContext(Path.cwd(),"test") a=backend.generate(spec("floor_tile"),context) changed=spec("floor_tile").model_copy(update={"seed":124}) b=backend.generate(changed,context) assert not np.array_equal(a.frames[0].rgba,b.frames[0].rgba) def test_variants_and_postprocess_streams(): raw=create_backend("procedural").generate(spec("decal",variants=3,width=31,height=17),BackendContext(Path.cwd(),"x")) normalized=normalize_asset(raw,"24x12") palette=build_palette([normalized]);frames=encode_frames(normalized,palette,dither=True) assert len(frames)==3 and len(palette)==256 assert all(f.width<=24 and f.height<=12 for f in frames) assert all(f.normals_xy is not None and f.depth is not None for f in frames) def test_explicit_source_scale_downsamples_small_supersampled_silhouette(): raw=create_backend("procedural").generate(spec("decal",width=40,height=20),BackendContext(Path.cwd(),"x")) normalized=normalize_asset(raw,"96x112",0.25) frame=normalized.frames[0] assert frame.rgba.shape[1]<=10 and frame.rgba.shape[0]<=5 def test_unknown_generator_is_textual(): with pytest.raises(ValueError,match="available"): create_backend("procedural").generate(spec("missing"),BackendContext(Path.cwd(),"x")) def test_unknown_generator_parameter_is_rejected(): with pytest.raises(ValueError,match="heigth"): create_backend("procedural").generate(spec("wall_tile",heigth=20),BackendContext(Path.cwd(),"x")) def test_registry_contains_three_generators(): assert {"floor_tile","wall_tile","decal"}<=set(generator_names()) def test_single_frame_generator_still_refuses_extra_directions(): turned=spec("floor_tile").model_copy(update={"dirs":4}) with pytest.raises(ValueError,match="dirs: 1"): create_backend("procedural").generate(turned,BackendContext(Path.cwd(),"x")) # Слепки старых генераторов: расширение бэкенда последовательностями не имеет # права поменять ни один байт уже собранных ассетов. GOLDEN={"floor_tile":("4d1c4610ac48af25d8acdc8f33f6f75416ca493b764dbc45d35afd5d99d38cf5",{"width":32,"height":16,"variants":2}), "wall_tile":("3d89b51db3989e1c9fad63a3f0682d504a8f7a5055a4ab94c19a69351c344096",{"width":32,"top_height":16,"height":24}), "decal":("654dc4cff086dafb1285914e78120055e2b9adbcfd0697011968b6176697c17f",{"width":24,"height":12})} @pytest.mark.parametrize("name",sorted(GOLDEN)) def test_existing_generators_are_byte_identical(name): digest,params=GOLDEN[name] frozen=AssetSpec(backend="procedural",generator=name,params=params,dirs=1,seed=77,fps=10) asset=create_backend("procedural").generate(frozen,BackendContext(Path.cwd(),"t")) hasher=hashlib.sha256() for frame in asset.frames: hasher.update(frame.rgba.tobytes()) hasher.update(str((frame.pivot_x,frame.pivot_y,frame.duration_ms)).encode()) assert hasher.hexdigest()==digest