Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ step, no framework** — static files served as-is. The only runtime dependency
vendored math.js bundle (`vendor/math.min.js`) powering the formula language.

See `README.md` for the full feature list and `docs/CONCEPTS.md` for the simulation
model (tick order, fair allocation, one-step variable lag).
model (tick order, fair allocation, one-step variable lag, synchronous conditions).

## Commands

Expand Down Expand Up @@ -36,6 +36,11 @@ node cli.js diagram.json --runs 1000 --steps 200 --seed 42 --param rate=3
`test(...)` call runs inline, top to bottom, as the file loads. To iterate on one
test, temporarily comment out the others (there is no `test.only`).

**CI:** `.github/workflows/tests.yml` runs the unit tests (`npm test`) and the
browser smoke test (`npm run smoke`, Chromium) on every push to `main` and every
PR. It uses `npm install`, not `npm ci`, because `package-lock.json` is gitignored.
`test/export.js` is a manual-run export check and is **not** part of CI.

## Architecture

### No build, load order matters
Expand All @@ -55,6 +60,17 @@ it lets `test/run.js` load them into a bare `new Function` sandbox, lets `cli.js
run them under Node, and lets Monte Carlo clone a `Diagram` + `SimEngine` per trial.
Do not reach for `document`/`window` in these two files.

Two engine invariants to preserve when editing the tick:
- **Conditions are synchronous.** Activator and connection-condition checks read a
start-of-step snapshot (`_tickSnap`, via `_condValueOf`), so a step's outcome does
not depend on node storage order. Trigger and reverse-trigger cascades are the
deliberate exception and read live mid-step state (queue entries carry a `live`
flag). A permutation test in `test/run.js` guards this.
- **Reproducible RNG.** Seeded randomness flows through `SimRandom`; formula
randomness (`random`/`randomInt`/`pickRandom`, and `Math.random`) is routed through
it too, and `SimRandom.getState`/`setState` let checkpoints capture and restore the
stream position. See docs/CONCEPTS.md "Conditions are synchronous" and §13.

### The App class is split across files via prototype mixins

`App` is one logical class, but it was too large for one file, so it is physically
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ accumulate, convert, and drain — with charts, batch (Monte Carlo) analysis, an
shareable diagrams.

Pure vanilla JavaScript. **No build step, no framework** — just static files and
an SVG canvas. The only dependency is a vendored copy of
[math.js](https://mathjs.org/) powering the formula language.
an SVG canvas. The only runtime code dependency is a vendored copy of
[math.js](https://mathjs.org/) powering the formula language; Font Awesome icons
are vendored too.

> **Status — docs in progress.** A major UI/UX overhaul is underway. This README
> documents the parts that are stable regardless of how the interface looks: the
Expand All @@ -35,15 +36,15 @@ Opening `index.html` directly via `file://` also works for the core editor and
simulation. A few conveniences (clipboard copy of share links, embed routing)
behave best over `http://`, so a local server is recommended.

The app boots with a small example diagram. Use the **Examples** menu to load the
The app boots with a small example diagram. Use the **Library** to load the
bundled samples, or start from scratch.

---

## What it does

You build a directed graph of **nodes** (pools, sources, drains, gates, converters,
registers, delays, queues) joined by **connections** (resource flows or state
registers, delays, queues, traders) joined by **connections** (resource flows or state
links). A discrete-time engine then advances the model one step at a time:

- Resources move along resource connections at configurable rates (fixed, dice,
Expand All @@ -70,6 +71,7 @@ thousands of Monte‑Carlo trials, and share the whole diagram in a URL.
- **Register** — a computed value from a **formula** over shared variables (chains across registers, resolved to a fixpoint each tick).
- **Delay** — holds a batch of resources for N steps, then releases them together.
- **Queue** — a FIFO line feeding one or more parallel `servers` (`servers ÷ processTime` throughput); a single server is the classic bottleneck with per‑item latency, distinct from Delay. Optional `maxLine` (balk) and `patience` (renege) model lost demand. Reports live metrics: throughput, average/longest wait, peak line length, balked/reneged losses.
- **Trader** — an atomic exchange between two partners: `A → T → B` means A pays the in‑rate to B and B pays the out‑rate back to A, all or nothing. Extra in/out pairs trade in wiring order; the trader itself never holds resources.

### Activation modes
`automatic` (fires every step), `passive` (only when triggered), `interactive`
Expand Down Expand Up @@ -231,7 +233,8 @@ no bundler.
| `js/renderer.js` | `Renderer` (SVG drawing, hit‑testing, pan/zoom) and `BallSystem` (flow animation). |
| `js/editor.js` | `Editor` — pointer/keyboard/touch input, tools, selection, drag‑to‑connect. |
| `js/charts.js` | `Sparkline` and `TimelineChart` (canvas 2D). |
| `js/app.js` | `App` — wires everything together: toolbar, properties panel, persistence, examples, import/export. |
| `js/kb.js` | Concept‑guide / knowledge‑base article content shown in the in‑app help. |
| `js/app.js` (+ `js/app-props.js`, `-demos`, `-analysis`, `-fields`, `-library`, `-clipboard`, `-export`) | `App` — one logical class split across prototype‑mixin files: toolbar, properties panel, persistence, examples, import/export, Monte Carlo / sweeps / sensitivity, and clipboard. |
| `cli.js` | Headless Node runner — simulate a diagram JSON from the terminal (traces, Monte Carlo, seeds, param overrides). |
| `css/style.css` | All styling. |
| `vendor/math.min.js` | Vendored [math.js](https://mathjs.org/) bundle — the formula evaluator. |
Expand Down Expand Up @@ -277,6 +280,10 @@ NODE_PATH=$(npm root -g) node test/smoke.js

Requires `playwright` (and a Chromium build) available on `NODE_PATH`.

Both suites run automatically on every push and pull request via GitHub Actions
(`.github/workflows/tests.yml`); the badge at the top of this file reflects the
latest run. (`test/export.js` is a manual‑run export check, not part of CI.)

---

## Browser support
Expand Down
10 changes: 6 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ small, 3 is the one larger investment worth making for that audience.
- [x] Animated resource balls along connections
- [x] Per-node history sparkline
- [x] Save / load JSON, sample diagrams (Basic / Loot Farm / Factory Line)
- [x] Headless unit test suite + Playwright smoke test
- [x] Headless unit test suite + Playwright smoke test, run automatically on every
push and PR via GitHub Actions (`.github/workflows/tests.yml`; status badge in the README)

---

Expand Down Expand Up @@ -215,8 +216,9 @@ small, 3 is the one larger investment worth making for that audience.
- [x] **Checkpoint / fork / compare.** `SimEngine.captureState()` /
`restoreState()` snapshot the complete simulation state — diagram
structure, node runtime (resources, in-flight delay/queue contents,
counters) plus the Reset baselines, the variable store, and the engine
clock/history/trigger state. A "Branch" rail panel checkpoints the run
counters) plus the Reset baselines, the variable store, the engine
clock/history/trigger state, and the RNG position (so a forked branch
replays reproducibly under a seed). A "Branch" rail panel checkpoints the run
mid-flight and forks back to any checkpoint; the superseded run is kept
automatically as a **ghost branch** — dashed, faded traces overlaid on
the timeline chart (same colour = same node across timelines, x-axis in
Expand Down Expand Up @@ -284,7 +286,7 @@ knowledge) drove the real app and reported friction. Fixes that shipped from it:
re-selecting a connection is still fiddly despite the 24px hit area.
_(Two earlier snags here have since shipped: the welcome glossary now
lists all nine node types, and connections show their configured rate
at rest — `renderer.js` ~L1143 — not only the flow badge while running.)_
at rest — `renderer.js` ~L1163 — not only the flow badge while running.)_

## ✅ Sensitivity analysis

Expand Down
19 changes: 13 additions & 6 deletions docs/CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,13 @@ From `SimEngine._tick()`:
targets.
4. **Artificial player.** Scheduled/conditional interactive nodes fire as if clicked
(still within this tick).
5. **Advance delays and queues.** Matured delay batches and finished queue units are
5. **Resolve push contention** (`_applyPushProposals`). Where several source nodes
pushed into the same target, capacity is fair‑allocated among them before delays
and queues claim any remaining room.
6. **Advance delays and queues.** Matured delay batches and finished queue units are
released (respecting target capacity).
6. **Commit atomically** (`_applyCtx`). All accumulated movements apply at once.
7. **Apply modifiers**, then **refresh variables** and **evaluate registers**.
7. **Commit atomically** (`_applyCtx`). All accumulated movements apply at once.
8. **Apply modifiers**, then **refresh variables** and **evaluate registers**.

### Conditions are synchronous

Expand Down Expand Up @@ -222,9 +225,13 @@ Registers are evaluated to a **fixpoint** (bounded by the register count) so a
register that references another register's label resolves correctly in a single
tick, regardless of creation order.

**One‑step lag.** Variables are committed at the *end* of a tick, so a connection
condition or rate formula reads the value from the **previous** step. This is by
design and is consistent across the model (the unit tests encode it explicitly).
**One‑step lag.** Variables are committed at the *end* of a tick, so a **rate
formula** — and any **condition that references a named variable** — reads the value
from the **previous** step. This is by design and is consistent across the model (the
unit tests encode it explicitly). A condition that tests a **source node's value**
instead reads that node's **tick‑start snapshot** (§7, "Conditions are synchronous"):
the same start‑of‑step value every check sees, which is what keeps the result
independent of node storage order rather than lagged a full step behind.

**Computing vs. accumulating.** A register is **recomputed from its formula every
tick** — it holds an *instantaneous* value, not a running total, and cannot read its
Expand Down
12 changes: 6 additions & 6 deletions js/kb.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ const KB_ARTICLES = [
+ 'the app places a fresh independent copy (new node IDs, ready to edit) '
+ 'so modifying a placed copy never touches the saved original or any other '
+ 'instance. Components are a fast way to build repeated patterns such as a '
+ 'sourcepooldrain trio, a feedback loop or a probability gate, without '
+ 'source-pool-drain trio, a feedback loop or a probability gate, without '
+ 'redrawing them every time.',
},
{
Expand Down Expand Up @@ -678,10 +678,10 @@ const KB_ARTICLES = [
},
{
id: 'saving', category: 'Saving and sharing', title: 'Saving and loading',
keywords: 'save load autosave library file JSON export import browser recovery banner',
keywords: 'save load autosave library file JSON export import browser recovery',
body: 'The diagram saves to the browser automatically on every change, so '
+ 'closing the tab does not lose your work. On your next visit, a banner '
+ 'offers to restore the last session if the canvas is empty. To keep '
+ 'closing the tab does not lose your work. On your next visit the app '
+ 'restores your last diagram automatically, so you pick up where you left off. To keep '
+ 'multiple named diagrams, open the Library and click Save diagram, and each '
+ 'entry is stored independently in the browser and can be loaded, renamed '
+ 'or deleted at any time. Use File → Save as JSON to download the current '
Expand All @@ -691,8 +691,8 @@ const KB_ARTICLES = [
{
id: 'sharing', category: 'Saving and sharing', title: 'Sharing and embedding',
keywords: 'share URL link embed export SVG PNG publish present encode hash clipboard',
body: 'Click Share in the File menu to encode the current diagram into the '
+ 'URL. The link contains the whole diagram, so anyone you send it to opens '
body: 'Click Copy share link in the File menu to encode the current diagram into '
+ 'a URL and copy it to your clipboard. The link contains the whole diagram, so anyone you send it to opens '
+ 'the same model in their browser, with no sign-in or file transfer required. '
+ 'For a clean view without the editing chrome, add ?embed to the URL or '
+ 'append #embed to the hash; all the toolbars and panels hide, leaving only '
Expand Down
Loading