Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 63 additions & 2 deletions contrat/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@
"tags": [
"jeu"
],
"summary": "Répond à une question de ricochet (ADR 0044). Le serveur juge sur les\nidentifiants, applique la règle de maîtrise et compte la réponse parmi les\ncinq du jour. Une carte pas à revoir renvoie son résultat sans changer de niveau.\nRien n'est consommé : ni exemplaire, ni énergie, ni encre.",
"summary": "Répond à une question de ricochet (ADR 0044). Le serveur juge sur les\nidentifiants, applique la règle de maîtrise et compte la réponse parmi les\ncinq du jour. Une carte pas à revoir renvoie son résultat sans changer de niveau.\nRien n'est consommé : ni exemplaire, ni énergie, ni encre. Une coupe juste de\n*découper* qui laisse des coupes à poser (`suite = continue`, ADR 0084) ne\nmodifie rien : seule la réponse qui clôt la question est une commande.",
"operationId": "repondre_ricochet",
"requestBody": {
"content": {
Expand Down Expand Up @@ -2764,6 +2764,17 @@
},
"description": "Positions de coupe de la composition préférée ; vide : le mot ne se découpe pas."
},
"decoupage": {
"oneOf": [
{
"$ref": "#/components/schemas/Noeud",
"description": "Arbre des coupes justes, compositions chaînées comprises (ADR 0084). Absent d'un\nmanifeste antérieur : `coupes` seules font alors foi, sur un seul niveau."
},
{
"type": "null"
}
]
},
"id": {
"$ref": "#/components/schemas/IdCatalogue"
}
Expand Down Expand Up @@ -2801,6 +2812,44 @@
"mot"
]
},
"Noeud": {
"type": "object",
"description": "Un morceau du mot et ses coupes justes.",
"required": [
"debut",
"fin",
"coupes"
],
"properties": {
"coupes": {
"type": "array",
"items": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"description": "Positions des coupes de ce morceau, dans le mot entier."
},
"debut": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"fin": {
"type": "integer",
"format": "int32",
"description": "Fin exclue.",
"minimum": 0
},
"parties": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Noeud"
},
"description": "Sous-nœuds : les parties qui se coupent à leur tour."
}
}
},
"OuverturePli": {
"type": "object",
"required": [
Expand Down Expand Up @@ -3267,7 +3316,11 @@
},
"juste": {
"type": "boolean",
"description": "Vrai si la réponse est la bonne, que la carte ait été à revoir ou non."
"description": "Vrai si la réponse est la bonne, que la carte ait été à revoir ou non.\nPour une coupe de *découper* qui laisse la question ouverte : vrai."
},
"suite": {
"$ref": "#/components/schemas/SuiteRicochet",
"description": "`continue` : *découper* a encore des coupes à poser (ADR 0084), rien n'est\nécrit ni compté. Absent des anciennes réponses : `terminee`."
}
},
"additionalProperties": false
Expand Down Expand Up @@ -3661,6 +3714,14 @@
"degrade"
]
},
"SuiteRicochet": {
"type": "string",
"description": "Où en est la question après la réponse.",
"enum": [
"terminee",
"continue"
]
},
"Transformation": {
"type": "object",
"required": [
Expand Down
72 changes: 51 additions & 21 deletions crates/api/src/ricochets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use etymo_contrat::{
FormatRicochet, QuestionRicochet, ReponseJoueur, RicochetEtat, RicochetsDuJour,
};
use etymo_domaine::IdCatalogue;
use etymo_domaine::decoupage::{self, Verdict};
use etymo_domaine::ricochet::{self, Carte};
use sqlx::PgConnection;
use uuid::Uuid;
Expand Down Expand Up @@ -176,7 +177,7 @@ fn question<'a>(
{
formats.push(FormatRicochet::MotSens);
}
if !m.coupes.is_empty() {
if m.arbre().is_some() {
formats.push(FormatRicochet::Decouper);
}
}
Expand Down Expand Up @@ -369,26 +370,51 @@ pub fn juger(
.ok_or_else(|| invalide("reponse : un mot connu attendu"))?;
Ok(choisi == unite || autre.classe_litteral.as_ref() == Some(classe))
}
(FormatRicochet::Decouper, ReponseJoueur::Coupes(coupes)) => {
let mot = (unite.prefixe() == "unt")
.then(|| ricochets.mots.get(unite))
.flatten()
.ok_or_else(inconnue)?;
if mot.coupes.is_empty() {
return Err(invalide("format : ce mot ne se découpe pas"));
}
if coupes.len() > COUPES_MAX {
return Err(invalide("reponse : trop de coupes"));
}
let mut coupes = coupes.clone();
coupes.sort_unstable();
coupes.dedup();
Ok(coupes == mot.coupes)
}
_ => Err(invalide("reponse : forme incompatible avec le format")),
}
}

/// Ce que vaut une réponse. Seul *découper* a un entre-deux : les coupes posées
/// sont justes, mais il en reste (ADR 0084).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Jugement {
Faux,
Juste,
/// Question pas terminée : rien ne s'écrit.
Continue,
}

/// Juge une réponse, découper en chaîne compris (`etymo_domaine::decoupage`).
pub fn jugement(
jeu: &Jeu,
format: FormatRicochet,
unite: &IdCatalogue,
reponse: &ReponseJoueur,
) -> Result<Jugement, ErreurApi> {
let (FormatRicochet::Decouper, ReponseJoueur::Coupes(coupes)) = (format, reponse) else {
return Ok(if juger(jeu, format, unite, reponse)? {
Jugement::Juste
} else {
Jugement::Faux
});
};
let mot = (unite.prefixe() == "unt")
.then(|| jeu.ricochets.mots.get(unite))
.flatten()
.ok_or_else(|| ErreurApi::Interdit("carte inconnue"))?;
let arbre = mot
.arbre()
.ok_or_else(|| invalide("format : ce mot ne se découpe pas"))?;
if coupes.len() > COUPES_MAX {
return Err(invalide("reponse : trop de coupes"));
}
Ok(match decoupage::juger(&arbre, coupes) {
Verdict::Faux => Jugement::Faux,
Verdict::Continue => Jugement::Continue,
Verdict::Termine => Jugement::Juste,
})
}

// -------------------------------------------------------------------- base

fn id(texte: &str) -> Result<IdCatalogue, ErreurApi> {
Expand Down Expand Up @@ -613,6 +639,7 @@ mod tests {
id: mot(n),
classe_litteral: Some(mot(classe)),
coupes: if n <= 2 { vec![3] } else { vec![] },
decoupage: None,
},
);
}
Expand Down Expand Up @@ -783,8 +810,11 @@ mod tests {
assert!(!juger(&jeu, FormatRicochet::MotSens, &mot(1), &choix(mot(2))).unwrap());
// Découper : l'ordre et les doublons ne comptent pas.
let coupes = |c: &[u32]| ReponseJoueur::Coupes(c.to_vec());
assert!(juger(&jeu, FormatRicochet::Decouper, &mot(1), &coupes(&[3, 3])).unwrap());
assert!(!juger(&jeu, FormatRicochet::Decouper, &mot(1), &coupes(&[2])).unwrap());
let decouper =
|c: &[u32]| jugement(&jeu, FormatRicochet::Decouper, &mot(1), &coupes(c)).unwrap();
assert_eq!(decouper(&[3, 3]), Jugement::Juste);
assert_eq!(decouper(&[2]), Jugement::Faux);
assert_eq!(decouper(&[]), Jugement::Continue);
}

#[test]
Expand Down Expand Up @@ -816,7 +846,7 @@ mod tests {
Err(ErreurApi::RequeteInvalide(_))
));
assert!(matches!(
juger(
jugement(
&jeu,
FormatRicochet::Decouper,
&mot(3),
Expand All @@ -825,7 +855,7 @@ mod tests {
Err(ErreurApi::RequeteInvalide(_))
));
assert!(matches!(
juger(
jugement(
&jeu,
FormatRicochet::Decouper,
&mot(1),
Expand Down
Loading
Loading