fuzzyworld

nordic-skating / mistral-medium-35-littlejs / files / test / skater.test.ts

Branch mistral-medium-35-littlejs · default branch · all branches

skater.test.ts

Blame: the commit that last changed each line · file · history

8b9467ca 2026-10-09 Pavel Safronov1import { describe, expect, it } from 'vitest'
1cb1fefc 2026-10-09 Pavel Safronov2import { BREAKING_THICKNESS } from '../src/ice/strength.js'
8b9467ca 2026-10-09 Pavel Safronov3import {
c2870e67 2026-10-10 Pavel Safronov4 ISLET_MARGIN, MAX_REACH, MAX_STRIDE, MAX_TURN, MIN_STRIDE, SHORE_MARGIN, SPEED, UNDER_SECONDS,
1ee103b4 2026-10-11 Pavel Safronov5 PACES, PACE_NAMES, RULES, type Skater, createSkater, stepSkater, strideSeconds,
8b9467ca 2026-10-09 Pavel Safronov6} from '../src/skater.js'
4f816cf0 2026-10-10 Pavel Safronov7import { type Lake, createLake, mulberry32 } from '../src/world/lake.js'
8b9467ca 2026-10-09 Pavel Safronov8
9const DT = 1 / 60
10const lake = createLake(1)
c2870e67 2026-10-10 Pavel Safronov11/** An autopilot that skates a straight line where it is put, for trying what is not about its carving. */
12const STRAIGHT = { falling: false, carve: false }
8b9467ca 2026-10-09 Pavel Safronov13
c2870e67 2026-10-10 Pavel Safronov14function run(k: Skater, steer: number, seconds: number, rules = STRAIGHT, on = lake): void {
8b9467ca 2026-10-09 Pavel Safronov15 const steps = Math.round(seconds / DT)
c2870e67 2026-10-10 Pavel Safronov16 for (let i = 0; i < steps; i++) stepSkater(k, on, steer, DT, rules)
8b9467ca 2026-10-09 Pavel Safronov17}
a8fd3f13 2026-10-10 Pavel Safronov18/** How far the skater is from the middle of their channel, metres; positive is right. */
19const off = (k: Skater): number => k.x - k.channel.centre(k.s)
4f816cf0 2026-10-10 Pavel Safronov20/** How far off the line of their channel they point, radians, between a half turn either way. */
21const out = (k: Skater): number => {
22 const angle = k.heading - Math.atan(k.channel.slope(k.s))
23 return angle - 2 * Math.PI * Math.round(angle / (2 * Math.PI))
24}
25/** How much ice there is between the skater and the nearest islet's edge, metres. */
26const clearOfIslets = (k: Skater): number =>
27 Math.min(Infinity, ...k.channel.islets(k.s - 30, k.s + 30).map((i) => Math.hypot(k.x - i.x, k.s - i.s) - i.radius))
26ace8f5 2026-10-10 Pavel Safronov28/** How much water there is between the skater and the island in their channel, metres; all there could be where there is none. */
29const clearOfIsland = (k: Skater): number => (k.channel.island(k.s) > 0 ? Math.abs(off(k)) - k.channel.island(k.s) : Infinity)
30/** Where the skater's channel next has an island at its widest, from `from` on. */
31function nextIsland(from: number): number {
32 let channel = lake.start
33 let s = from
34 for (; ; s += 5) {
35 while (channel.end.s <= s) channel = channel.end.preferred
36 if (channel.island(s) > 5 && channel.island(s + 5) < channel.island(s)) return s
37 }
38}
39/** The channel a skater who does nothing is in at `s`. */
40function channelAtIsland(s: number): Skater['channel'] {
41 let channel = lake.start
42 while (channel.end.s <= s) channel = channel.end.preferred
43 return channel
44}
4f816cf0 2026-10-10 Pavel Safronov45/** Someone steering this way and that, and sometimes not at all: the same someone every time. */
46function fidget(seed: number): (t: number) => number {
47 const random = mulberry32(seed)
48 let until = 0
49 let steer = 0
50 return (t) => {
51 if (t >= until) {
52 until = t + 0.5 + 4 * random()
53 steer = random() < 0.3 ? 0 : random() * 2 - 1
54 }
55 return steer
56 }
57}
a8fd3f13 2026-10-10 Pavel Safronov58/** Whether there is nothing but the skater's own channel to either side of them. */
59const alone = (k: Skater): boolean => !k.channel.birth || k.s > k.channel.birth.tip
60// Channels are compared with `===`, never handed to `expect`: a channel leads to its fork
61// and the fork to more channels without end, and a failure that tried to print one would
62// follow them until the stack ran out.
8b9467ca 2026-10-09 Pavel Safronov63
64describe('skater', () => {
a8fd3f13 2026-10-10 Pavel Safronov65 it('left alone, skates the lake for as long as you like, through every fork', () => {
1cb1fefc 2026-10-09 Pavel Safronov66 const k = createSkater(lake)
67 let forks = 0
a8fd3f13 2026-10-10 Pavel Safronov68 let slowest = Infinity
69 let fastest = 0
70 let widest = 0
26ace8f5 2026-10-10 Pavel Safronov71 const islands = new Set<number>()
a8fd3f13 2026-10-10 Pavel Safronov72 const start = k.s
8b9467ca 2026-10-09 Pavel Safronov73 for (let t = 0; t < 3600; t += DT) {
74 const before = k.s
1cb1fefc 2026-10-09 Pavel Safronov75 const channel = k.channel
8b9467ca 2026-10-09 Pavel Safronov76 stepSkater(k, lake, 0, DT)
77 expect(k.s).toBeGreaterThan(before)
1cb1fefc 2026-10-09 Pavel Safronov78 expect(k.under).toBe(0)
a8fd3f13 2026-10-10 Pavel Safronov79 // On the ice, always: never on the shore side of the two metres kept clear of it.
80 if (alone(k)) expect(Math.abs(off(k))).toBeLessThan(k.channel.halfWidth(k.s) - 0.5)
26ace8f5 2026-10-10 Pavel Safronov81 // Round every islet, with room to spare, and down one side of every island.
4f816cf0 2026-10-10 Pavel Safronov82 expect(clearOfIslets(k)).toBeGreaterThan(ISLET_MARGIN)
26ace8f5 2026-10-10 Pavel Safronov83 expect(clearOfIsland(k)).toBeGreaterThan(SHORE_MARGIN / 2)
84 if (k.channel.island(k.s) > 0) islands.add(Math.sign(off(k)))
1cb1fefc 2026-10-09 Pavel Safronov85 if (k.channel !== channel) {
c2870e67 2026-10-10 Pavel Safronov86 // One or other way at every fork, whichever the skater was over towards; or, just
87 // past one, across to its other channel while there is still open ice between.
88 const taken = k.channel === channel.end.left || k.channel === channel.end.right
89 expect(taken || k.channel.birth === channel.birth).toBe(true)
90 if (taken) forks++
1cb1fefc 2026-10-09 Pavel Safronov91 }
a8fd3f13 2026-10-10 Pavel Safronov92 slowest = Math.min(slowest, k.speed)
93 fastest = Math.max(fastest, k.speed)
94 widest = Math.max(widest, Math.abs(off(k)))
8b9467ca 2026-10-09 Pavel Safronov95 }
1cb1fefc 2026-10-09 Pavel Safronov96 expect(forks).toBeGreaterThan(5)
26ace8f5 2026-10-10 Pavel Safronov97 // Some islands passed on the left and some on the right, as each prefers.
98 expect([...islands].sort()).toEqual([-1, 1])
a8fd3f13 2026-10-10 Pavel Safronov99 // It carves: well off the middle at times, where there is the room.
100 expect(widest).toBeGreaterThan(4)
101 // The speed breathes with the stride, around its cruising speed and never far from it.
102 expect((k.s - start) / 3600).toBeGreaterThan(SPEED * 0.9)
103 expect((k.s - start) / 3600).toBeLessThan(SPEED * 1.1)
104 expect(slowest).toBeGreaterThan(SPEED * 0.8)
105 expect(fastest).toBeLessThan(SPEED * 1.2)
106 expect(fastest - slowest).toBeGreaterThan(1)
8b9467ca 2026-10-09 Pavel Safronov107 })
108
4f816cf0 2026-10-10 Pavel Safronov109 it('moves smoothly, however it is steered: no jumps, no jerks', () => {
110 for (const seed of [1, 2, 3]) {
a8fd3f13 2026-10-10 Pavel Safronov111 const k = createSkater(lake)
4f816cf0 2026-10-10 Pavel Safronov112 const steer = fidget(seed)
113 for (let t = 0; t < 600; t += DT) {
114 const { x, s, heading, speed } = k
115 stepSkater(k, lake, steer(t), DT)
116 expect(Math.hypot(k.x - x, k.s - s)).toBeLessThan(0.25)
a8fd3f13 2026-10-10 Pavel Safronov117 expect(Math.abs(k.heading - heading)).toBeLessThan(0.05)
118 expect(Math.abs(k.speed - speed)).toBeLessThan(0.05)
119 expect(Math.abs(k.turn)).toBeLessThanOrEqual(2 * MAX_TURN)
120 }
121 }
122 })
123
124 it('steers by turning, with a moment to lean into it', () => {
1cb1fefc 2026-10-09 Pavel Safronov125 const k = createSkater(lake)
a8fd3f13 2026-10-10 Pavel Safronov126 run(k, 1, 0.1)
127 // A tenth of a second in, the turn has barely begun.
128 expect(k.turn).toBeGreaterThan(0)
129 expect(k.turn).toBeLessThan(MAX_TURN / 2)
8b9467ca 2026-10-09 Pavel Safronov130 run(k, 1, 1)
a8fd3f13 2026-10-10 Pavel Safronov131 expect(k.turn).toBeCloseTo(MAX_TURN, 1)
132 expect(out(k)).toBeGreaterThan(0.2)
133 expect(off(k)).toBeGreaterThan(0.5)
134 run(k, -1, 3)
135 expect(out(k)).toBeLessThan(0)
8b9467ca 2026-10-09 Pavel Safronov136 })
137
4f816cf0 2026-10-10 Pavel Safronov138 it('cannot be steered into a shore or onto an islet, however wide or narrow the lake', () => {
139 for (const seed of [4, 5, 6]) {
1cb1fefc 2026-10-09 Pavel Safronov140 const k = createSkater(lake)
4f816cf0 2026-10-10 Pavel Safronov141 const steer = fidget(seed)
142 let nearest = Infinity
143 for (let t = 0; t < 1200; t += DT) {
c2870e67 2026-10-10 Pavel Safronov144 stepSkater(k, lake, steer(t), DT, STRAIGHT)
a8fd3f13 2026-10-10 Pavel Safronov145 if (alone(k)) {
1ee103b4 2026-10-11 Pavel Safronov146 expect(Math.abs(off(k))).toBeLessThan(Math.min(MAX_REACH + SHORE_MARGIN, k.channel.halfWidth(k.s)) - 0.3)
1cb1fefc 2026-10-09 Pavel Safronov147 }
4f816cf0 2026-10-10 Pavel Safronov148 nearest = Math.min(nearest, clearOfIslets(k))
26ace8f5 2026-10-10 Pavel Safronov149 expect(clearOfIsland(k)).toBeGreaterThan(0.5)
4f816cf0 2026-10-10 Pavel Safronov150 // Never on one. (Steered straight at one they come inside its margin for a moment.)
151 expect(nearest).toBeGreaterThan(0.2)
8b9467ca 2026-10-09 Pavel Safronov152 }
4f816cf0 2026-10-10 Pavel Safronov153 // And they did get in among the islets: this was not passed by keeping away.
154 expect(nearest).toBeLessThan(15)
155 }
156 })
157
158 it('held hard over, turns right round, and left alone turns back down the lake', () => {
159 // Out at sea, where there is the room to.
160 const k = createSkater(lake, 3400)
161 expect(k.channel.halfWidth(k.s)).toBeGreaterThan(100)
162 let furthestBack = 0
163 const start = k.heading
164 for (let t = 0; t < (2 * Math.PI) / MAX_TURN + 3; t += DT) {
165 const before = k.s
c2870e67 2026-10-10 Pavel Safronov166 stepSkater(k, lake, 1, DT, STRAIGHT)
4f816cf0 2026-10-10 Pavel Safronov167 furthestBack = Math.min(furthestBack, k.s - before)
8b9467ca 2026-10-09 Pavel Safronov168 }
4f816cf0 2026-10-10 Pavel Safronov169 // A whole circle, part of it back up the lake.
170 expect(k.heading - start).toBeGreaterThan(2 * Math.PI * 0.9)
171 expect(furthestBack).toBeLessThan(-0.05)
172 // Let go part way round the next one, facing back the way they came.
c2870e67 2026-10-10 Pavel Safronov173 for (let i = 0; i < 1200 && Math.cos(out(k)) > -0.9; i++) stepSkater(k, lake, 1, DT, STRAIGHT)
4f816cf0 2026-10-10 Pavel Safronov174 expect(Math.cos(out(k))).toBeLessThan(-0.85)
175 run(k, 0, 25)
176 expect(Math.abs(out(k))).toBeLessThan(0.1)
177 const then = k.s
178 run(k, 0, 5)
179 expect(k.s - then).toBeGreaterThan(SPEED * 5 * 0.8)
8b9467ca 2026-10-09 Pavel Safronov180 })
181
c2870e67 2026-10-10 Pavel Safronov182 it('down the middle and left alone, takes the way a fork prefers', () => {
183 const fork = lake.start.end
184 const k = createSkater(lake, fork.s - 100)
185 while (k.s < fork.tip + 50) stepSkater(k, lake, 0, DT, STRAIGHT)
186 expect(k.channel === fork.preferred).toBe(true)
187 })
188
1cb1fefc 2026-10-09 Pavel Safronov189 it('takes the left of a fork when steered left and the right when steered right', () => {
190 const fork = lake.start.end
191 for (const [steer, wanted] of [[-1, fork.left], [1, fork.right]] as const) {
192 const k = createSkater(lake, fork.s - 30)
4f816cf0 2026-10-10 Pavel Safronov193 // A nudge that way and no more: held over, it would be a circle.
194 run(k, steer, 1.2)
c2870e67 2026-10-10 Pavel Safronov195 while (k.s < fork.tip + 50) stepSkater(k, lake, 0, DT, STRAIGHT)
a8fd3f13 2026-10-10 Pavel Safronov196 expect(k.channel === wanted).toBe(true)
c2870e67 2026-10-10 Pavel Safronov197 // And from there on left alone, stays in it.
1cb1fefc 2026-10-09 Pavel Safronov198 run(k, 0, 60)
a8fd3f13 2026-10-10 Pavel Safronov199 expect(k.channel === wanted).toBe(true)
1cb1fefc 2026-10-09 Pavel Safronov200 }
201 })
202
4f816cf0 2026-10-10 Pavel Safronov203 it('can change its mind at a fork until the island begins, and then runs along its shore', () => {
1cb1fefc 2026-10-09 Pavel Safronov204 const fork = lake.start.end
205 const other = fork.preferred === fork.left ? fork.right : fork.left
206 const towards = other === fork.right ? 1 : -1
207 const k = createSkater(lake, fork.s - 1)
208 run(k, 0, 1)
a8fd3f13 2026-10-10 Pavel Safronov209 expect(k.channel === fork.preferred).toBe(true)
4f816cf0 2026-10-10 Pavel Safronov210 run(k, towards, 1.2)
c2870e67 2026-10-10 Pavel Safronov211 while (k.s < fork.tip + 5) stepSkater(k, lake, 0, DT, STRAIGHT)
4f816cf0 2026-10-10 Pavel Safronov212 expect(k.channel === other).toBe(true)
213 // Past the island's tip, a nudge back towards it does not cross it.
214 run(k, -towards, 1.5)
215 run(k, 0, 20)
a8fd3f13 2026-10-10 Pavel Safronov216 expect(k.channel === other).toBe(true)
1cb1fefc 2026-10-09 Pavel Safronov217 })
218
26ace8f5 2026-10-10 Pavel Safronov219 it('passes an island on the side it was steered towards on the way up to it', () => {
220 const middle = nextIsland(2000)
221 for (const steer of [-1, 1]) {
222 // From 150 m short of the island's near end: a touch, and then nothing.
223 let start = middle
224 while (channelAtIsland(start).island(start) > 0) start -= 5
225 const k = createSkater(lake, start - 100)
226 run(k, steer, 0.6)
c2870e67 2026-10-10 Pavel Safronov227 while (k.s < middle) stepSkater(k, lake, 0, DT, STRAIGHT)
26ace8f5 2026-10-10 Pavel Safronov228 expect(Math.sign(off(k))).toBe(steer)
229 expect(Math.abs(off(k))).toBeGreaterThan(k.channel.island(k.s) + SHORE_MARGIN)
230 }
231 })
232
c2870e67 2026-10-10 Pavel Safronov233 it('left alone after being steered, goes on the way it was going, and does not go back', () => {
234 // Out at sea, where there is the room. The autopilot has been carving for a minute.
235 const k = createSkater(lake, 3300)
236 run(k, 0, 60, RULES)
237 const before = off(k)
238 // Steered right for a couple of seconds, and let go.
239 run(k, 1, 2, RULES)
240 const letGo = off(k)
241 expect(out(k)).toBeGreaterThan(0.3)
242 expect(letGo).toBeGreaterThan(before + 2)
243 // It does not fight: half a second on it is still heading right, and it never turns
244 // back harder than a gentle carve does.
245 let hardest = 0
246 for (let i = 0; i < Math.round(4 / DT); i++) {
247 stepSkater(k, lake, 0, DT, RULES)
248 if (i === Math.round(0.5 / DT)) expect(out(k)).toBeGreaterThan(0.2)
249 hardest = Math.min(hardest, k.turn)
250 }
251 expect(hardest).toBeGreaterThan(-0.32)
252 // Four seconds on it has gone further right still.
253 expect(off(k)).toBeGreaterThan(letGo + 4)
254 // And for the next two minutes it carves about where it was sent, not where it had been.
255 let sum = 0
256 let count = 0
257 for (let t = 0; t < 120; t += DT) {
258 stepSkater(k, lake, 0, DT, RULES)
259 sum += off(k)
260 count++
261 }
262 expect(sum / count).toBeGreaterThan(letGo - 3)
263 expect(sum / count - before).toBeGreaterThan(5)
264 })
265
266 it('with carving off, stays on the line it was steered to', () => {
267 const k = createSkater(lake, 3300)
a8fd3f13 2026-10-10 Pavel Safronov268 run(k, 1, 1.5)
c2870e67 2026-10-10 Pavel Safronov269 run(k, 0, 15)
270 const settled = off(k)
271 expect(settled).toBeGreaterThan(3)
272 run(k, 0, 30)
273 expect(off(k)).toBeCloseTo(settled, 0)
a8fd3f13 2026-10-10 Pavel Safronov274 expect(Math.abs(out(k))).toBeLessThan(0.02)
8b9467ca 2026-10-09 Pavel Safronov275 })
276
9ff4c3fc 2026-10-09 Pavel Safronov277 it('takes long strides of varied length, the same ones every time', () => {
278 const lengths = Array.from({ length: 200 }, (_, n) => strideSeconds(n))
279 for (const [n, seconds] of lengths.entries()) {
280 expect(seconds).toBeGreaterThanOrEqual(MIN_STRIDE)
281 expect(seconds).toBeLessThanOrEqual(MAX_STRIDE)
282 expect(strideSeconds(n)).toBe(seconds)
283 }
284 // Varied: they fill the range rather than sitting at one length.
285 expect(Math.max(...lengths) - Math.min(...lengths)).toBeGreaterThan(0.8 * (MAX_STRIDE - MIN_STRIDE))
286 // And the skater takes them at that pace: the first three strides, to the frame.
1cb1fefc 2026-10-09 Pavel Safronov287 const k = createSkater(lake)
9ff4c3fc 2026-10-09 Pavel Safronov288 run(k, 0, lengths[0]! + lengths[1]! + lengths[2]! + 2 * DT)
289 expect(Math.floor(k.stride)).toBe(3)
290 })
291
a8fd3f13 2026-10-10 Pavel Safronov292 it('speeds up on the push and slows through the glide', () => {
293 const k = createSkater(lake)
294 const before = k.speed
295 run(k, 0, 0.5)
296 expect(k.speed).toBeGreaterThan(before + 0.5)
297 const pushed = k.speed
298 run(k, 0, strideSeconds(0) - 0.5 - 2 * DT)
299 expect(k.speed).toBeLessThan(pushed - 0.3)
300 })
301
8bb08e4a 2026-10-10 Pavel Safronov302 it('is slowed by snow lying on the ice, and picks up again off it', () => {
303 const snowy: Lake = { ...lake, snow: () => 1 }
304 const k = createSkater(lake, 3300)
305 run(k, 0, 40, STRAIGHT, snowy)
306 // About half speed in the middle of a drift, and still moving.
307 expect(k.speed).toBeLessThan(SPEED * 0.65)
308 expect(k.speed).toBeGreaterThan(SPEED * 0.35)
309 run(k, 0, 40)
310 expect(k.speed).toBeGreaterThan(SPEED * 0.85)
311 })
312
1ee103b4 2026-10-11 Pavel Safronov313 it('keeps each pace: settles round its speed, in strides to match, and breathes by the same share', () => {
314 for (const pace of PACE_NAMES) {
315 const k = createSkater(lake, 3300)
316 k.pace = pace
317 run(k, 0, 40)
318 let sum = 0
319 let slowest = Infinity
320 let fastest = 0
321 const strides = k.stride
322 const steps = Math.round(120 / DT)
323 for (let i = 0; i < steps; i++) {
324 stepSkater(k, lake, 0, DT, STRAIGHT)
325 sum += k.speed
326 slowest = Math.min(slowest, k.speed)
327 fastest = Math.max(fastest, k.speed)
328 }
329 const wanted = PACES[pace].speed
330 expect(sum / steps / wanted).toBeGreaterThan(0.93)
331 expect(sum / steps / wanted).toBeLessThan(1.07)
332 // Between a tenth and a third of its speed between the slowest and fastest of a stride.
333 expect((fastest - slowest) / wanted).toBeGreaterThan(0.1)
334 expect((fastest - slowest) / wanted).toBeLessThan(0.35)
335 // Strides as long as the pace takes them: about 2.3 s at a cruise.
336 expect(120 / (k.stride - strides) / PACES[pace].strides).toBeCloseTo(2.3, 0)
337 }
338 })
339
340 it('works up to a faster pace and eases down to a slower one over a few strides', () => {
341 const k = createSkater(lake, 3300)
342 k.pace = 'olympic'
343 run(k, 0, 2)
344 // Not at once.
345 expect(k.speed).toBeLessThan(13)
346 run(k, 0, 25)
347 expect(k.speed).toBeGreaterThan(13)
348 k.pace = 'meander'
349 run(k, 0, 3)
350 expect(k.speed).toBeGreaterThan(5)
351 run(k, 0, 40)
352 expect(k.speed).toBeLessThan(4.5)
353 })
354
355 it('at a racing pace turns in a wider circle, and still keeps to the ice and clear of everything', () => {
356 const k = createSkater(lake)
357 k.pace = 'olympic'
358 const steer = fidget(11)
359 let hardest = 0
360 for (let t = 0; t < 1200; t += DT) {
361 stepSkater(k, lake, steer(t), DT, RULES)
362 // Never pressed sideways much harder than half a g, turning away from an edge excepted.
363 hardest = Math.max(hardest, Math.abs(k.turn) * k.speed)
364 if (alone(k)) expect(Math.abs(off(k))).toBeLessThan(Math.min(MAX_REACH + SHORE_MARGIN, k.channel.halfWidth(k.s)) - 0.3)
365 expect(clearOfIslets(k)).toBeGreaterThan(0)
366 expect(clearOfIsland(k)).toBeGreaterThan(0.3)
367 }
368 expect(hardest).toBeLessThan(11)
369 const auto = createSkater(lake)
370 auto.pace = 'olympic'
371 for (let t = 0; t < 1200; t += DT) {
372 stepSkater(auto, lake, 0, DT, RULES)
373 if (alone(auto)) expect(Math.abs(off(auto))).toBeLessThan(auto.channel.halfWidth(auto.s) - 0.5)
374 expect(clearOfIslets(auto)).toBeGreaterThan(0.5)
375 expect(clearOfIsland(auto)).toBeGreaterThan(0.5)
376 }
377 // Twenty minutes at that pace is the better part of eighteen kilometres.
378 expect(auto.s).toBeGreaterThan(15 * 1200 * 0.85)
379 })
380
8b9467ca 2026-10-09 Pavel Safronov381 it('skates the same far down the lake as at its start', () => {
1cb1fefc 2026-10-09 Pavel Safronov382 const k = createSkater(lake, 1e6)
8b9467ca 2026-10-09 Pavel Safronov383 run(k, 0, 10)
1cb1fefc 2026-10-09 Pavel Safronov384 expect(k.s - 1e6).toBeGreaterThan(SPEED * 10 * 0.9)
6216d5cb 2026-10-09 Pavel Safronov385 })
386
a8fd3f13 2026-10-10 Pavel Safronov387 it('does not go through the ice, however thin, unless the rules say it can', () => {
388 // A lake that would give way anywhere 5 m to the right of the middle.
c2870e67 2026-10-10 Pavel Safronov389 const thin: Lake = { ...lake, thickness: (x, s) => (x - lake.start.centre(s) > 5 ? BREAKING_THICKNESS / 2 : 0.05) }
a8fd3f13 2026-10-10 Pavel Safronov390 const safe = createSkater(lake)
c2870e67 2026-10-10 Pavel Safronov391 run(safe, 1, 20, STRAIGHT, thin)
a8fd3f13 2026-10-10 Pavel Safronov392 expect(off(safe)).toBeGreaterThan(5)
393 expect(safe.under).toBe(0)
394
1cb1fefc 2026-10-09 Pavel Safronov395 const k = createSkater(lake)
c2870e67 2026-10-10 Pavel Safronov396 while (k.under === 0) stepSkater(k, thin, 1, DT, { falling: true, carve: false })
a8fd3f13 2026-10-10 Pavel Safronov397 expect(off(k)).toBeGreaterThan(5)
6216d5cb 2026-10-09 Pavel Safronov398 const where = k.s
c2870e67 2026-10-10 Pavel Safronov399 for (let i = 0; i < Math.round(UNDER_SECONDS / DT) - 2; i++) stepSkater(k, thin, 1, DT, { falling: true, carve: false })
6216d5cb 2026-10-09 Pavel Safronov400 expect(k.under).toBeGreaterThan(0)
401 expect(k.s).toBe(where)
c2870e67 2026-10-10 Pavel Safronov402 for (let i = 0; i < 4; i++) stepSkater(k, thin, 0, DT, { falling: true, carve: false })
6216d5cb 2026-10-09 Pavel Safronov403 expect(k.under).toBe(0)
a8fd3f13 2026-10-10 Pavel Safronov404 expect(off(k)).toBeCloseTo(0, 1)
6216d5cb 2026-10-09 Pavel Safronov405 expect(k.s).toBeGreaterThan(where)
406 })
8b9467ca 2026-10-09 Pavel Safronov407})