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 A–F 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
bac1–bac5 upstream with an HTTP 400 response. Adding a bac6
level or a regional variation would require two modifications:
- Add a
case "bac6" -> …incomputeDynamicAnswer() - Add the agent code format choice in
createSession() - Create the
PedagogiqueEnigmeBac6companion class with its catalog
The defaultTimerSeconds() method should also be extended with the
new duration.
Next pages: ← Enigma system · Star system → · Hints →