Files
Forge-Engine/tests/test_animal_sprite_library.py
T
williamandClaude Sonnet 5 d5a59bdbd8
Build and deploy / test-python (push) Failing after 6m10s
Build and deploy / test-js (push) Successful in 58s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Fusion des deux moteurs : type d'écran par écran, plus par projet
"document" (écrans %) et "jeu_2d" (scène pixels) devient une propriété
PAR ÉCRAN (_screens.kind, migration automatique idempotente dans
ensure_schema.py, source = l'ancien game_type au niveau projet) plutôt
qu'un choix figé pour tout le jeu — un même projet peut désormais
mélanger écrans classiques et scènes 2D librement.

- routes/screens/screen_edit.py : dispatch vers l'éditeur de scène selon
  screen["kind"] (l'écran demandé), plus game["game_type"].
- screens/payload/full_game_payload.py, templates/play.html,
  static/js/play/screens.js : le rendu jouable (payload, markup, bascule
  du mode plein-écran #playFrame) décide écran par écran, y compris en
  cours de partie (changer d'écran ne recharge pas la page).
- screens/screens_repo/create_screen.py : nouveau paramètre kind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:21:13 +02:00

164 lines
8.0 KiB
Python

"""Bibliothèque de sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY,
screens/labels/animal_sprite_library.py) — réservée au rôle admin
(licence CraftPix, voir core/sprite_gate.py et le plan "Bibliothèque de
sprites animaux CraftPix"). Un compte "user" ne doit ni la voir dans la
galerie de l'éditeur, ni pouvoir l'assigner par un POST direct."""
import json
import screens
def test_admin_sprite_library_loaded_from_manifest():
assert len(screens.ADMIN_SPRITE_LIBRARY) > 0
bunny = screens.ADMIN_SPRITE_LIBRARY["animal-bunny-01"]
assert bunny["family"] == "bunny"
assert bunny["animations"]["idle"][0] == "/static/characters/animals/bunny/01/idle0.png"
assert len(bunny["animations"]["idle"]) == 20
def test_sprite_library_is_public_plus_admin_merged():
assert set(screens.SPRITE_LIBRARY) == set(screens.PUBLIC_SPRITE_LIBRARY) | set(screens.ADMIN_SPRITE_LIBRARY)
assert screens.ADMIN_ONLY_CHARACTER_SLUGS == set(screens.ADMIN_SPRITE_LIBRARY)
assert "male-adventurer" not in screens.ADMIN_ONLY_CHARACTER_SLUGS
def test_sprite_gallery_families_shows_one_tile_per_animal():
families = screens.sprite_gallery_families(screens.SPRITE_LIBRARY)
slugs = [slug for slug, _ in families]
assert len(slugs) == len(screens.PUBLIC_SPRITE_LIBRARY) + 14 # 6 Kenney + 14 familles animales
assert "animal-bunny-01" in slugs
assert "animal-bunny-02" not in slugs
def test_document_editor_hides_animal_characters_from_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
html = user_client.get(f"/game/{user_game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" not in html
assert "male-adventurer" in html
def test_document_editor_shows_animal_characters_to_admin(game, client):
resp = client.post(f"/game/{game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" in html
def _create_jeu2d_game_as(post_client, name, slug_owner_client=None):
# Fusion des moteurs (voir screens/screens_repo/ensure_schema.py) : un
# jeu n'a plus de type propre — seul l'écran créé ensuite par chaque
# test (screens.create_screen(..., kind="jeu_2d")) est une scène.
resp = post_client.post("/games/new", data={"name": name}, follow_redirects=False)
assert resp.status_code == 302
return resp.headers["Location"].rstrip("/").split("/")[-1]
def test_scene_editor_hides_animal_characters_from_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_user"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
html = user_client.get(f"/game/{slug}/screens/{screen_id}/edit").get_data(as_text=True)
assert "animal-bunny-01" not in html
def test_scene_object_add_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_gate"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = user_client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
)
assert resp.status_code == 403
def test_scene_object_add_allows_animal_character_for_admin(client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin"))
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
resp = client.post(
f"/game/{slug}/screens/{screen_id}/scene-objects/add",
data={"kind": "personnage", "forge_character": "animal-bunny-01"},
follow_redirects=False,
)
assert resp.status_code == 302
obj_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
obj = screens.get_scene_object(slug, obj_id)
assert screens.resolve_personnage_data(obj)["forge_character"] == "animal-bunny-01"
def test_scene_object_personnage_swap_rejects_animal_character_for_a_user_account(client, user_client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_swap_gate"))
with user_client.session_transaction() as sess:
import auth
auth.set_project_slug(sess["user_id"], slug)
screen_id = screens.create_screen(slug, "Scène 1")
obj_id = screens.add_scene_object(slug, screen_id)
resp = user_client.post(
f"/game/{slug}/scene-objects/{obj_id}/personnage",
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
content_type="application/json",
)
assert resp.status_code == 403
def test_element_add_rejects_animal_character_for_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
resp = user_client.post(
f"/game/{user_game}/screens/{screen_id}/elements/add",
data={"widget": "personnage", "forge_character": "animal-bunny-01"},
)
assert resp.status_code == 403
def test_element_set_personnage_data_rejects_animal_character_for_a_user_account(user_game, user_client):
resp = user_client.post(f"/game/{user_game}/screens/new", data={"name": "Accueil"}, follow_redirects=False)
screen_id = resp.headers["Location"].rstrip("/").split("/")[-2]
resp = user_client.post(
f"/game/{user_game}/screens/{screen_id}/elements/add", data={"widget": "personnage"}, follow_redirects=False
)
element_id = int(resp.headers["Location"].rsplit("selected=", 1)[1])
resp = user_client.post(
f"/game/{user_game}/elements/{element_id}/personnage",
data=json.dumps({"source": "forge", "forge_character": "animal-bunny-01"}),
content_type="application/json",
)
assert resp.status_code == 403
def test_list_used_forge_characters_scans_elements_and_scene_objects(client, tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_used_characters"))
screen_id = screens.create_screen(slug, "Scène 1")
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
screens.add_scene_object(slug, screen_id, kind="decor")
used = screens.list_used_forge_characters(slug)
assert used == {"animal-bunny-01"}
def test_build_package_only_copies_used_animal_character_folders(client, tmp_game_slug_cleanup, tmp_path):
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_publish_animals"))
screen_id = screens.create_screen(slug, "Scène 1")
screens.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
from publish import build_package
zip_path, _name = build_package.build_game_zip(slug)
try:
import zipfile
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
animal_paths = {n for n in names if "static/characters/animals/" in n and not n.endswith("manifest.json")}
animals_used = {n.split("static/characters/animals/")[1].split("/")[0] for n in animal_paths}
assert animals_used == {"bunny"}
assert any("static/characters/animals/bunny/01/" in n for n in animal_paths)
assert not any("static/characters/animals/bear/" in n for n in animal_paths)
assert any(n.endswith("static/characters/animals/manifest.json") for n in names)
assert any("static/characters/male-adventurer/" in n for n in names)
finally:
import os
os.remove(zip_path)