Game Client¶
app/src/BartGame.tsx
is a self-contained React component (bundled by Vite into a static SPA) that
administers the dynamic-hazard BART and captures pump-level telemetry. Its UI
strings are in Turkish, matching the reference study’s cohort.
Component contract¶
import BartGame from "./BartGame";
<BartGame
candidateId="participant-123"
onComplete={(result) => {
// result is the scored AssessmentResult returned by your backend
}}
/>
Prop |
Type |
Purpose |
|---|---|---|
|
|
Identifier attached to the submitted session. |
|
|
Called with the scored result after submission. When provided, the final button reads “Next” instead of “See my results”; when omitted, the component shows its own results screen. |
Session generation¶
generateSessionConfig() builds 30 balloons — 10 each of orange, teal, and
purple — then shuffles them with a Fisher–Yates pass. Each balloon stores its
color and maxPumps (\(N\) = 8 / 32 / 128).
The explosion model¶
The client implements exactly the same hazard rule as the scoring engine. On the \(k\)-th pump of a balloon with capacity \(N\):
const explosionProbability = newPumps / maxPumps; // k / N
const explode = newPumps >= maxPumps || Math.random() < explosionProbability;
The newPumps >= maxPumps guard caps the balloon at \(N\), mirroring the engine’s
distribution cap.
Event logging¶
Every action appends a GameEvent-shaped record to an
in-memory log with a high-resolution monotonic timestamp:
eventLogRef.current.push({
timestamp: performance.now(),
type, // "pump" | "collect" | "explode"
payload: { balloon_id, color, ...extra },
});
performance.now() is used (not Date.now()) for sub-millisecond, monotonic
timing — which is what the engine’s latency and auto-repeat checks rely on.
Controls: Space pumps, Enter collects. Each collected pump is worth $0.25; a burst forfeits the balloon.
Submission¶
On completion the component POSTs the session to a backend endpoint:
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
await fetch(`${apiUrl}/assessments/bart`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session_id, // crypto.randomUUID()
game_type: "BART_RISK",
candidate_id,
events, // the full GameEvent[] log
}),
});
The POST body matches the GameSession schema exactly.
The scoring endpoint is yours to provide
This repository ships the game client and the scoring engine as reusable
parts, but not the HTTP server that connects them. The client expects a backend
that accepts the GameSession payload at POST /assessments/bart, runs
scoring.bart.score_bart(), and returns an AssessmentResult. A minimal
adapter (e.g. FastAPI) looks like:
from fastapi import FastAPI
from scoring.schemas import GameSession
from scoring.bart import score_bart
app = FastAPI()
@app.post("/assessments/bart")
def score(session: GameSession):
metrics = score_bart(session.events)
return {
"session_id": session.session_id,
"game_type": session.game_type,
"candidate_id": session.candidate_id,
"raw_metrics": metrics.model_dump(),
"normalized_scores": [], # fill in against your population norms
}
The normalized_scores and profile_traits fields the client renders are
population-relative and depend on norms you supply; they are optional and may be
returned empty.