Files
Forge-Engine/tests/game/test_animal_sprite_library.py
T
williamandClaude Sonnet 5 b2e933f322
Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur
Support de formation (voir docs/plan/PLAN.md), sans toucher a
l'architecture en couches existante :

- screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D,
  avant l'arrivee d'un second "moteur" cote document) : ~85 imports
  corriges, contrat import-linter mis a jour, meme forme de couches.
- routes/, scripts/, static/, templates/, tests/ : tout ce qui est
  propre au jeu 2D deplace dans un sous-dossier game/ de chacun
  (routes/game/, static/game/, templates/game/, tests/game/,
  scripts/game/) ; ce qui est partage par le site (auth, onboarding,
  dashboard, uploads, db/) reste a la racine de chaque dossier. Un
  sous-dossier document/ (vide) cree dans chacun pour le futur chantier.
- styles/ volontairement inchange : les 3 fichiers sources sont
  concatenes en un seul static/style.css charge par tout le site,
  scinder leur CONTENU (editeur vs partage) serait un refactor CSS
  distinct, pas un deplacement mecanique.
- Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en
  profondeur : copie des assets, URLs d'icones relatives a
  static/style.css (qui ne bouge pas), manifeste, wrapper SCORM.
- Deux regressions d'un sweep de renommage anterieur corrigees au passage
  (screens.js/screens/scene-objects incorrectement convertis en
  game_engine.js/game_engine/scene-objects dans des commentaires).
- Effet de bord Windows decouvert et corrige : git mv + Path.write_text
  convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers
  normalises en LF.
- .eslintrc.json/package.json : uniquement les chemins de glob mis a jour
  (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7
  reste volontairement non committee (package-lock.json restaure a la
  version precedente).

Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit,
lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ;
demarrage serveur + requetes HTTP manuelles confirmant que les assets
deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien.

SKIP=djlint : backlog H021 (styles inline) deja documente comme dette
assumee dans CODE_QUALITY.md section 6, aucun template touche par ce
commit au-dela d'un deplacement de fichier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 12:27:53 +02:00

151 lines
6.8 KiB
Python

"""Bibliothèque de sprites animaux CraftPix (game_engine.ADMIN_SPRITE_LIBRARY,
game_engine/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
from collections.abc import Callable
from pathlib import Path
from typing import Any
from flask.testing import FlaskClient
import game_engine
from tests.conftest import not_none
def test_admin_sprite_library_loaded_from_manifest() -> None:
assert len(game_engine.ADMIN_SPRITE_LIBRARY) > 0
bunny = game_engine.ADMIN_SPRITE_LIBRARY["animal-bunny-01"]
assert bunny["family"] == "bunny"
assert bunny["animations"]["idle"][0] == "/static/game/characters/animals/bunny/01/idle0.png"
assert len(bunny["animations"]["idle"]) == 20
def test_sprite_library_is_public_plus_admin_merged() -> None:
merged = set(game_engine.PUBLIC_SPRITE_LIBRARY) | set(game_engine.ADMIN_SPRITE_LIBRARY)
assert set(game_engine.SPRITE_LIBRARY) == merged
assert set(game_engine.ADMIN_SPRITE_LIBRARY) == game_engine.ADMIN_ONLY_CHARACTER_SLUGS
assert "male-adventurer" not in game_engine.ADMIN_ONLY_CHARACTER_SLUGS
def test_sprite_gallery_families_shows_one_tile_per_animal() -> None:
families = game_engine.sprite_gallery_families(game_engine.SPRITE_LIBRARY)
slugs = [slug for slug, _ in families]
assert len(slugs) == len(game_engine.PUBLIC_SPRITE_LIBRARY) + 14 # 6 Kenney + 14 familles animales
assert "animal-bunny-01" in slugs
assert "animal-bunny-02" not in slugs
def _create_jeu2d_game_as(post_client: FlaskClient, name: str, slug_owner_client: FlaskClient | None = None) -> Any:
# Fusion des moteurs (voir game_engine/screens_repo/ensure_schema.py) : un
# jeu n'a plus de type propre — seul l'écran créé ensuite par chaque
# test (game_engine.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: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
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 = game_engine.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: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
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 = game_engine.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: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_animal_scene_admin"))
screen_id = game_engine.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 = not_none(game_engine.get_scene_object(slug, obj_id))
assert game_engine.resolve_personnage_data(obj)["forge_character"] == "animal-bunny-01"
def test_scene_object_personnage_swap_rejects_animal_character_for_a_user_account(
client: FlaskClient, user_client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
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 = game_engine.create_screen(slug, "Scène 1")
obj_id = game_engine.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_list_used_forge_characters_scans_elements_and_scene_objects(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str]
) -> None:
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_used_characters"))
screen_id = game_engine.create_screen(slug, "Scène 1")
game_engine.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
game_engine.add_scene_object(slug, screen_id, kind="decor")
used = game_engine.list_used_forge_characters(slug)
assert used == {"animal-bunny-01"}
def test_build_scorm_package_only_copies_used_animal_character_folders(
client: FlaskClient, tmp_game_slug_cleanup: Callable[[str], str], tmp_path: Path
) -> None:
slug = tmp_game_slug_cleanup(_create_jeu2d_game_as(client, "pytest_scorm_animals"))
screen_id = game_engine.create_screen(slug, "Scène 1")
game_engine.add_scene_object(slug, screen_id, forge_character="animal-bunny-01")
with client.application.test_request_context():
from publish import build_scorm_package
zip_path, _name = build_scorm_package.build_scorm_zip(slug)
try:
import zipfile
with zipfile.ZipFile(zip_path) as zf:
names = zf.namelist()
animal_paths = {n for n in names if "static/game/characters/animals/" in n and not n.endswith("manifest.json")}
animals_used = {n.split("static/game/characters/animals/")[1].split("/")[0] for n in animal_paths}
assert animals_used == {"bunny"}
assert any("static/game/characters/animals/bunny/01/" in n for n in animal_paths)
assert not any("static/game/characters/animals/bear/" in n for n in animal_paths)
assert any(n.endswith("static/game/characters/animals/manifest.json") for n in names)
assert any("static/game/characters/male-adventurer/" in n for n in names)
finally:
import os
os.remove(zip_path)