fuzzyworld

basin / files / readme.md

readme.md

5.5 KB · history · blame

basin

A browser puzzle game about water basins — read a coastline and deduce where the rivers begin.

Built with Vite + vanilla TypeScript + Canvas2D; no framework, no backend. Every board is a seed, so a board is just a link.

How it works

Each board has a heightmap that falls to the sea at its ocean sides. Water released from a spring flows downhill, branching to every strictly-lower neighbour, and exits wherever it reaches an ocean edge. Every board is generated with a hidden set of springs; their combined flow lights up a target edge signature, shown as gold outlines on the shore — the land cells immediately inland of each wet ocean edge, rather than the ocean cells themselves.

Each of the four sides is independently rolled ocean (blue — water exits) or a mountain wall (high ground that blocks water). Walls make canyons: e.g. north + south walls with east + west oceans is a valley that drains sideways. Water can never leave through a wall.

The terrain can also be aged: an erosion amount simulates how much weathering happened before you arrived. At 0 the board is a pristine dome; turn it up and simulated hydraulic erosion carves grooves and channels, ragging the coastline and moving where water finally reaches the sea — so a heavily-aged board reads a visibly different edge signature from its pristine self. Erosion is self-limiting: if a particular seed would wash away too much standing land, generation automatically backs off the effective aging for that board so it never degenerates into an almost-all-water board.

Reach controls how far from the shore the hidden springs are drawn from: low values favour land just above the waterline, high values favour high ground near the peak, and the midpoint has no preference (the original behaviour). This only shapes which legible answer generation reaches for — it never affects the unambiguous-solution guarantee below.

How to play

Goal: find the positions of the hidden springs that reproduce that signature. Tap cells to explore the terrain — a wrong tap just previews its flow (and marks any edges it wrongly wets in red), while landing on a real spring locks it in (green, numbered). Find them all to solve the board.

Generation guarantees the puzzle is unambiguous: the hidden springs are the only set of that size whose combined flow produces the target signature, so the gold-edge hint always pins down a single answer (no redundant or interchangeable springs). Boards that can't be made unique fall back to the richest signature found, so a board is always produced.

Springs also never sit on a shore cell — the gold outline never doubles as a giveaway for one of the hidden positions. (Only an extreme combination, like a 5×5 board with 6 springs, can make this strictly impossible; generation falls back gracefully there rather than failing.)

Configuring & sharing boards

Every axis of a board is a control in the panel: size, spring count, which sides are walls, the aging (erosion) amount, reach, and the terrain seed. All of it is captured in a single shareable code — a 9-character token (e.g. B0B0bgTL9) that packs the six axes into 52 bits and base64url-encodes them, so the settings aren't legible at a glance (reversible obfuscation, not encryption). The code and the controls are two-way views of the same config — changing any control rewrites the code, and pasting a code sets every control. The reach axis grew the code by a character, so pre-reach 8-character codes no longer decode — a stale shared link just rolls a fresh board instead.

The code also lives in the URL as ?code=<code>, so a link reproduces the exact board. 🎲 Random rolls a whole new config; the 🎲 next to Terrain rerolls just the terrain while keeping the other settings.

Each axis also has a lock (🔓/🔒) toggle. Locking an axis pins it: 🎲 Random then rerolls every other setting while leaving the locked ones untouched — so you can, say, freeze the size and walls and keep rolling only the terrain and springs. Locks are a session convenience and aren't part of the shared code.

Develop

npm install
npm run dev      # Vite dev server
npm run build    # type-check + production build
npm test         # run the model tests (Vitest)

Deploy

.gitlab-ci.yml builds the app and publishes it to GitLab Pages on every push to the default branch. Vite is configured with base: '/basin/' and builds into public/ (the dir Pages serves), so the live site is https://apps.fuzzyworld.net/basin/.

Architecture

The model (typed arrays) is decoupled from the renderer so a tiled/WebGL renderer can replace Canvas2D for large boards later.

Source
1# basin
2
3A browser puzzle game about water basins — read a coastline and deduce where the
4rivers begin.
5
6- **▶ Play:** <https://apps.fuzzyworld.net/basin/>
7- **Source:** <https://code.fuzzyworld.net/basin/>
8
9Built with Vite + vanilla TypeScript + Canvas2D; no framework, no backend. Every
10board is a seed, so a board is just a link.
11
12## How it works
13
14Each board has a heightmap that falls to the sea at its **ocean** sides. Water
15released from a *spring* flows downhill, branching to every strictly-lower
16neighbour, and exits wherever it reaches an ocean edge. Every board is generated
17with a hidden set of springs; their combined flow lights up a **target edge
18signature**, shown as gold outlines on the *shore* — the land cells immediately
19inland of each wet ocean edge, rather than the ocean cells themselves.
20
21Each of the four sides is independently rolled **ocean** (blue — water exits) or
22a **mountain wall** (high ground that blocks water). Walls make canyons: e.g.
23north + south walls with east + west oceans is a valley that drains sideways.
24Water can never leave through a wall.
25
26The terrain can also be **aged**: an *erosion* amount simulates how much
27weathering happened before you arrived. At 0 the board is a pristine dome; turn
28it up and simulated hydraulic erosion carves grooves and channels, ragging the
29coastline and moving where water finally reaches the sea — so a heavily-aged
30board reads a visibly different edge signature from its pristine self. Erosion
31is self-limiting: if a particular seed would wash away too much standing land,
32generation automatically backs off the effective aging for that board so it
33never degenerates into an almost-all-water board.
34
35**Reach** controls how far from the shore the hidden springs are drawn from:
36low values favour land just above the waterline, high values favour high
37ground near the peak, and the midpoint has no preference (the original
38behaviour). This only shapes which legible answer generation reaches for — it
39never affects the unambiguous-solution guarantee below.
40
41## How to play
42
43**Goal:** find the positions of the hidden springs that reproduce that signature.
44Tap cells to explore the terrain — a wrong tap just previews its flow (and marks
45any edges it wrongly wets in red), while landing on a real spring **locks it in**
46(green, numbered). Find them all to solve the board.
47
48Generation guarantees the puzzle is **unambiguous**: the hidden springs are the
49*only* set of that size whose combined flow produces the target signature, so the
50gold-edge hint always pins down a single answer (no redundant or interchangeable
51springs). Boards that can't be made unique fall back to the richest signature
52found, so a board is always produced.
53
54Springs also never sit on a shore cell — the gold outline never doubles as a
55giveaway for one of the hidden positions. (Only an extreme combination, like a
565×5 board with 6 springs, can make this strictly impossible; generation falls
57back gracefully there rather than failing.)
58
59## Configuring & sharing boards
60
61Every axis of a board is a control in the panel: **size**, **spring count**, which
62sides are **walls**, the **aging** (erosion) amount, **reach**, and the **terrain
63seed**. All of it is captured in a single shareable **code** — a 9-character token
64(e.g. `B0B0bgTL9`) that packs the six axes into 52 bits and base64url-encodes
65them, so the settings aren't legible at a glance (reversible obfuscation, not
66encryption). The code and the controls are two-way views of the same config —
67changing any control rewrites the code, and pasting a code sets every control.
68The reach axis grew the code by a character, so pre-reach 8-character codes no
69longer decode — a stale shared link just rolls a fresh board instead.
70
71The code also lives in the URL as `?code=<code>`, so a link reproduces the exact
72board. **🎲 Random** rolls a whole new config; the 🎲 next to Terrain rerolls just
73the terrain while keeping the other settings.
74
75Each axis also has a **lock** (🔓/🔒) toggle. Locking an axis pins it: **🎲 Random**
76then rerolls every *other* setting while leaving the locked ones untouched — so you
77can, say, freeze the size and walls and keep rolling only the terrain and springs.
78Locks are a session convenience and aren't part of the shared code.
79
80## Develop
81
82```bash
83npm install
84npm run dev # Vite dev server
85npm run build # type-check + production build
86npm test # run the model tests (Vitest)
87```
88
89## Deploy
90
91`.gitlab-ci.yml` builds the app and publishes it to **GitLab Pages** on every push
92to the default branch. Vite is configured with `base: '/basin/'` and builds into
93`public/` (the dir Pages serves), so the live site is
94<https://apps.fuzzyworld.net/basin/>.
95
96## Architecture
97
98The model (typed arrays) is decoupled from the renderer so a tiled/WebGL renderer
99can replace Canvas2D for large boards later.
100
101- `src/model/heightmap.ts` — terrain + per-side edge types (ocean/wall) +
102 droplet-based hydraulic **erosion** (the aging axis; skipped, and rng-free, at
103 0) + priority-flood depression filling (guarantees every cell drains to an
104 *ocean* edge, seeded only from ocean sides).
105- `src/model/flow.ts` — the branching watershed flood (single- and multi-spring).
106- `src/model/puzzle.ts` — the board `Config`, its shareable code (encode/decode),
107 config-driven puzzle generation (with an unambiguous-solution check), and the
108 solution check.
109- `src/render/renderer.ts` — Canvas2D board + puzzle overlay.
110- `src/game.ts` — puzzle state: exploration probe, locked springs, win detection.