fuzzyworld

tower / files / test / farms.test.ts

farms.test.ts

6.5 KB · history · blame

1import { readFileSync } from 'node:fs'
2import { describe, expect, it } from 'vitest'
3import { CIRCUMFERENCE_M } from '../src/canon/constants.js'
4import {
5 BLOCKS_AROUND, BLOCK_A_M, CROPS, GROW_AROUND, GROW_A_M, GROW_M, fieldAt, fieldLight, growFarm, pickCrop,
6} from '../src/terrain/farms.js'
7import { compileWorld } from '../src/terrain/compile.js'
8import { prepare } from '../src/terrain/field.js'
9import { parseWorld } from '../src/world/schema.js'
10
11const world = parseWorld(JSON.parse(readFileSync('content/world.json', 'utf8')))
12
13/** The fields met on a grid over a square of ground, by the point each was laid out from. */
14function fieldsIn(z0: number, a0: number, sideM: number, stepM: number): Map<string, number> {
15 const seen = new Map<string, number>()
16 for (let z = z0; z < z0 + sideM; z += stepM) {
17 for (let a = a0; a < a0 + sideM; a += stepM) {
18 const f = fieldAt(z, a)
19 if (f.track) continue
20 const k = `${f.centreZ.toFixed(1)},${f.centreA.toFixed(1)}`
21 seen.set(k, (seen.get(k) ?? 0) + 1)
22 }
23 }
24 return seen
25}
26
27describe('the fields are laid out as farmland grows, not on a grid (D-097)', () => {
28 it('closes the ring on a whole number of blocks, with no seam', () => {
29 expect(BLOCKS_AROUND * BLOCK_A_M).toBeCloseTo(CIRCUMFERENCE_M, 6)
30 expect(fieldAt(467_123, 2_345)).toEqual(fieldAt(467_123, 2_345))
31 expect(fieldAt(467_123, 2_345)).toEqual(fieldAt(467_123, 2_345 + CIRCUMFERENCE_M))
32 expect(fieldAt(467_123, -100)).toEqual(fieldAt(467_123, CIRCUMFERENCE_M - 100))
33 })
34
35 it('makes fields of many sizes, not one', () => {
36 const sizes = [...fieldsIn(460_000, 100_000, 6000, 40).values()].sort((x, y) => x - y)
37 expect(sizes.length).toBeGreaterThan(60)
38 const small = sizes[Math.floor(sizes.length * 0.2)]!, large = sizes[Math.floor(sizes.length * 0.8)]!
39 expect(large / small).toBeGreaterThan(2)
40 })
41
42 it('runs tracks along the blocks and no further', () => {
43 let tracks = 0
44 for (let i = 0; i < 4000; i++) if (fieldAt(460_000 + (i % 80) * 37, 100_000 + Math.floor(i / 80) * 41).track) tracks++
45 expect(tracks / 4000).toBeGreaterThan(0.001)
46 expect(tracks / 4000).toBeLessThan(0.03)
47 })
48
49 it('plants every crop, flowers most', () => {
50 const seen = new Map<number, number>()
51 for (let i = 0; i < 6000; i++) {
52 const f = fieldAt(400_000 + (i % 80) * 230, Math.floor(i / 80) * 290)
53 seen.set(f.crop, (seen.get(f.crop) ?? 0) + 1)
54 }
55 expect(seen.size).toBe(CROPS.length)
56 const flowers = [...seen].filter(([c]) => c <= 10).reduce((s, [, n]) => s + n, 0)
57 expect(flowers / 6000).toBeGreaterThan(0.6)
58 expect(pickCrop(0)).toBe(0)
59 expect(pickCrop(0.9999)).toBe(CROPS.length - 1)
60 })
61})
62
63describe('a farm grows over the land (D-097)', () => {
64 const patch = GROW_M * GROW_A_M
65 const all = (): boolean => true
66
67 it('takes the ground it is given, in one piece', () => {
68 const nodes = growFarm(300_000, 50_000, 28e6, 5, all)
69 expect(nodes.length * patch).toBeGreaterThan(27.5e6)
70 expect(nodes.length * patch).toBeLessThan(28.5e6)
71 const set = new Set(nodes)
72 const reached = new Set([nodes[0]!])
73 const queue = [nodes[0]!]
74 while (queue.length) {
75 const n = queue.pop()!
76 const iz = Math.floor(n / GROW_AROUND), ia = n % GROW_AROUND
77 for (const m of [n + GROW_AROUND, n - GROW_AROUND, iz * GROW_AROUND + ((ia + 1) % GROW_AROUND), iz * GROW_AROUND + ((ia + GROW_AROUND - 1) % GROW_AROUND)]) {
78 if (set.has(m) && !reached.has(m)) { reached.add(m); queue.push(m) }
79 }
80 }
81 expect(reached.size).toBe(nodes.length)
82 })
83
84 it('grows ragged, with lobes and bays — more edge than a disc of the same size', () => {
85 const nodes = new Set(growFarm(300_000, 50_000, 28e6, 5, all))
86 let edge = 0
87 for (const n of nodes) {
88 const iz = Math.floor(n / GROW_AROUND), ia = n % GROW_AROUND
89 for (const m of [n + GROW_AROUND, n - GROW_AROUND, iz * GROW_AROUND + ia + 1, iz * GROW_AROUND + ia - 1]) if (!nodes.has(m)) edge++
90 }
91 const disc = 2 * Math.sqrt(Math.PI * nodes.size) * (4 / Math.PI)
92 expect(edge).toBeGreaterThan(disc * 1.3)
93 })
94
95 it('never takes ground that does not fit, and goes round it', () => {
96 const river = (z: number): boolean => Math.abs(z - 300_600) > 300
97 const nodes = growFarm(300_000, 50_000, 28e6, 5, (z) => river(z))
98 for (const n of nodes) expect(river(Math.floor(n / GROW_AROUND) * GROW_M)).toBe(true)
99 expect(nodes.length * patch).toBeGreaterThan(27.5e6)
100 })
101})
102
103describe('each crop gets its own day (D-095)', () => {
104 // An equinox day: sunrise 07:00, sunset 19:00.
105 const rise = 7, set = 19
106
107 it('lights a long-day crop on into the night, and then gives it darkness', () => {
108 expect(fieldLight(18, 12, rise, set)).toBe('on')
109 expect(fieldLight(18, 23.5, rise, set)).toBe('on')
110 expect(fieldLight(18, 1.5, rise, set)).toBe('night')
111 })
112
113 it('takes a short-day crop down to the rod\'s dusk before the sun sets', () => {
114 expect(fieldLight(12, 19.5, rise, 20)).toBe('dim')
115 expect(fieldLight(12, 21, rise, 20)).toBe('night')
116 })
117
118 it('gives every crop at least six hours of darkness', () => {
119 for (const c of CROPS) expect(24 - c.dayH).toBeGreaterThanOrEqual(6)
120 })
121})
122
123describe('the farms stand round the world (D-095, D-096, D-097)', () => {
124 const cw = compileWorld(world)
125 const rt = prepare(cw)
126 const farms = world.features.filter((f) => f.kind === 'farmland')
127
128 it('are nine, seeded round the ring and down the axis, grown to 250 km² between them', () => {
129 expect(farms).toHaveLength(9)
130 const thetas = farms.map((f) => (f.geometry.type === 'point' ? f.geometry.theta : NaN))
131 const zs = farms.map((f) => (f.geometry.type === 'point' ? f.geometry.z : NaN))
132 expect(Math.max(...thetas) - Math.min(...thetas)).toBeGreaterThan(4)
133 expect(Math.max(...zs) - Math.min(...zs)).toBeGreaterThan(300_000)
134 const area = cw.farms!.reduce((s, f) => s + f.nodes.length, 0) * GROW_M * GROW_A_M / 1e6
135 expect(area).toBeGreaterThan(240)
136 expect(area).toBeLessThan(260)
137 })
138
139 it('is farmland about each seed, cleared of forest, and none far from them', () => {
140 for (const f of farms) {
141 if (f.geometry.type !== 'point') continue
142 let farmed = 0
143 for (let i = -2; i <= 2; i++) for (let j = -2; j <= 2; j++) {
144 const s = rt.sample(f.geometry.theta + (j * 400) / 50_000, f.geometry.z + i * 400, 20)
145 if (s.farm > 0.5) { farmed++; expect(s.stems, f.name).toBeLessThan(1) }
146 }
147 expect(farmed, f.name).toBeGreaterThan(12)
148 }
149 expect(rt.sample(0, 440_000, 20).farm).toBe(0)
150 })
151}, 120_000)