CodeSignal Probability Lab is an interactive probability simulator for repeated-trial experiments. It is designed to help learners compare theoretical probability with experimental results and see convergence over time.
The app currently supports:
- Single-event experiments with a coin, die, spinner, or custom device
- Two-event experiments with joint outcomes shown in a heatmap and two-way table
- Fair and biased devices
- Independent and dependent relationships in two-event mode
- Custom devices with 2-50 outcomes and optional custom probabilities
- Optional UI sections such as bar chart, convergence chart, frequency table, joint distribution, two-way table, and single-mode history
- Activity logging to
activity.logfor grading
npm ci
# or
npm installnpm run start:devThen open http://localhost:3000.
Development uses two local servers:
http://localhost:3000: Vite dev server for the app UIhttp://localhost:3001: API server that accepts/logrequests and writesactivity.log
In dev mode, the browser sends activity events to /log, and Vite proxies those requests to port 3001.
npm run build
npm run start:prodThis serves the built app on http://localhost:3000 and writes activity events to the same root-level activity.log file.
By default, the production server reads ./config.json from the repository root. You can point it at another file with CONFIG_PATH.
CONFIG_PATH=./some-other-config.json npm run start:prodThe app loads its runtime configuration from /config.json.
If a field is missing or invalid, the app falls back to safe defaults. Invalid custom probabilities are normalized when possible; otherwise they fall back to a uniform distribution.
| Key | Used in | Description |
|---|---|---|
mode |
all configs | "single" or "two" |
device |
single mode | Initial device: "coin", "die", "spinner", or "custom" |
deviceA |
two mode | Initial device for event A |
deviceB |
two mode | Initial device for event B |
deviceSettings |
single mode with device: "custom" |
Custom device definition |
deviceASettings |
two mode with deviceA: "custom" |
Custom device definition for A |
deviceBSettings |
two mode with deviceB: "custom" |
Custom device definition for B |
sections |
all configs | Controls which result panels are visible |
visualElements |
all configs | Controls selected UI elements such as the edit button and bias tag |
| Setting | Valid values | Default if missing or invalid |
|---|---|---|
mode |
single, two |
single |
device, deviceA, deviceB |
coin, die, spinner, custom |
coin |
sections.* |
true, false |
false for each supported section key |
visualElements.editExperimentButton |
true, false |
true |
visualElements.biasTag |
true, false |
true |
Single-mode section keys:
barChartconvergencefrequencyTablehistory
Two-mode section keys:
jointDistributiontwoWayTable
Notes:
historyonly applies to single mode- When
historyistruein single mode, history is shown as a standalone widget card instead of only through the History modal - Missing or invalid section values default to
false
Supported UI toggles:
editExperimentButtonbiasTag
Both default to true.
Each custom device settings object can include:
| Key | Required | Description |
|---|---|---|
name |
no | Display name for the custom device |
icon |
no | Optional icon string shown in the UI |
outcomes |
yes | Array of outcome labels |
probabilities |
no | Array of non-negative weights or probabilities aligned with outcomes |
Current constraints:
outcomesmust contain 2-50 unique, non-empty strings- Extra outcomes beyond 50 are truncated
- Duplicate or empty outcome labels are ignored
- If
probabilitiesis present, it must match the final outcome count - Probability values must be non-negative numbers
- Probability values are normalized to sum to
1 - If
probabilitiesis missing, invalid, or sums to0, the app uses a uniform distribution
Do not pretend config.json controls everything. It does not.
These are adjusted in the app UI, not through runtime config:
- Bias settings for coin, die, and spinner
- Spinner sector count
- Selected event outcomes in single mode
- Relationship mode in two-event experiments (
independentordependent)
{
"mode": "single",
"device": "die",
"sections": {
"barChart": true,
"convergence": true,
"frequencyTable": true,
"history": false
},
"visualElements": {
"editExperimentButton": true,
"biasTag": true
}
}{
"mode": "single",
"device": "custom",
"deviceSettings": {
"name": "Exam",
"icon": "π",
"outcomes": ["Pass", "Fail"],
"probabilities": [0.7, 0.3]
},
"sections": {
"barChart": true,
"convergence": true,
"frequencyTable": true,
"history": true
},
"visualElements": {
"editExperimentButton": true,
"biasTag": true
}
}{
"mode": "two",
"deviceA": "coin",
"deviceB": "die",
"sections": {
"jointDistribution": true,
"twoWayTable": true
},
"visualElements": {
"editExperimentButton": true,
"biasTag": true
}
}{
"mode": "two",
"deviceA": "custom",
"deviceASettings": {
"name": "Weather",
"icon": "π¦",
"outcomes": ["Sunny", "Rainy", "Snowy"],
"probabilities": [0.6, 0.3, 0.1]
},
"deviceB": "custom",
"deviceBSettings": {
"name": "Traffic",
"icon": "π",
"outcomes": ["Light", "Medium", "Heavy"],
"probabilities": [0.5, 0.35, 0.15]
},
"sections": {
"jointDistribution": true,
"twoWayTable": true
},
"visualElements": {
"editExperimentButton": true,
"biasTag": true
}
}The app writes grading or review data to a root-level activity.log file as JSON Lines: one JSON object per line.
Behavior by environment:
- Development: the browser posts to
/log, Vite proxies that request to the API server on port3001, andserver.jsappends toactivity.log - Production:
server.jsserves the built app and appends the same event stream toactivity.log
The log is append-only. If you want a clean grading run, you need to clear or rotate the file yourself before starting.
| Event type | When it appears | What it contains |
|---|---|---|
app_start |
Initial app load | A config snapshot with mode, device selection, and visible sections |
settings_change |
User changes settings | Changed keys plus a full settings snapshot |
run_reset |
A run is reset because settings changed | Reset reason plus a full settings snapshot |
status |
At trial milestones during simulation | Current trial count and mode-specific results |
click |
User clicks a selected cell in two-event mode | Source and selected cell labels |
status events are not written on every single trial forever. They are logged at milestone counts:
1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, ...
That keeps the log useful without making it absurdly noisy.
Single-mode status events can include:
trialslastOutcomeevent.selectedOutcomesevent.pEstimatedevent.pTheoreticalbarChart.rowswhen the bar chart section is enabledconvergencewhen the convergence section is enabledfrequencyTable.rowswhen the frequency table section is enabled
Two-mode status events can include:
trialslastOutcome.aandlastOutcome.brelationshipjointDistribution.labelsA,jointDistribution.labelsB,jointDistribution.matrixReltwoWayTable.labelsA,twoWayTable.labelsB,twoWayTable.jointCounts
Single-mode status:
{"type":"status","data":{"mode":"single","trials":100,"lastOutcome":"Heads","event":{"selectedOutcomes":["Heads"],"pEstimated":0.55,"pTheoretical":0.5},"barChart":{"rows":[{"outcome":"Heads","count":55,"relFreq":0.55},{"outcome":"Tails","count":45,"relFreq":0.45}]}}}Two-mode status:
{"type":"status","data":{"mode":"two","trials":200,"lastOutcome":{"a":"Sunny","b":"Heavy"},"relationship":"independent","jointDistribution":{"labelsA":["Sunny","Rainy"],"labelsB":["Light","Heavy"],"matrixRel":[[0.4,0.2],[0.3,0.1]]},"twoWayTable":{"labelsA":["Sunny","Rainy"],"labelsB":["Light","Heavy"],"jointCounts":[[80,40],[60,20]]}}}settings_change:
{"type":"settings_change","data":{"changed":["bias"],"settings":{"mode":"single","device":"coin","sections":{"barChart":true,"convergence":true,"frequencyTable":true,"jointDistribution":false,"twoWayTable":false},"spinnerSectors":8,"bias":{"coinProbabilities":[1,0],"dieProbabilities":[0.167,0.167,0.167,0.167,0.167,0.167],"spinnerSkew":0},"eventSelected":["Heads"]}}}run_reset:
{"type":"run_reset","data":{"reason":"bias_change","settings":{"mode":"single","device":"coin","sections":{"barChart":true,"convergence":true,"frequencyTable":true,"jointDistribution":false,"twoWayTable":false},"spinnerSectors":8,"bias":{"coinProbabilities":[0,1],"dieProbabilities":[0.167,0.167,0.167,0.167,0.167,0.167],"spinnerSkew":0},"eventSelected":["Heads"]}}}click:
{"type":"click","data":{"source":"jointDistributionHeatmap","cell":{"r":0,"c":1},"labels":{"a":"Sunny","b":"Heavy"}}}This repository has a GitHub Actions workflow at .github/workflows/build-release.yml.
Current behavior:
- Every push to
maintriggers the workflow - The workflow checks out the repo and initializes the design-system submodule
- It installs dependencies with
npm ci - It builds the app with
npm run build - It installs production dependencies only
- It creates a
release.tar.gzarchive containing the built app,server.js,package.json, and productionnode_modules - It uploads the build artifact in GitHub Actions
- It publishes a GitHub Release automatically