|
1 | 1 | # CodeSignal Probability Lab |
2 | 2 |
|
3 | | -A Bespoke Simulation for repeated-trial probability experiments (coin, die, spinner) that visualizes convergence: as you run more trials, relative frequencies become more stable and tend to approach theoretical probabilities. |
| 3 | +## Overview |
4 | 4 |
|
5 | | -## What’s Included |
| 5 | +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. |
6 | 6 |
|
7 | | -- **One event mode**: event builder (select outcomes), live bar chart, convergence chart, frequency table |
8 | | -- **Two events mode**: joint heatmap + two-way table; click a cell to see joint and conditional probabilities |
9 | | -- **Bias controls**: explore fair vs biased devices |
| 7 | +The app currently supports: |
10 | 8 |
|
11 | | -## Development |
| 9 | +- Single-event experiments with a coin, die, spinner, or custom device |
| 10 | +- Two-event experiments with joint outcomes shown in a heatmap and two-way table |
| 11 | +- Fair and biased devices |
| 12 | +- Independent and dependent relationships in two-event mode |
| 13 | +- Custom devices with 2-50 outcomes and optional custom probabilities |
| 14 | +- Optional UI sections such as bar chart, convergence chart, frequency table, joint distribution, two-way table, and single-mode history |
| 15 | +- Activity logging to `activity.log` for grading |
| 16 | + |
| 17 | +## Using the App |
| 18 | + |
| 19 | +### Install dependencies |
| 20 | + |
| 21 | +```bash |
| 22 | +npm ci |
| 23 | +# or |
| 24 | +npm install |
| 25 | +``` |
| 26 | + |
| 27 | +### Start in development |
12 | 28 |
|
13 | 29 | ```bash |
14 | 30 | npm run start:dev |
15 | 31 | ``` |
16 | 32 |
|
17 | | -Open `http://localhost:3000`. |
| 33 | +Then open [http://localhost:3000](http://localhost:3000). |
18 | 34 |
|
19 | | -## Build / Production |
| 35 | +Development uses two local servers: |
| 36 | + |
| 37 | +- `http://localhost:3000`: Vite dev server for the app UI |
| 38 | +- `http://localhost:3001`: API server that accepts `/log` requests and writes `activity.log` |
| 39 | + |
| 40 | +In dev mode, the browser sends activity events to `/log`, and Vite proxies those requests to port `3001`. |
| 41 | + |
| 42 | +### Start a production-style build locally |
20 | 43 |
|
21 | 44 | ```bash |
22 | 45 | npm run build |
23 | 46 | npm run start:prod |
24 | 47 | ``` |
25 | 48 |
|
26 | | -## Key Files |
| 49 | +This serves the built app on [http://localhost:3000](http://localhost:3000) and writes activity events to the same root-level `activity.log` file. |
| 50 | + |
| 51 | +### Use an alternate production config |
| 52 | + |
| 53 | +By default, the production server reads `./config.json` from the repository root. You can point it at another file with `CONFIG_PATH`. |
| 54 | + |
| 55 | +```bash |
| 56 | +CONFIG_PATH=./some-other-config.json npm run start:prod |
| 57 | +``` |
| 58 | + |
| 59 | +## Configuring `config.json` |
| 60 | + |
| 61 | +The app loads its runtime configuration from `/config.json`. |
| 62 | + |
| 63 | +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. |
| 64 | + |
| 65 | +### Supported top-level keys |
| 66 | + |
| 67 | +| Key | Used in | Description | |
| 68 | +| --- | --- | --- | |
| 69 | +| `mode` | all configs | `"single"` or `"two"` | |
| 70 | +| `device` | single mode | Initial device: `"coin"`, `"die"`, `"spinner"`, or `"custom"` | |
| 71 | +| `deviceA` | two mode | Initial device for event A | |
| 72 | +| `deviceB` | two mode | Initial device for event B | |
| 73 | +| `deviceSettings` | single mode with `device: "custom"` | Custom device definition | |
| 74 | +| `deviceASettings` | two mode with `deviceA: "custom"` | Custom device definition for A | |
| 75 | +| `deviceBSettings` | two mode with `deviceB: "custom"` | Custom device definition for B | |
| 76 | +| `sections` | all configs | Controls which result panels are visible | |
| 77 | +| `visualElements` | all configs | Controls selected UI elements such as the edit button and bias tag | |
| 78 | + |
| 79 | +### Supported values and defaults |
| 80 | + |
| 81 | +| Setting | Valid values | Default if missing or invalid | |
| 82 | +| --- | --- | --- | |
| 83 | +| `mode` | `single`, `two` | `single` | |
| 84 | +| `device`, `deviceA`, `deviceB` | `coin`, `die`, `spinner`, `custom` | `coin` | |
| 85 | +| `sections.*` | `true`, `false` | `false` for each supported section key | |
| 86 | +| `visualElements.editExperimentButton` | `true`, `false` | `true` | |
| 87 | +| `visualElements.biasTag` | `true`, `false` | `true` | |
| 88 | + |
| 89 | +### `sections` keys |
| 90 | + |
| 91 | +Single-mode section keys: |
| 92 | + |
| 93 | +- `barChart` |
| 94 | +- `convergence` |
| 95 | +- `frequencyTable` |
| 96 | +- `history` |
| 97 | + |
| 98 | +Two-mode section keys: |
| 99 | + |
| 100 | +- `jointDistribution` |
| 101 | +- `twoWayTable` |
| 102 | + |
| 103 | +Notes: |
| 104 | + |
| 105 | +- `history` only applies to single mode |
| 106 | +- When `history` is `true` in single mode, history is shown as a standalone widget card instead of only through the History modal |
| 107 | +- Missing or invalid section values default to `false` |
| 108 | + |
| 109 | +### `visualElements` keys |
| 110 | + |
| 111 | +Supported UI toggles: |
| 112 | + |
| 113 | +- `editExperimentButton` |
| 114 | +- `biasTag` |
| 115 | + |
| 116 | +Both default to `true`. |
| 117 | + |
| 118 | +### Custom device settings |
| 119 | + |
| 120 | +Each custom device settings object can include: |
| 121 | + |
| 122 | +| Key | Required | Description | |
| 123 | +| --- | --- | --- | |
| 124 | +| `name` | no | Display name for the custom device | |
| 125 | +| `icon` | no | Optional icon string shown in the UI | |
| 126 | +| `outcomes` | yes | Array of outcome labels | |
| 127 | +| `probabilities` | no | Array of non-negative weights or probabilities aligned with `outcomes` | |
| 128 | + |
| 129 | +Current constraints: |
| 130 | + |
| 131 | +- `outcomes` must contain 2-50 unique, non-empty strings |
| 132 | +- Extra outcomes beyond 50 are truncated |
| 133 | +- Duplicate or empty outcome labels are ignored |
| 134 | +- If `probabilities` is present, it must match the final outcome count |
| 135 | +- Probability values must be non-negative numbers |
| 136 | +- Probability values are normalized to sum to `1` |
| 137 | +- If `probabilities` is missing, invalid, or sums to `0`, the app uses a uniform distribution |
| 138 | + |
| 139 | +### What is not configured through `config.json` |
| 140 | + |
| 141 | +Do not pretend `config.json` controls everything. It does not. |
| 142 | + |
| 143 | +These are adjusted in the app UI, not through runtime config: |
| 144 | + |
| 145 | +- Bias settings for coin, die, and spinner |
| 146 | +- Spinner sector count |
| 147 | +- Selected event outcomes in single mode |
| 148 | +- Relationship mode in two-event experiments (`independent` or `dependent`) |
| 149 | + |
| 150 | +## `config.json` Examples |
| 151 | + |
| 152 | +### Single mode with a standard device |
| 153 | + |
| 154 | +```json |
| 155 | +{ |
| 156 | + "mode": "single", |
| 157 | + "device": "die", |
| 158 | + "sections": { |
| 159 | + "barChart": true, |
| 160 | + "convergence": true, |
| 161 | + "frequencyTable": true, |
| 162 | + "history": false |
| 163 | + }, |
| 164 | + "visualElements": { |
| 165 | + "editExperimentButton": true, |
| 166 | + "biasTag": true |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +### Single mode with a custom device |
| 172 | + |
| 173 | +```json |
| 174 | +{ |
| 175 | + "mode": "single", |
| 176 | + "device": "custom", |
| 177 | + "deviceSettings": { |
| 178 | + "name": "Exam", |
| 179 | + "icon": "📚", |
| 180 | + "outcomes": ["Pass", "Fail"], |
| 181 | + "probabilities": [0.7, 0.3] |
| 182 | + }, |
| 183 | + "sections": { |
| 184 | + "barChart": true, |
| 185 | + "convergence": true, |
| 186 | + "frequencyTable": true, |
| 187 | + "history": true |
| 188 | + }, |
| 189 | + "visualElements": { |
| 190 | + "editExperimentButton": true, |
| 191 | + "biasTag": true |
| 192 | + } |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +### Two-event mode with standard devices |
| 197 | + |
| 198 | +```json |
| 199 | +{ |
| 200 | + "mode": "two", |
| 201 | + "deviceA": "coin", |
| 202 | + "deviceB": "die", |
| 203 | + "sections": { |
| 204 | + "jointDistribution": true, |
| 205 | + "twoWayTable": true |
| 206 | + }, |
| 207 | + "visualElements": { |
| 208 | + "editExperimentButton": true, |
| 209 | + "biasTag": true |
| 210 | + } |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +### Two-event mode with custom devices |
| 215 | + |
| 216 | +```json |
| 217 | +{ |
| 218 | + "mode": "two", |
| 219 | + "deviceA": "custom", |
| 220 | + "deviceASettings": { |
| 221 | + "name": "Weather", |
| 222 | + "icon": "🌦", |
| 223 | + "outcomes": ["Sunny", "Rainy", "Snowy"], |
| 224 | + "probabilities": [0.6, 0.3, 0.1] |
| 225 | + }, |
| 226 | + "deviceB": "custom", |
| 227 | + "deviceBSettings": { |
| 228 | + "name": "Traffic", |
| 229 | + "icon": "🚗", |
| 230 | + "outcomes": ["Light", "Medium", "Heavy"], |
| 231 | + "probabilities": [0.5, 0.35, 0.15] |
| 232 | + }, |
| 233 | + "sections": { |
| 234 | + "jointDistribution": true, |
| 235 | + "twoWayTable": true |
| 236 | + }, |
| 237 | + "visualElements": { |
| 238 | + "editExperimentButton": true, |
| 239 | + "biasTag": true |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +## Activity Logging and Grading |
| 245 | + |
| 246 | +The app writes grading or review data to a root-level `activity.log` file as JSON Lines: one JSON object per line. |
| 247 | + |
| 248 | +Behavior by environment: |
| 249 | + |
| 250 | +- Development: the browser posts to `/log`, Vite proxies that request to the API server on port `3001`, and `server.js` appends to `activity.log` |
| 251 | +- Production: `server.js` serves the built app and appends the same event stream to `activity.log` |
| 252 | + |
| 253 | +The log is append-only. If you want a clean grading run, you need to clear or rotate the file yourself before starting. |
| 254 | + |
| 255 | +### Event types written today |
| 256 | + |
| 257 | +| Event type | When it appears | What it contains | |
| 258 | +| --- | --- | --- | |
| 259 | +| `app_start` | Initial app load | A config snapshot with mode, device selection, and visible sections | |
| 260 | +| `settings_change` | User changes settings | Changed keys plus a full settings snapshot | |
| 261 | +| `run_reset` | A run is reset because settings changed | Reset reason plus a full settings snapshot | |
| 262 | +| `status` | At trial milestones during simulation | Current trial count and mode-specific results | |
| 263 | +| `click` | User clicks a selected cell in two-event mode | Source and selected cell labels | |
| 264 | + |
| 265 | +### Status milestone schedule |
| 266 | + |
| 267 | +`status` events are not written on every single trial forever. They are logged at milestone counts: |
| 268 | + |
| 269 | +`1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, ...` |
| 270 | + |
| 271 | +That keeps the log useful without making it absurdly noisy. |
| 272 | + |
| 273 | +### Mode-specific `status` payloads |
| 274 | + |
| 275 | +Single-mode `status` events can include: |
| 276 | + |
| 277 | +- `trials` |
| 278 | +- `lastOutcome` |
| 279 | +- `event.selectedOutcomes` |
| 280 | +- `event.pEstimated` |
| 281 | +- `event.pTheoretical` |
| 282 | +- `barChart.rows` when the bar chart section is enabled |
| 283 | +- `convergence` when the convergence section is enabled |
| 284 | +- `frequencyTable.rows` when the frequency table section is enabled |
| 285 | + |
| 286 | +Two-mode `status` events can include: |
| 287 | + |
| 288 | +- `trials` |
| 289 | +- `lastOutcome.a` and `lastOutcome.b` |
| 290 | +- `relationship` |
| 291 | +- `jointDistribution.labelsA`, `jointDistribution.labelsB`, `jointDistribution.matrixRel` |
| 292 | +- `twoWayTable.labelsA`, `twoWayTable.labelsB`, `twoWayTable.jointCounts` |
| 293 | + |
| 294 | +### Example log lines |
| 295 | + |
| 296 | +Single-mode `status`: |
| 297 | + |
| 298 | +```json |
| 299 | +{"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}]}}} |
| 300 | +``` |
| 301 | + |
| 302 | +Two-mode `status`: |
| 303 | + |
| 304 | +```json |
| 305 | +{"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]]}}} |
| 306 | +``` |
| 307 | + |
| 308 | +`settings_change`: |
| 309 | + |
| 310 | +```json |
| 311 | +{"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"]}}} |
| 312 | +``` |
| 313 | + |
| 314 | +`run_reset`: |
| 315 | + |
| 316 | +```json |
| 317 | +{"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"]}}} |
| 318 | +``` |
| 319 | + |
| 320 | +`click`: |
| 321 | + |
| 322 | +```json |
| 323 | +{"type":"click","data":{"source":"jointDistributionHeatmap","cell":{"r":0,"c":1},"labels":{"a":"Sunny","b":"Heavy"}}} |
| 324 | +``` |
| 325 | + |
| 326 | +## CI/CD and Automated Releases |
| 327 | + |
| 328 | +This repository has a GitHub Actions workflow at [`.github/workflows/build-release.yml`](/Users/diego/repos/bespoke-sims/learn_probability-lab/.github/workflows/build-release.yml). |
| 329 | + |
| 330 | +Current behavior: |
| 331 | + |
| 332 | +- Every push to `main` triggers the workflow |
| 333 | +- The workflow checks out the repo and initializes the design-system submodule |
| 334 | +- It installs dependencies with `npm ci` |
| 335 | +- It builds the app with `npm run build` |
| 336 | +- It installs production dependencies only |
| 337 | +- It creates a `release.tar.gz` archive containing the built app, `server.js`, `package.json`, and production `node_modules` |
| 338 | +- It uploads the build artifact in GitHub Actions |
| 339 | +- It publishes a GitHub Release automatically |
27 | 340 |
|
28 | | -- `client/index.html` – app shell + layout |
29 | | -- `client/app.js` – simulation engine + rendering |
30 | | -- `client/app.css` – app-specific styling |
31 | | -- `client/help-content.html` – Help modal content |
|
0 commit comments