fuzzyworld

basin / files / src / model / flow.test.ts

flow.test.ts

4.1 KB · history · blame

1import { describe, it, expect } from 'vitest';
2import { simulateFlow, simulateFlows } from './flow';
3import type { Edges, Heightmap } from './heightmap';
4import { ALL_OCEAN, generateDome } from './heightmap';
5import { mulberry32 } from '../rng';
6
7/** Build a heightmap from an explicit grid of rows. */
8function grid(rows: number[][], edges: Edges = ALL_OCEAN): Heightmap {
9 const height = rows.length;
10 const width = rows[0].length;
11 const data = new Float32Array(width * height);
12 for (let y = 0; y < height; y++) {
13 for (let x = 0; x < width; x++) data[y * width + x] = rows[y][x];
14 }
15 return { width, height, data, edges };
16}
17
18/** Collect wet cells as "x,y" strings for order-independent comparison. */
19function wetCells(hm: Heightmap, wet: Uint8Array): Set<string> {
20 const s = new Set<string>();
21 for (let y = 0; y < hm.height; y++) {
22 for (let x = 0; x < hm.width; x++) if (wet[y * hm.width + x]) s.add(`${x},${y}`);
23 }
24 return s;
25}
26
27describe('simulateFlow', () => {
28 it('flows downhill along a ramp and stops at the low edge', () => {
29 // Heights decrease left→right; a spring at the top of the ramp wets the row.
30 const hm = grid([
31 [5, 4, 3],
32 [5, 4, 3],
33 [5, 4, 3],
34 ]);
35 const { wet, outflow } = simulateFlow(hm, 0, 1);
36 expect(wetCells(hm, wet)).toEqual(new Set(['0,1', '1,1', '2,1']));
37 // Both ends sit on the border → both are outflow cells.
38 expect(new Set(outflow.map((c) => `${c.x},${c.y}`))).toEqual(new Set(['0,1', '2,1']));
39 });
40
41 it('is strictly decreasing: equal-height neighbours stay dry', () => {
42 // A single peak over a flat plateau. Water reaches the 4 edge-mids (all < 3)
43 // but cannot cross from one plateau cell to an equal-height corner.
44 const hm = grid([
45 [2, 2, 2],
46 [2, 3, 2],
47 [2, 2, 2],
48 ]);
49 const { wet } = simulateFlow(hm, 1, 1);
50 expect(wetCells(hm, wet)).toEqual(new Set(['1,1', '1,0', '0,1', '2,1', '1,2']));
51 });
52
53 it('does not flow at all from a local pit', () => {
54 const hm = grid([
55 [9, 9, 9],
56 [9, 0, 9],
57 [9, 9, 9],
58 ]);
59 const { wet, outflow } = simulateFlow(hm, 1, 1);
60 expect(wetCells(hm, wet)).toEqual(new Set(['1,1']));
61 expect(outflow).toHaveLength(0);
62 });
63
64 it('reports outflow as exactly the wet border cells', () => {
65 const hm = grid([
66 [5, 4, 3],
67 [5, 4, 3],
68 [5, 4, 3],
69 ]);
70 const { wet, outflow } = simulateFlow(hm, 0, 1);
71 const expected = new Set<string>();
72 for (let y = 0; y < hm.height; y++) {
73 for (let x = 0; x < hm.width; x++) {
74 const isEdge = x === 0 || y === 0 || x === hm.width - 1 || y === hm.height - 1;
75 if (isEdge && wet[y * hm.width + x]) expected.add(`${x},${y}`);
76 }
77 }
78 expect(new Set(outflow.map((c) => `${c.x},${c.y}`))).toEqual(expected);
79 });
80});
81
82describe('simulateFlows', () => {
83 it('unions the wet sets: wet = OR of each spring flooded alone', () => {
84 const hm = generateDome(10, 10, mulberry32(7));
85 const a = simulateFlow(hm, 3, 3);
86 const b = simulateFlow(hm, 6, 6);
87 const both = simulateFlows(hm, [
88 { x: 3, y: 3 },
89 { x: 6, y: 6 },
90 ]);
91 for (let i = 0; i < both.wet.length; i++) {
92 expect(both.wet[i]).toBe(a.wet[i] | b.wet[i]);
93 }
94 });
95
96 it('does not exit through a wall side (water pools against it)', () => {
97 // Heights fall toward the north row; north is a wall, south is ocean.
98 const hm = grid(
99 [
100 [3, 3, 3],
101 [4, 4, 4],
102 [5, 5, 5],
103 ],
104 { n: 'wall', e: 'ocean', s: 'ocean', w: 'ocean' },
105 );
106 const { wet, outflow } = simulateFlow(hm, 1, 2);
107 // Water reaches the north wall cell...
108 expect(wetCells(hm, wet)).toEqual(new Set(['1,2', '1,1', '1,0']));
109 // ...but only the south ocean cell (the spring itself) counts as outflow.
110 expect(new Set(outflow.map((c) => `${c.x},${c.y}`))).toEqual(new Set(['1,2']));
111 });
112
113 it('deduplicates coincident springs', () => {
114 const hm = grid([
115 [5, 4, 3],
116 [5, 4, 3],
117 [5, 4, 3],
118 ]);
119 const one = simulateFlow(hm, 0, 1);
120 const dup = simulateFlows(hm, [
121 { x: 0, y: 1 },
122 { x: 0, y: 1 },
123 ]);
124 expect(wetCells(hm, dup.wet)).toEqual(wetCells(hm, one.wet));
125 });
126});