Code agent dynamique¶
Le code agent est un identifiant court de quatre caractères affiché
à l'étudiant sur la page de confirmation de commande (composant
OrderConfirmation.jsx). Il sert de graine individuelle à la réponse
de la dernière étape de chaque parcours — l'étape dite DYNAMIC. Cette
page décrit la génération du code, les deux formats utilisés selon le
niveau, les cinq formules de calcul de la réponse, et le flux de
validation associé.
Pourquoi un code agent¶
Sans code agent, les 100 énigmes de PerfShop partageraient toutes un hash statique, et le premier étudiant ayant terminé un niveau pourrait chuchoter la dernière réponse à ses voisins. Le code agent rend la dernière étape unique pour chaque étudiant : la réponse dépend d'un calcul appliqué au code, et chaque étudiant a le sien.
La contrainte technique est stricte : la bonne réponse ne doit
jamais transiter sur le réseau. Elle est calculée une seule fois
côté serveur au moment du POST /pedagogique/join, hashée en SHA-256,
puis stockée dans la session. L'étudiant fait le calcul mentalement
(ou sur papier) en lisant son code agent dans la boutique, tape la
réponse dans l'overlay, le frontend la hashe, et le backend compare les
deux hashes.
Génération du code¶
La génération a lieu dans DefaultPedagogiqueSessionService.createSession(),
immédiatement après la création du token UUID de la session. Le code
dépend du token et du niveau :
String agentCode;
if ("bac1".equals(level) || "bac2".equals(level)) {
// BAC+1 / BAC+2 : code 100% numérique
String digitsOnly = session.token.replaceAll("[^0-9]", "");
agentCode = digitsOnly.substring(0, Math.min(4, digitsOnly.length()));
} else {
// BAC+3 à BAC+5 : code hexadécimal
agentCode = session.token.replace("-", "").substring(0, 4).toUpperCase();
}
session.agentCode = agentCode;
Le token de session est un UUID v4 (format
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx en minuscules hexadécimales) ; il
a 36 caractères, 32 sans les tirets. Le code agent est dérivé par l'un
des deux procédés suivants :
| Niveau | Procédé | Exemple (token a8f3e2b1-…) |
|---|---|---|
bac1, bac2 |
Ne garder que les chiffres décimaux du token, prendre les 4 premiers | token a8f3e2b1-4c7d-4a6e-9b0f-3e5a7d1c8b42 → chiffres 83214746903571842 → code 8321 |
bac3, bac4, bac5 |
Retirer les tirets, prendre les 4 premiers caractères, mettre en majuscules | token a8f3e2b1-… → A8F3 |
Pourquoi deux formats¶
Le choix du format numérique pour bac1/bac2 est pédagogique
et non technique : les étudiants de ces niveaux (lycéens post-bac, BTS
débutants) n'ont pas forcément rencontré l'hexadécimal dans leur
cursus. Leur imposer un calcul sur des chars A–F pour la toute
dernière étape d'un parcours de découverte serait un mur injustifié.
À l'inverse, l'hexadécimal pour bac3 et au-dessus est lui-même
un objectif pédagogique : savoir lire A3F7, le convertir en décimal,
calculer une somme de digits en base 16, appliquer un modulo 256 ou un
XOR bit à bit fait partie des compétences attendues à ces niveaux. Le
format du code agent prépare l'étudiant à la formule de calcul
correspondante.
Unicité¶
La probabilité que deux étudiants d'une même session obtiennent le même code agent est non nulle mais faible :
- Format numérique (bac1/bac2) : les 4 premiers chiffres d'un UUID v4 peuvent avoir entre 0 et 4 chiffres décimaux sur leurs 8 premiers hex chars, ce qui rend le codage peu uniforme, mais la collision reste anecdotique pour des classes de moins de 100 étudiants.
- Format hexadécimal (bac3+) : $16^4 = 65\,536$ codes possibles — la probabilité de collision dans une classe de 30 étudiants est de l'ordre de 0,7 %. En pratique, deux collisions n'empêchent rien : les deux étudiants ont la même réponse finale et aucun autre effet de bord.
Le code agent n'est pas conçu comme un secret cryptographique — c'est un générateur de variabilité pédagogique.
Les cinq formules DYNAMIC¶
La méthode computeDynamicAnswer(String agentCode, String level) dans
DefaultPedagogiqueSessionService dispatch selon le niveau. Chaque
formule a été choisie pour que la difficulté du calcul mental
corresponde au niveau cible.
| Niveau | Code | Formule de la réponse | Exemple (non-spoiler) |
|---|---|---|---|
bac1 |
4 chiffres décimaux | $R = (\sum_{i=1}^{4} c_i) \times 10$ | code 8321 → $(8+3+2+1) \times 10 = 140$ |
bac2 |
4 chiffres décimaux | $R = \lfloor \text{code} \div \sum c_i \rfloor$ | code 8321 → $8321 \div (8+3+2+1) = 8321 \div 14 = 594$ |
bac3 |
4 chars hex | $R = \sum_{i=1}^{4} h_i$ (valeur décimale de chaque digit hex) | code A3F7 → $10 + 3 + 15 + 7 = 35$ |
bac4 |
4 chars hex | $R = \text{code}_{16} \bmod 256$ | code A3F7 → $0xA3F7 = 41975$ ; $41975 \bmod 256 = 247$ |
bac5 |
4 chars hex | $R = \text{octet}\text{haut} \oplus \text{octet}\text{bas}$ | code A3F7 → $0xA3 \oplus 0xF7 = 10100011 \oplus 11110111 = 01010100 = 84$ |
Ces exemples ne sont pas des réponses
Le code agent affiché à un étudiant dépend de son token de session
— il est différent à chaque join. Les exemples ci-dessus illustrent
la mécanique avec un code choisi arbitrairement. Aucun étudiant
n'obtiendra 8321 ou A3F7 sauf coïncidence.
Implémentation Java (référence)¶
static String computeDynamicAnswer(String agentCode, String level) {
return switch (level) {
case "bac1" -> {
int sum = 0;
for (char c : agentCode.toCharArray()) sum += (c - '0');
yield String.valueOf(sum * 10);
}
case "bac2" -> {
int code = Integer.parseInt(agentCode);
int sum = 0;
for (char c : agentCode.toCharArray()) sum += (c - '0');
yield String.valueOf(sum > 0 ? code / sum : code);
}
case "bac3" -> {
int sum = 0;
for (char c : agentCode.toCharArray()) {
sum += (c >= '0' && c <= '9') ? (c - '0') : (c - 'A' + 10);
}
yield String.valueOf(sum);
}
case "bac4" -> {
int decimalCode = Integer.parseInt(agentCode, 16);
yield String.valueOf(decimalCode % 256);
}
case "bac5" -> {
int decimalCode = Integer.parseInt(agentCode, 16);
int high = (decimalCode >> 8) & 0xFF;
int low = decimalCode & 0xFF;
yield String.valueOf(high ^ low);
}
default -> "0";
};
}
La méthode est static et package-private — elle n'est appelée que
par createSession() dans le même paquet com.perfshop.service.
Du /join à la validation finale¶
sequenceDiagram
autonumber
actor E as Étudiant
participant CTR as ChaosStudentController
participant SVC as PedagogiqueSessionService
participant DB as MySQL
participant OC as OrderConfirmation.jsx
participant OVR as PedagogiqueOverlay
E->>CTR: POST /pedagogique/join
CTR->>SVC: createSession(alias, "bac3", 3600)
SVC->>SVC: token = UUID.randomUUID()
SVC->>SVC: agentCode = "A3F7" (hex, bac3)
SVC->>SVC: dynamicAnswer = "35" (somme digits hex)
SVC->>SVC: extractionAnswerHash = sha256("35")
SVC->>DB: INSERT agent_code, extraction_answer_hash
CTR-->>E: {token, alias, totalSteps}
Note over E: Parcours des 19 premières étapes...
E->>OVR: Étape 19 (add_to_cart, checkout)
OVR->>CTR: POST /validate step=19
CTR-->>OVR: {valid:true, nextEnigme: step 20 DYNAMIC}
E->>OC: Confirmation de commande
Note over OC: L'étudiant lit son<br/>code agent : "A3F7"
E->>E: Calcule mentalement<br/>A+3+F+7 = 10+3+15+7 = 35
E->>OVR: Tape "35", clique Valider
OVR->>OVR: sha256hex("35") = "efaa08…"
OVR->>CTR: POST /validate step=20<br/>{answerHash:"efaa08…"}
CTR->>CTR: enigme.answerHash() == "DYNAMIC"
CTR->>CTR: expectedHash = session.extractionAnswerHash
CTR->>CTR: compare → match
CTR->>SVC: session.step = 20 ; completedAt = now()
CTR-->>OVR: {valid:true, completed:true, stars, maxStars}
Note over E: Navigation auto vers /s/{token}
Ce flux illustre la propriété clé du code agent : la bonne réponse
en clair n'existe que dans la tête de l'étudiant et dans le
stockage éphémère du service au moment du /join. Une fois hashée,
elle ne peut plus être retrouvée sans bruteforce.
Persistance du code agent¶
Le champ agentCode de MutableSession est volatile et son
équivalent en base est la colonne agent_code VARCHAR(10) de la table
pedagogique_sessions. Un redémarrage du backend n'invalide pas le
code — il est rechargé à l'identique dans toMutableSession(), et le
extractionAnswerHash stocké reste aligné puisqu'il dépend uniquement
du code + niveau.
Le service expose aussi une méthode getAgentCodeForToken(String token)
utilisée par OrderController pour afficher le code sur la page de
confirmation de commande sans exposer l'intégralité de la session à
l'UI e-commerce.
Variations non implémentées¶
Les formules ci-dessus sont les seules implémentées à date. La
méthode computeDynamicAnswer() a un default -> "0" qui servirait de
fallback défensif si un niveau inconnu était demandé — en pratique, le
contrôleur rejette déjà tout niveau autre que bac1–bac5 en amont
avec une réponse HTTP 400. Ajouter un niveau bac6 ou une variation
régionale nécessiterait deux modifications :
- Ajouter un
case "bac6" -> …danscomputeDynamicAnswer() - Ajouter le choix de format de code agent dans
createSession() - Créer la classe compagne
PedagogiqueEnigmeBac6avec son catalogue
La méthode defaultTimerSeconds() devrait aussi être étendue avec la
nouvelle durée.
Pages suivantes : ← Système d'énigmes · Système d'étoiles → · Indices →