[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft.
flo-bit.dev/shapecraft/
3.7 kB
123 lines
1import { describe, it, expect } from 'vitest'
2import { createRng } from '../src/core/rng'
3
4describe('createRng', () => {
5 it('is callable and returns floats in [0, 1)', () => {
6 const rng = createRng(1)
7 for (let i = 0; i < 100; i++) {
8 const v = rng()
9 expect(v).toBeGreaterThanOrEqual(0)
10 expect(v).toBeLessThan(1)
11 }
12 })
13
14 it('is deterministic for the same seed', () => {
15 const a = createRng(42)
16 const b = createRng(42)
17 const seqA = Array.from({ length: 10 }, () => a())
18 const seqB = Array.from({ length: 10 }, () => b())
19 expect(seqA).toEqual(seqB)
20 })
21
22 it('differs across seeds', () => {
23 const a = createRng(1)
24 const b = createRng(2)
25 expect(a()).not.toEqual(b())
26 })
27
28 it('accepts string seeds', () => {
29 const a = createRng('hello')
30 const b = createRng('hello')
31 expect(a()).toEqual(b())
32 })
33})
34
35describe('Rng helpers', () => {
36 it('int() is inclusive and within bounds', () => {
37 const rng = createRng(7)
38 const seen = new Set<number>()
39 for (let i = 0; i < 1000; i++) {
40 const v = rng.int(1, 6)
41 expect(Number.isInteger(v)).toBe(true)
42 expect(v).toBeGreaterThanOrEqual(1)
43 expect(v).toBeLessThanOrEqual(6)
44 seen.add(v)
45 }
46 // Should eventually hit both ends of the inclusive range
47 expect(seen.has(1)).toBe(true)
48 expect(seen.has(6)).toBe(true)
49 })
50
51 it('float(min, max) stays within bounds', () => {
52 const rng = createRng(3)
53 for (let i = 0; i < 100; i++) {
54 const v = rng.float(5, 10)
55 expect(v).toBeGreaterThanOrEqual(5)
56 expect(v).toBeLessThan(10)
57 }
58 })
59
60 it('range() passes through fixed values and resolves tuples', () => {
61 const rng = createRng(9)
62 expect(rng.range(2.5)).toBe(2.5)
63 const v = rng.range([0, 4])
64 expect(v).toBeGreaterThanOrEqual(0)
65 expect(v).toBeLessThanOrEqual(4)
66 const n = rng.range([0, 4], true)
67 expect(Number.isInteger(n)).toBe(true)
68 })
69
70 it('pick() returns an element of the array', () => {
71 const rng = createRng(11)
72 const arr = ['a', 'b', 'c']
73 for (let i = 0; i < 50; i++) {
74 expect(arr).toContain(rng.pick(arr))
75 }
76 })
77
78 it('seed() produces deterministic integer seeds', () => {
79 const a = createRng(5)
80 const b = createRng(5)
81 expect(a.seed()).toBe(b.seed())
82 expect(Number.isInteger(a.seed())).toBe(true)
83 })
84})
85
86describe('named streams', () => {
87 it('a stream is deterministic for the same name', () => {
88 const rootA = createRng(1)
89 const rootB = createRng(1)
90 const seqA = Array.from({ length: 5 }, () => rootA.stream('canopy')())
91 // Re-derive each time — same name + same root => same first value
92 expect(rootB.stream('canopy')()).toEqual(rootA.stream('canopy')())
93 expect(seqA.every((v) => v >= 0 && v < 1)).toBe(true)
94 })
95
96 it('different stream names are independent', () => {
97 const root = createRng(1)
98 expect(root.stream('a')()).not.toEqual(root.stream('b')())
99 })
100
101 it('consuming one stream does not perturb another (the key property)', () => {
102 // Baseline: read canopy without touching snow at all.
103 const root1 = createRng(99)
104 const canopy1 = root1.stream('canopy')
105 const baseline = [canopy1(), canopy1(), canopy1()]
106
107 // Now heavily consume an unrelated stream first, then read canopy.
108 const root2 = createRng(99)
109 const snow2 = root2.stream('snow')
110 for (let i = 0; i < 50; i++) snow2()
111 const canopy2 = root2.stream('canopy')
112 const after = [canopy2(), canopy2(), canopy2()]
113
114 expect(after).toEqual(baseline)
115 })
116
117 it('fork() yields independent anonymous streams', () => {
118 const root = createRng(1)
119 const f1 = root.fork()
120 const f2 = root.fork()
121 expect(f1()).not.toEqual(f2())
122 })
123})