Aller au contenu

Système d'étoiles

À la fin d'un parcours, l'étudiant reçoit un nombre d'étoiles qui récompense à la fois la difficulté du niveau atteint et le respect du temps imparti. Ce score est affiché sur la page de succès (/s/:token) et utilisé par le panneau formateur pour ordonner les résultats dans le récapitulatif de session.

La méthode computeStars

Le calcul est centralisé dans ChaosStudentController.computeStars() :

static int computeStars(String level, boolean inTime) {
    int base = switch (level != null ? level : "") {
        case "bac1" -> 1;
        case "bac2" -> 2;
        case "bac3" -> 3;
        case "bac4" -> 4;
        case "bac5" -> 5;
        default     -> 1;
    };
    return inTime ? base : Math.max(1, base - 1);
}

Deux principes simples se combinent :

  1. Base = numéro du niveau. bac1 rapporte au plus 1 étoile, bac5 au plus 5. L'étoile maximale pour un niveau donné est une constante, pas une variable de difficulté dynamique.
  2. Malus d'une étoile en cas de dépassement. Si la session n'est pas complétée dans le temps imparti (durationSeconds > pedagogiqueTimer), on soustrait 1 étoile, avec un plancher à 1 étoile — il est impossible de retomber à 0, même sur un parcours terminé plusieurs heures après le délai.

Tableau des scores possibles

Niveau Durée imparti Étoiles si dans les temps Étoiles si hors temps
bac1 30 min 1 ⭐ 1 ⭐
bac2 45 min 2 ⭐⭐ 1 ⭐
bac3 60 min 3 ⭐⭐⭐ 2 ⭐⭐
bac4 75 min 4 ⭐⭐⭐⭐ 3 ⭐⭐⭐
bac5 90 min 5 ⭐⭐⭐⭐⭐ 4 ⭐⭐⭐⭐

Pourquoi bac1 ne perd jamais d'étoile

Le plancher à 1 étoile garantit que le niveau bac1 — pensé comme un parcours d'initiation — ne « punit » pas un étudiant qui prend le temps d'explorer la boutique sans stress. Un dépassement sur bac1 rend la durée visible au formateur mais conserve la récompense symbolique.

Calcul du inTime

Le booléen inTime est calculé au moment où le parcours est complété, dans validatePedagogique() :

boolean inTime =
    (Instant.now().toEpochMilli() - pedagogiqueStartTime) / 1000L
    <= pedagogiqueTimer;

Point important : la comparaison utilise le temps absolu écoulé depuis le démarrage du parcours par le formateur (pedagogiqueStartTime), pas le temps depuis que l'étudiant individuel a rejoint la session (session.joinedAt). Un étudiant qui rejoint la session 10 minutes après le /activate dispose donc de 20 minutes pour un bac1 et non de 30, car son décompte partage la même base temporelle que celui du formateur.

Ce choix est pédagogique : le formateur peut démarrer un parcours et laisser les étudiants le rejoindre progressivement, mais le « tous en même temps » force les retardataires à accélérer — ce qui correspond à la réalité d'une classe.

Fallback pour completedAt = 0

Dans le bloc buildPedagogiqueState() qui sert les polls /status, un garde-fou gère le cas où completedAt = 0 alors que l'étape finale est marquée comme terminée :

int stars;
if (session.completedAt > 0) {
    long dur = (session.completedAt - session.joinedAt) / 1000L;
    stars = computeStars(pedagogiqueLevel, dur <= pedagogiqueTimer);
} else {
    log.warn("[Pédagogique] completedAt=0 pour {} — fallback temps global",
        session.displayAlias());
    stars = computeStars(pedagogiqueLevel, elapsed <= pedagogiqueTimer);
}

Si completedAt est à zéro (situation anormale — une session qui a traversé un redémarrage mal nettoyé, par exemple), le calcul repasse sur le temps global du formateur. Un warning est tracé pour alerter l'équipe d'exploitation.

Propagation du score

sequenceDiagram
    autonumber
    actor E as Étudiant
    participant OVR as PedagogiqueOverlay
    participant CTR as ChaosStudentController
    participant DB as MySQL
    participant SUC as PedagogiqueSucces
    participant ADM as chaos-admin

    Note over E,OVR: Dernière étape DYNAMIC validée
    OVR->>CTR: POST /validate step=N
    CTR->>CTR: session.completedAt = now()
    CTR->>DB: UPDATE pedagogique_sessions<br/>SET completed_at
    CTR->>CTR: stars = computeStars(level,<br/>dur <= timer)
    CTR-->>OVR: {valid, completed:true,<br/>stars:4, maxStars:5}

    Note over E: Navigation /s/{token}
    SUC->>CTR: GET /pedagogique/succes/{token}
    CTR->>DB: SELECT session
    CTR->>CTR: stars = computeStars(level, inTime)
    CTR-->>SUC: {alias, stars:4, maxStars:5,<br/>durationSeconds:3842}
    SUC->>SUC: SuccesStars render ⭐⭐⭐⭐☆

    Note over ADM: Formateur consulte sessions
    ADM->>CTR: GET /pedagogique/sessions
    CTR->>CTR: Pour chaque session complétée :<br/>computeStars(level, dur<=timer)
    CTR-->>ADM: [{alias, stars, maxStars, ...}]

Le score est calculé à trois endroits qui doivent rester cohérents :

  1. validatePedagogique() — réponse à la validation de l'étape finale, directement vers l'overlay étudiant.
  2. buildPedagogiqueState() — inclus dans chaque poll /status pour les étudiants dont la session est déjà complétée (persistance de l'affichage après reload).
  3. getSessions() et buildSessionSnapshot() — pour la vue formateur en temps réel et pour l'archivage dans lastSessionSummary au moment de la désactivation.

Les trois appels utilisent la même méthode statique computeStars(level, inTime) — garantissant un résultat identique quel que soit le chemin de lecture.

Archivage dans le snapshot formateur

Quand le formateur clique sur Désactiver (ou démarre un nouveau parcours), buildSessionSnapshot() construit un instantané immuable de toutes les sessions pour la vue lastSessionSummary :

if (completed && s.completedAt > 0) {
    long dur = (s.completedAt - s.joinedAt) / 1000L;
    m.put("durationSeconds", dur);
    m.put("stars",    computeStars(pedagogiqueLevel, dur <= pedagogiqueTimer));
    m.put("maxStars", computeStars(pedagogiqueLevel, true));
}

Le snapshot stocke à la fois les étoiles obtenues et les étoiles maximales du niveau — ce qui permet à l'UI d'afficher correctement 4 / 5 ⭐ même après que le parcours a été arrêté et vidé de la base par clearAll().

Rôle pédagogique du malus

Le malus d'une étoile pour dépassement est un signal doux, pas une sanction. Un étudiant qui termine un bac5 en 2 heures au lieu de 1h30 repart avec 4 étoiles au lieu de 5 — il sait qu'il a terminé mais que le rythme n'était pas optimal, ce qui alimente la discussion en débrief de séance.

Ce choix est volontairement non configurable depuis l'UI formateur : personnaliser le barème en cours de session romprait la comparabilité entre cohortes. Modifier le comportement requiert une édition du code source et un nouveau build.

Champ maxStars dans les réponses API

Toutes les réponses de progression incluent à la fois stars (étoiles obtenues) et maxStars (étoiles atteignables sur ce niveau). Cette redondance facilite le rendu côté frontend — aucun composant n'a besoin de connaître le barème en dur :

{
  "valid": true,
  "completed": true,
  "inTime": true,
  "stars": 3,
  "maxStars": 3
}

Le composant React SuccesStars lit ces deux valeurs et rend trois étoiles pleines (⭐) suivies de zéro étoiles vides (☆) — ou, pour un bac5 hors temps, quatre pleines suivies d'une vide (4/5).


Pages suivantes : ← Code agent · Indices → · Page de succès →