Quiz jouable : budget de récompense, boîte à quiz, widget score
- Récompense d'une quête = budget MAXIMUM pour ses questions : la somme des points des "❓ Question" ne peut plus dépasser recompense_score (routes/quests/quest_dialogues.py à l'enregistrement des dialogues, quest_update.py si on abaisse la récompense sous ce qui est déjà réparti) — message d'erreur explicite (400) dans les deux sens, affiché via alert() côté éditeur (quest-editor.js), jamais enregistré silencieusement dans un état incohérent. - Nombre de choix par question plafonné à 4 (au lieu de 8). - Deux nouveaux widgets "🖥️ Interface" (mêmes fondations que "💬 Boîte de dialogue" — screens/rendering/dialogue_box_style.py, même panneau "🎨 Style") : - "❓ Boîte à quiz" : affiche la question et ses choix, sans pied (une question se résout au clic sur un choix, pas de "Suivant"). - "🏆 Score" : affiche en continu les points gagnés, TOUJOURS visible une fois posé (contrairement aux boîtes de dialogue/quiz, masquées par défaut). - Moteur d'exécution (static/js/play/dialogue-box-controller.js, refonte) : une conversation de quête alterne maintenant répliques (boîte de dialogue) et questions (boîte à quiz) selon le type de chaque ligne. Bonne réponse -> crédite le score (compteur runtime dédié, jamais lié au score du moteur document) et avance ; mauvaise réponse -> rien, la question reste affichée pour réessayer ; aucune boîte à quiz posée -> la question est ignorée plutôt que de bloquer la conversation. Le dialogue "en_cours" épuisé (donc, s'il contenait des questions, toutes répondues) fait passer la quête "terminee" (en mémoire seulement, comme le reste de ce système — voir accepter/ refuser une offre de quête) : le joueur peut alors quitter la scène normalement, ces widgets n'étant jamais des obstacles. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e972acf9e6
commit
b5546df044
@@ -5,6 +5,8 @@ et un dialogue par statut (3 colonnes — voir static/js/quests/
|
||||
quest-editor.js et sanitize_quest_dialogues). Une quête vit pour TOUT le
|
||||
jeu, jamais par écran (mêmes conventions que les événements
|
||||
personnalisés, db/custom_events/)."""
|
||||
import json
|
||||
|
||||
import db
|
||||
|
||||
|
||||
@@ -192,3 +194,71 @@ def test_quest_dialogues_route_sanitizes_and_persists(client, tmp_game_slug_clea
|
||||
def test_quest_dialogues_route_404s_for_missing_quest(client, game):
|
||||
resp = client.post(f"/game/{game}/quests/999999/dialogues", data='{"dialogues": {}}', content_type="application/json")
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def _question_line(reward_amount, correct_index=0):
|
||||
return {
|
||||
"type": "question", "text": "2+2 ?", "choices": ["3", "4"],
|
||||
"correct_index": correct_index, "reward_type": "score", "reward_amount": reward_amount,
|
||||
}
|
||||
|
||||
|
||||
def test_quest_dialogues_route_rejects_questions_totaling_more_than_the_reward(client, tmp_game_slug_cleanup):
|
||||
slug = _make_game(tmp_game_slug_cleanup)
|
||||
quest = db.create_quest(slug, "Trouver la clé")
|
||||
db.update_quest(slug, quest["id"], {"recompense_score": 5})
|
||||
resp = client.post(
|
||||
f"/game/{slug}/quests/{quest['id']}/dialogues",
|
||||
data=json.dumps({"dialogues": {"nouvelle": [_question_line(3), _question_line(3)], "en_cours": [], "terminee": []}}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "pts" in resp.get_json()["error"]
|
||||
# Rien n'a été enregistré — la répartition invalide n'est jamais persistée.
|
||||
assert db.get_quest(slug, quest["id"])["dialogues"] == {}
|
||||
|
||||
|
||||
def test_quest_dialogues_route_accepts_questions_totaling_exactly_the_reward(client, tmp_game_slug_cleanup):
|
||||
slug = _make_game(tmp_game_slug_cleanup)
|
||||
quest = db.create_quest(slug, "Trouver la clé")
|
||||
db.update_quest(slug, quest["id"], {"recompense_score": 5})
|
||||
resp = client.post(
|
||||
f"/game/{slug}/quests/{quest['id']}/dialogues",
|
||||
data=json.dumps({"dialogues": {"nouvelle": [_question_line(2), _question_line(3)], "en_cours": [], "terminee": []}}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_quest_update_route_rejects_lowering_reward_below_already_allocated_questions(client, tmp_game_slug_cleanup):
|
||||
slug = _make_game(tmp_game_slug_cleanup)
|
||||
quest = db.create_quest(slug, "Trouver la clé")
|
||||
db.update_quest(slug, quest["id"], {"recompense_score": 10})
|
||||
db.set_quest_dialogues(slug, quest["id"], {"nouvelle": [_question_line(6)], "en_cours": [], "terminee": []})
|
||||
resp = client.post(
|
||||
f"/game/{slug}/quests/{quest['id']}",
|
||||
data=json.dumps({"recompense_score": 3}),
|
||||
content_type="application/json",
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert "pts" in resp.get_json()["error"]
|
||||
assert db.get_quest(slug, quest["id"])["recompense_score"] == 10 # inchangé
|
||||
|
||||
|
||||
def test_sanitize_quest_dialogues_caps_choices_at_four():
|
||||
raw = {
|
||||
"nouvelle": [{"type": "question", "text": "Q", "choices": ["a", "b", "c", "d", "e"], "correct_index": 4, "reward_type": "score", "reward_amount": 1}],
|
||||
"en_cours": [], "terminee": [],
|
||||
}
|
||||
cleaned = db.sanitize_quest_dialogues(raw)
|
||||
# Coupé à 4 choix -> l'index 4 (qui pointait sur le 5e, retiré) sort des bornes -> ligne rejetée.
|
||||
assert cleaned["nouvelle"] == []
|
||||
|
||||
|
||||
def test_quest_dialogues_total_reward_sums_questions_across_all_statuses():
|
||||
dialogues = {
|
||||
"nouvelle": [_question_line(2)],
|
||||
"en_cours": [_question_line(3), {"type": "dialogue", "speaker": "pnj", "text": "x"}],
|
||||
"terminee": [],
|
||||
}
|
||||
assert db.quest_dialogues_total_reward(dialogues) == 5
|
||||
|
||||
Reference in New Issue
Block a user