Skip to content

Star system

At the end of a journey, the student receives a number of stars that rewards both the difficulty of the level reached and respecting the allotted time. This score is displayed on the success page (/s/:token) and used by the instructor panel to order results in the session summary.

The computeStars method

The computation is centralized in 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);
}

Two simple principles combine:

  1. Base = level number. bac1 yields at most 1 star, bac5 at most 5. The maximum star for a given level is a constant, not a dynamic difficulty variable.
  2. Penalty of one star if time exceeded. If the session is not completed within the allotted time (durationSeconds > pedagogiqueTimer), one star is subtracted, with a floor of 1 star — it is impossible to fall back to 0, even on a journey finished several hours past the deadline.

Possible scores table

Level Allotted time Stars if in time Stars if out of time
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 ⭐⭐⭐⭐

Why bac1 never loses a star

The floor of 1 star guarantees that the bac1 level — designed as an introductory journey — does not "punish" a student who takes the time to explore the store without stress. An overrun on bac1 makes the duration visible to the instructor but preserves the symbolic reward.

Computing inTime

The inTime boolean is computed when the journey is completed, in validatePedagogique():

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

Important point: the comparison uses the absolute time elapsed since the instructor started the journey (pedagogiqueStartTime), not the time since the individual student joined the session (session.joinedAt). A student who joins the session 10 minutes after the /activate therefore has 20 minutes for a bac1, not 30, because their countdown shares the same time base as the instructor's.

This choice is pedagogical: the instructor can start a journey and let students join progressively, but the "all together" approach forces latecomers to speed up — which matches the reality of a classroom.

Fallback for completedAt = 0

In the buildPedagogiqueState() block that serves the /status polls, a safeguard handles the case where completedAt = 0 while the final step is marked as done:

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

If completedAt is at zero (abnormal situation — a session that went through a badly cleaned restart, for example), the computation falls back to the instructor's global time. A warning is logged to alert the operations team.

Score propagation

sequenceDiagram
    autonumber
    actor E as Student
    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: Final DYNAMIC step validated
    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 renders ⭐⭐⭐⭐☆

    Note over ADM: Instructor views sessions
    ADM->>CTR: GET /pedagogique/sessions
    CTR->>CTR: For each completed session:<br/>computeStars(level, dur<=timer)
    CTR-->>ADM: [{alias, stars, maxStars, ...}]

The score is computed in three places that must remain consistent:

  1. validatePedagogique() — response to the validation of the final step, directly to the student overlay.
  2. buildPedagogiqueState() — included in each /status poll for students whose session is already completed (persistence of the display after reload).
  3. getSessions() and buildSessionSnapshot() — for the real-time instructor view and for archiving in lastSessionSummary at deactivation time.

The three calls use the same static method computeStars(level, inTime) — guaranteeing an identical result regardless of the read path.

Archiving in the instructor snapshot

When the instructor clicks Deactivate (or starts a new journey), buildSessionSnapshot() builds an immutable snapshot of all sessions for the lastSessionSummary view:

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));
}

The snapshot stores both the stars obtained and the maximum stars of the level — which allows the UI to correctly display 4 / 5 ⭐ even after the journey has been stopped and cleared from the database by clearAll().

Pedagogical role of the penalty

The one-star penalty for overrun is a soft signal, not a sanction. A student who finishes a bac5 in 2 hours instead of 1h30 leaves with 4 stars instead of 5 — they know they finished but that the pace was not optimal, which feeds the debrief discussion.

This choice is deliberately not configurable from the instructor UI: customizing the scale during a session would break comparability between cohorts. Changing the behavior requires editing the source code and a new build.

maxStars field in API responses

All progress responses include both stars (stars obtained) and maxStars (stars achievable at this level). This redundancy simplifies frontend rendering — no component needs to know the scale by heart:

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

The React SuccesStars component reads these two values and renders three filled stars (⭐) followed by zero empty stars (☆) — or, for a bac5 out of time, four filled followed by one empty (4/5).


Next pages: ← Agent code · Hints → · Success page →