# FLUXFLOW

A modern reimagining of **Pipe Dream** (1991). Lay pipe across a factory grid before the
ooze escapes through an open end, flood the pipeline past its target, and survive thirty
levels of steadily rising pressure.

Zero dependencies, zero build step, zero downloaded assets — every pipe, every glow and
every note is generated at runtime in the browser.

## Play it

```bash
python3 -m http.server 8080     # or any static file server
# open http://localhost:8080/
```

It must be served over `http://` rather than opened as a `file://` path, because the
game ships as ES modules.

`?level=N` (1–30) drops straight into a level, which is handy for sharing a board or
grabbing a screenshot.

## The rules

You never choose your piece — only where it goes. That is the whole game.

| | |
|---|---|
| **Place** | Click or tap any empty tile to drop the highlighted piece from the queue. |
| **Overwrite** | Dropping onto existing pipe replaces it for a small penalty — but never onto pipe the ooze has already reached. |
| **Target** | Flood at least the level's target number of pipes and it's yours. Every pipe past the target is worth extra. |
| **Crossovers** | The ooze passes straight through a cross, so it can be flooded twice. The second channel pays a large bonus. |
| **Bonus pads** | Build pipe on a ★ pad and flood it for a burst of points. |
| **Drains** | From level 10 a drain appears. Reach it *after* hitting target for a huge bonus and an instant clear; arrive early and it stays sealed — a spill. |
| **Spills** | An open end costs an engineer. Three engineers per run. Spilling *after* target still clears the level. |

**Controls** — `Space` fast-forwards the flow and scores as it goes · `D` discards the
next piece · `Enter` releases the ooze early · `Esc` pauses · `M` toggles music.

## How it fits together

| File | Responsibility |
|---|---|
| `src/pieces.js` | Pipe topology: openings, channels, and where flow entering one side leaves. |
| `src/levels.js` | The difficulty curve, the six visual zones, and seeded board generation. |
| `src/game.js` | Rules and simulation. Pure state — it knows nothing about canvas or DOM. |
| `src/render.js` | Canvas renderer: plate, pipes, ooze, particles, ghost preview, shake. |
| `src/audio.js` | WebAudio synthesis — every cue and a generative underscore that tightens with the flow. |
| `src/attract.js` | The self-playing board behind the menus. |
| `src/ui.js` | Piece SVGs and the overlay screens' markup. |
| `src/main.js` | Loop, input, HUD, and the screen state machine. |
| `src/storage.js` | Guarded `localStorage` for settings, unlocks and the leaderboard. |

Boards are generated from a per-level seed, so every player faces the identical grid —
which is what makes the leaderboard mean anything.

## Tests

```bash
node test/engine.test.mjs
```

86 assertions against the real `Game` class: placement rules, flow and leak detection,
scoring, the crossover bonus, the sealed drain, and that all thirty levels are solvable
in principle (every board has room for its own target).

## Design notes

- **The simulation sub-steps.** Fast-forward moves the head five times faster, so
  `update` slices the frame into ≤0.2-tile steps. Without that, a slow frame would let
  the head tunnel through a tile and skip its leak check.
- **Ooze fills from the end it entered.** Each cell records the side the flow arrived
  from, and the renderer traces that channel's path starting there, then reveals it with
  a dash offset. Fill direction is otherwise wrong half the time on elbows.
- **Storage always throws somewhere.** Sandboxed origins raise on `localStorage` access
  rather than returning empty, so every read and write is wrapped and the game runs fine
  with no persistence at all.
