Skip to content

Dynamic agent code

The agent code is a short four-character identifier displayed to the student on the order confirmation page (OrderConfirmation.jsx component). It serves as an individual seed for the answer to the last step of each journey — the so-called DYNAMIC step. This page describes code generation, the two formats used depending on the level, the five answer-computation formulas, and the associated validation flow.

Why an agent code

Without an agent code, PerfShop's 100 enigmas would all share a static hash, and the first student to finish a level could whisper the final answer to their neighbors. The agent code makes the last step unique for each student: the answer depends on a computation applied to the code, and each student has their own.

The technical constraint is strict: the correct answer must never travel over the network. It is computed once server-side at the time of POST /pedagogique/join, hashed with SHA-256, then stored in the session. The student does the computation mentally (or on paper) by reading their agent code in the store, types the answer into the overlay, the frontend hashes it, and the backend compares the two hashes.

Code generation

Generation happens in DefaultPedagogiqueSessionService.createSession(), immediately after creating the session's UUID token. The code depends on the token and on the level:

String agentCode;
if ("bac1".equals(level) || "bac2".equals(level)) {
    // BAC+1 / BAC+2: 100% numeric code
    String digitsOnly = session.token.replaceAll("[^0-9]", "");
    agentCode = digitsOnly.substring(0, Math.min(4, digitsOnly.length()));
} else {
    // BAC+3 to BAC+5: hexadecimal code
    agentCode = session.token.replace("-", "").substring(0, 4).toUpperCase();
}
session.agentCode = agentCode;

The session token is a UUID v4 (format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx in lowercase hexadecimal); it has 36 characters, 32 without the dashes. The agent code is derived by one of the two following procedures:

Level Procedure Example (token a8f3e2b1-…)
bac1, bac2 Keep only the decimal digits of the token, take the first 4 token a8f3e2b1-4c7d-4a6e-9b0f-3e5a7d1c8b42 → digits 83214746903571842 → code 8321
bac3, bac4, bac5 Strip dashes, take the first 4 characters, uppercase token a8f3e2b1-…A8F3

Why two formats

The choice of the numeric format for bac1/bac2 is pedagogical, not technical: students at these levels (post-high school, beginning technical programs) have not necessarily encountered hexadecimal in their curriculum. Forcing them to compute with AF chars for the very last step of a discovery journey would be an unjustified wall.

Conversely, hexadecimal for bac3 and above is itself a pedagogical objective: reading A3F7, converting it to decimal, computing a digit sum in base 16, applying a modulo 256 or a bitwise XOR is part of the expected skills at these levels. The agent code format prepares the student for the corresponding computation formula.

Uniqueness

The probability that two students in the same session get the same agent code is non-zero but low:

  • Numeric format (bac1/bac2): the first 4 digits of a UUID v4 can have between 0 and 4 decimal digits out of their first 8 hex chars, which makes the encoding uneven, but collisions remain anecdotal for classes of fewer than 100 students.
  • Hexadecimal format (bac3+): $16^4 = 65{,}536$ possible codes — the collision probability in a class of 30 students is on the order of 0.7%. In practice, two collisions do not break anything: the two students have the same final answer and no other side-effect.

The agent code is not designed as a cryptographic secret — it is a generator of pedagogical variability.

The five DYNAMIC formulas

The computeDynamicAnswer(String agentCode, String level) method in DefaultPedagogiqueSessionService dispatches by level. Each formula was chosen so that the difficulty of the mental computation matches the target level.

Level Code Answer formula Example (non-spoiler)
bac1 4 decimal digits $R = (\sum_{i=1}^{4} c_i) \times 10$ code 8321 → $(8+3+2+1) \times 10 = 140$
bac2 4 decimal digits $R = \lfloor \text{code} \div \sum c_i \rfloor$ code 8321 → $8321 \div (8+3+2+1) = 8321 \div 14 = 594$
bac3 4 hex chars $R = \sum_{i=1}^{4} h_i$ (decimal value of each hex digit) code A3F7 → $10 + 3 + 15 + 7 = 35$
bac4 4 hex chars $R = \text{code}_{16} \bmod 256$ code A3F7 → $0xA3F7 = 41975$; $41975 \bmod 256 = 247$
bac5 4 hex chars $R = \text{high byte} \oplus \text{low byte}$ code A3F7 → $0xA3 \oplus 0xF7 = 10100011 \oplus 11110111 = 01010100 = 84$

These examples are not answers

The agent code displayed to a student depends on their session token — it is different on every join. The examples above illustrate the mechanism with an arbitrarily chosen code. No student will get 8321 or A3F7 except by coincidence.

Java implementation (reference)

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

The method is static and package-private — it is only called by createSession() in the same com.perfshop.service package.

From /join to final validation

sequenceDiagram
    autonumber
    actor E as Student
    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" (hex digit sum)
    SVC->>SVC: extractionAnswerHash = sha256("35")
    SVC->>DB: INSERT agent_code, extraction_answer_hash
    CTR-->>E: {token, alias, totalSteps}

    Note over E: Walking through the first 19 steps...

    E->>OVR: Step 19 (add_to_cart, checkout)
    OVR->>CTR: POST /validate step=19
    CTR-->>OVR: {valid:true, nextEnigme: step 20 DYNAMIC}

    E->>OC: Order confirmation
    Note over OC: The student reads their<br/>agent code: "A3F7"

    E->>E: Mentally computes<br/>A+3+F+7 = 10+3+15+7 = 35
    E->>OVR: Types "35", clicks Validate
    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: Auto-navigation to /s/{token}

This flow illustrates the key property of the agent code: the correct answer in clear only exists in the student's head and in the service's ephemeral storage at the time of /join. Once hashed, it cannot be recovered without brute force.

Agent code persistence

The agentCode field of MutableSession is volatile and its database equivalent is the agent_code VARCHAR(10) column of the pedagogique_sessions table. A backend restart does not invalidate the code — it is reloaded identically in toMutableSession(), and the stored extractionAnswerHash remains aligned since it depends only on the code + level.

The service also exposes a getAgentCodeForToken(String token) method used by OrderController to display the code on the order confirmation page without exposing the full session to the e-commerce UI.

Non-implemented variations

The formulas above are the only ones implemented to date. The computeDynamicAnswer() method has a default -> "0" that would serve as a defensive fallback if an unknown level were requested — in practice, the controller already rejects any level other than bac1bac5 upstream with an HTTP 400 response. Adding a bac6 level or a regional variation would require two modifications:

  1. Add a case "bac6" -> … in computeDynamicAnswer()
  2. Add the agent code format choice in createSession()
  3. Create the PedagogiqueEnigmeBac6 companion class with its catalog

The defaultTimerSeconds() method should also be extended with the new duration.


Next pages: ← Enigma system · Star system → · Hints →