simulate.gno
27.52 Kb · 1096 lines
1package physics
2
3import "math"
4
5// Outcomes.
6const (
7 Goal = 1
8 Save = 2
9 Miss = 3
10 Post = 4
11)
12
13// Woodwork parts.
14const (
15 LeftPost = 1
16 RightPost = 2
17 Crossbar = 3
18)
19
20// Shot is the taker's input in the browser engine's units.
21type Shot struct {
22 AimX, AimY float64 // m, point on the goal plane the swing heads for
23 ContactX, ContactY float64 // ball radii
24 Power float64 // 0..1
25 Mishit [24]byte
26 HasMishit bool
27}
28
29// Launch is the ball as it leaves the boot.
30type Launch struct {
31 Vel, Spin [3]float64
32 Speed float64
33 FuzzX, FuzzY float64 // contact error the mishit seed added, ball radii
34 ContactX, ContactY float64 // where the boot actually met the ball
35}
36
37// Contact is one keeper touch, decided at its first contact.
38type Contact struct {
39 Step int
40 Part int // index into PartNames
41 Catch bool
42 GaveWay bool
43}
44
45// Result is a decided kick.
46type Result struct {
47 Outcome int
48 DecidedStep int // the 1 kHz step it was decided in
49 DecidedAt float64 // s after contact
50 Launch Launch
51 Touch int // first part to touch the ball, -1 for none
52 TouchStep int
53 Woodwork int // first frame part it hit, 0 for none
54 Caught bool
55 Pounced bool // decided by the keeper pouncing on a loose ball
56 Contacts []Contact
57 Commits []Commit
58 Misread [4]float64 // the keeper's four normal draws (x, y, vx, jump)
59 // Where the ball's centre first reached the goal line (rounded to the
60 // millimetre, as the browser reports it), if it did.
61 LineCross bool
62 LineT, LineX, LineY float64
63 // Where it was when the whole ball was over the whole line, if it was.
64 Crossing bool
65 CrossT, CrossX, CrossY float64
66 EndX, EndY float64 // the ball when the kick was decided, m
67 Steps int
68 Trace uint32 // see Options.Trace
69}
70
71// Options are for tests and analysis.
72type Options struct {
73 NoKeeper bool // an empty goal
74 NoMisread bool // a keeper that reads perfectly
75 // Trace folds the state into Result.Trace every TraceEvery steps and at
76 // the deciding step, for comparing with the browser engine bit for bit.
77 Trace bool
78 TraceEvery int
79}
80
81type ball struct {
82 p, v, w vec
83 grounded bool
84 inGoal bool
85 held bool
86 sp float64 // speed at the last drag evaluation (see ballSpeed)
87}
88
89// engine holds the constants derived from a game's Params.
90type engine struct {
91 p Params
92 body [2]sideK
93 headX float64
94 headY float64
95 headR float64
96 headH float64
97 zeroZ float64
98 scale float64
99 maxReach float64
100 standHipY float64
101 netSideX float64
102 netRoofY float64
103 frame [3]capsule
104
105 // Per-game products the keeper's steps use, worked out once with the
106 // browser's operations.
107 shXL, shXR float64 // kShoulderX * side * S, left and right
108 shY float64 // kShoulderY * S
109 aimDZ, aimDZ2 float64 // 0.25 * S and its square: trackArms' forward offset
110 gloveReachS float64 // kGloveReach * S
111 misT1, misTd float64 // kMisreadT[1] and kMisreadT[1] - kMisreadT[0]
112 misX0, misXd float64 // lerpRange(kMisreadX, f) = misX0 + misXd*f
113 misY0, misYd float64
114}
115
116func newEngine(p Params) *engine {
117 e := &engine{p: p, scale: p.KeeperScale}
118 e.standHipY = kHipFactor * p.KeeperScale
119 e.initBody()
120 e.netSideX = p.GoalHalfWidth + postR
121 e.netRoofY = p.GoalHeight + postR
122 S := p.KeeperScale
123 e.shXL, e.shXR = kShoulderX*-1.0*S, kShoulderX*1.0*S
124 e.shY = kShoulderY * S
125 e.aimDZ = 0.25 * S
126 e.aimDZ2 = e.aimDZ * e.aimDZ
127 e.gloveReachS = kGloveReach * S
128 e.misT1, e.misTd = kMisreadT[1], kMisreadT[1]-kMisreadT[0]
129 e.misX0, e.misXd = kMisreadX[0], kMisreadX[1]-kMisreadX[0]
130 e.misY0, e.misYd = kMisreadY[0], kMisreadY[1]-kMisreadY[0]
131 // The most any part reaches, plus the ball and a centimetre: the
132 // thickest part's collision radius, slow-ball reaction and late hands.
133 e.maxReach = ballR + jsMax(jsMax(0.14*p.KeeperScale+kMarginTorso, 0.08*p.KeeperScale+kMarginLeg),
134 jsMax(kGloveR+kMarginGlove, 0.12*p.KeeperScale+kMarginHead)) + kReactReach + kHandLateMax + 0.01
135 px := p.GoalHalfWidth + postR
136 top := p.GoalHeight + postR
137 e.frame = [3]capsule{
138 {a: vec{-px, 0, goalZ}, b: vec{-px, top, goalZ}, r: postR, part: LeftPost},
139 {a: vec{px, 0, goalZ}, b: vec{px, top, goalZ}, r: postR, part: RightPost},
140 {a: vec{-px, top, goalZ}, b: vec{px, top, goalZ}, r: postR, part: Crossbar},
141 }
142 return e
143}
144
145// ---------------------------------------------------------------- the kick
146
147type disc struct{ x, y float64 }
148
149func clampDisc(x, y, max float64) disc {
150 r2 := x*x + y*y
151 if r2 <= max*max {
152 return disc{x, y}
153 }
154 s := max / fsqrt(r2)
155 return disc{x * s, y * s}
156}
157
158func mishitNormal(seed *[24]byte, offset int) float64 {
159 sum := 0.0
160 for i := 0; i < 6; i++ {
161 sum += float64(u16(seed[:], offset+2*i)) / 65535
162 }
163 return (sum - 3) * sqrt2
164}
165
166const misreadSalt = 0x6b656570 // "keep"
167
168func keeperMisread(seed *[24]byte) misread {
169 m := mulberry{a: fnvBytes(0x811c9dc5^misreadSalt, seed[:])}
170 normal := func() float64 {
171 sum := 0.0
172 for i := 0; i < 6; i++ {
173 sum += m.next()
174 }
175 return (sum - 3) * sqrt2
176 }
177 x := normal()
178 y := normal()
179 vx := normal()
180 jump := normal()
181 return misread{on: true, x: x, y: y, vx: vx, jump: jump}
182}
183
184type launch struct {
185 pos, vel, spin vec
186 speed float64
187 fuzz disc
188 contact disc
189}
190
191func (e *engine) computeLaunch(in *Shot) launch {
192 pos := vec{0, ballR, 0}
193 target := vec{clamp(in.AimX, -kickAimX, kickAimX), clamp(in.AimY, 0, kickAimY), goalZ}
194 f := norm(sub(target, pos))
195 right := norm(crossVec(f, upVec))
196 up := crossVec(right, f)
197
198 power := clamp(in.Power, 0, 1)
199 intended := clampDisc(in.ContactX, in.ContactY, e.p.MaxContact)
200 var fuzz disc
201 if in.HasMishit {
202 sigma := e.p.FuzzBase + e.p.FuzzPower*power
203 fuzz.x = mishitNormal(&in.Mishit, 0) * sigma
204 fuzz.y = mishitNormal(&in.Mishit, 12) * sigma
205 }
206 c := clampDisc(intended.x+fuzz.x, intended.y+fuzz.y, kickMaxStrike)
207 cx, cy := c.x, c.y
208 cz := fsqrt(1 - cx*cx - cy*cy)
209 n := add(add(scale(right, cx), scale(up, cy)), scale(f, -cz))
210
211 dir := norm(add(scale(f, 1-kickNormalMix), scale(n, -kickNormalMix)))
212 quality := cz
213 efficiency := 0.55 + 0.45*quality
214 speed := (e.p.BallMinSpeed + (e.p.BallMaxSpeed-e.p.BallMinSpeed)*power) * efficiency
215 vel := scale(dir, speed)
216
217 rc := scale(n, ballR)
218 dL := scale(crossVec(rc, scale(vel, ballM)), 1/ballI)
219 side := dot(dL, up)
220 backAxis := sub(dL, scale(up, side))
221 spin := add(scale(up, side*kickSideSpin), scale(backAxis, kickBackSpin))
222 sm := length(spin)
223 if sm > kickMaxSpin {
224 spin = scale(spin, kickMaxSpin/sm)
225 }
226 return launch{pos: pos, vel: vel, spin: spin, speed: speed, fuzz: fuzz, contact: c}
227}
228
229// ---------------------------------------------------------------- the ball
230
231func rollingSpin(u vec) vec { return vec{u.z / ballR, 0, -u.x / ballR} }
232
233func (e *engine) netAccel(b *ball) vec {
234 p, u := b.p, b.v
235 R := ballR
236 K := netKs / ballM
237 C := netCs / ballM
238 ax, ay, az := 0.0, 0.0, 0.0
239 touching := false
240 panel := func(pen, vn float64) float64 {
241 if pen <= 0 {
242 return 0
243 }
244 touching = true
245 k := K * netRecoil
246 if vn < 0 {
247 k = K
248 }
249 f := k*pen - C*vn
250 if f > 0 {
251 return f
252 }
253 return 0
254 }
255 az += panel(netBackZ-(p.z-R), u.z)
256 ay -= panel(p.y+R-e.netRoofY, -u.y)
257 sx := 1.0
258 if p.x > 0 {
259 sx = -1
260 }
261 ax += sx * panel(math.Abs(p.x)+R-e.netSideX, sx*u.x)
262 if touching {
263 ax -= netFriction * u.x
264 ay -= netFriction * u.y
265 az -= netFriction * u.z
266 }
267 return vec{ax, ay, az}
268}
269
270// ballSpeed is the ball's speed for drag: one Newton step from the speed of
271// the previous evaluation while the speed has changed by under half a per
272// cent, otherwise (the first step, after an impact) a square root.
273func ballSpeed(b *ball, u vec) float64 {
274 s2 := u.x*u.x + u.y*u.y + u.z*u.z
275 s := b.sp
276 d := s2 - s*s
277 if s > 0 && d <= 0.01*s2 && d >= -0.01*s2 {
278 return 0.5 * (s + s2/s)
279 }
280 return fsqrt(s2)
281}
282
283// heunBall is a free-flight step of dt (second-order Runge-Kutta), as the
284// browser's: ballSpeed and airAccel written out.
285func heunBall(b *ball, dt float64) {
286 ux, uy, uz := b.v.x, b.v.y, b.v.z
287 wx, wy, wz := b.w.x, b.w.y, b.w.z
288 s2 := ux*ux + uy*uy + uz*uz
289 sp := b.sp
290 if q := sp; !(q > 0) {
291 sp = fsqrt(s2)
292 } else if d, lim := s2-q*q, 0.01*s2; d > lim {
293 sp = fsqrt(s2)
294 } else if d < -lim {
295 sp = fsqrt(s2)
296 } else {
297 sp = 0.5 * (q + s2/q)
298 }
299 nk := -dragK * sp
300 a1x := nk*ux + (wy*uz-wz*uy)*magnusK
301 a1y := nk*uy - simG + (wz*ux-wx*uz)*magnusK
302 a1z := nk*uz + (wx*uy-wy*ux)*magnusK
303 vx, vy, vz := ux+a1x*dt, uy+a1y*dt, uz+a1z*dt
304 f := 1 - ballSpinDecay*dt
305 wx, wy, wz = wx*f, wy*f, wz*f
306 s2 = vx*vx + vy*vy + vz*vz
307 if q := sp; !(q > 0) {
308 sp = fsqrt(s2)
309 } else if d, lim := s2-q*q, 0.01*s2; d > lim {
310 sp = fsqrt(s2)
311 } else if d < -lim {
312 sp = fsqrt(s2)
313 } else {
314 sp = 0.5 * (q + s2/q)
315 }
316 b.sp = sp
317 nk = -dragK * sp
318 a2x := nk*vx + (wy*vz-wz*vy)*magnusK
319 a2y := nk*vy - simG + (wz*vx-wx*vz)*magnusK
320 a2z := nk*vz + (wx*vy-wy*vx)*magnusK
321 h := 0.5 * dt
322 p := &b.p
323 p.x += (ux + vx) * h
324 p.y += (uy + vy) * h
325 p.z += (uz + vz) * h
326 b.v = vec{ux + (a1x+a2x)*h, uy + (a1y+a2y)*h, uz + (a1z+a2z)*h}
327 b.w = vec{wx, wy, wz}
328}
329
330// integrateBall is the browser's semi-implicit Euler step, written out.
331// (x += e rounds exactly as x = x + e, and is cheaper in gno.)
332func (e *engine) integrateBall(b *ball, dt float64) {
333 v := &b.v
334 ux, uy, uz := v.x, v.y, v.z
335 s2 := ux*ux + uy*uy + uz*uz
336 sp := b.sp
337 if q := sp; !(q > 0) {
338 sp = fsqrt(s2)
339 } else if d, lim := s2-q*q, 0.01*s2; d > lim {
340 sp = fsqrt(s2)
341 } else if d < -lim {
342 sp = fsqrt(s2)
343 } else {
344 sp = 0.5 * (q + s2/q)
345 }
346 b.sp = sp
347 nk := -dragK * sp
348 ax, ay, az := nk*ux, nk*uy-simG, nk*uz
349 grounded := b.grounded
350 w := &b.w
351 if !grounded {
352 wx, wy, wz := w.x, w.y, w.z
353 ax += (wy*uz - wz*uy) * magnusK
354 ay += (wz*ux - wx*uz) * magnusK
355 az += (wx*uy - wy*ux) * magnusK
356 }
357 if b.inGoal {
358 n := e.netAccel(b)
359 ax, ay, az = ax+n.x, ay+n.y, az+n.z
360 }
361 if grounded {
362 if ay < 0 {
363 ay = 0
364 }
365 if h := fsqrt(ux*ux + uz*uz); h > 0 {
366 d := ballRollDecel // jsMin(ballRollDecel, h/dt), both positive
367 if q := h / dt; q < d {
368 d = q
369 }
370 ax -= (d * ux) / h
371 az -= (d * uz) / h
372 }
373 }
374 vx, vy, vz := ux+ax*dt, uy+ay*dt, uz+az*dt
375 v.x, v.y, v.z = vx, vy, vz
376 p := &b.p
377 p.x += vx * dt
378 p.y += vy * dt
379 p.z += vz * dt
380 if grounded {
381 w.x, w.y, w.z = vz/ballR, w.y*(1-2*dt), -vx/ballR
382 } else {
383 f := 1 - ballSpinDecay*dt
384 w.x *= f
385 w.y *= f
386 w.z *= f
387 }
388}
389
390func groundContact(b *ball) float64 {
391 if b.p.y >= ballR {
392 return 0
393 }
394 b.p = vec{b.p.x, ballR, b.p.z}
395 if b.v.y >= 0 {
396 return 0
397 }
398 vy := -b.v.y
399 if !b.grounded && vy > ballMinBounce {
400 k := ballGroundKep
401 b.v = vec{b.v.x * k, vy * ballEGround, b.v.z * k}
402 b.w = add(scale(b.w, 0.5), scale(rollingSpin(b.v), 0.5))
403 return vy
404 }
405 b.v = vec{b.v.x, 0, b.v.z}
406 b.grounded = true
407 return 0
408}
409
410type probe struct {
411 ok bool
412 relSpeed float64
413 dist float64
414}
415
416func probeCapsule(b *ball, a, bb vec, r float64, va, vb vec) probe {
417 ab := sub(bb, a)
418 abab := dot(ab, ab)
419 s := 0.0
420 if abab > 0 {
421 s = clamp(dot(sub(b.p, a), ab)/abab, 0, 1)
422 }
423 d := sub(b.p, add(a, scale(ab, s)))
424 minD := ballR + r
425 d2 := dot(d, d)
426 if d2 >= minD*minD {
427 return probe{}
428 }
429 dist := fsqrt(d2)
430 n := vec{0, 0, 1}
431 if dist > 1e-9 {
432 n = scale(d, 1/dist)
433 }
434 sv := add(va, scale(sub(vb, va), s))
435 rel := sub(b.v, sv)
436 vn := dot(rel, n)
437 if vn >= 0 {
438 return probe{}
439 }
440 return probe{ok: true, relSpeed: length(rel), dist: dist}
441}
442
443type hit struct {
444 ok bool
445 impactSpeed float64
446 gaveWay bool
447}
448
449// collideCapsule resolves the ball against a capsule moving with end
450// velocities va, vb (nil in the browser for the goal frame: still). give < 0
451// means the part never gives way (Infinity in the browser).
452func collideCapsule(b *ball, a, bb vec, r float64, moving bool, va, vb vec, e, mu, spinKeep, give float64) hit {
453 ab := sub(bb, a)
454 abab := dot(ab, ab)
455 s := 0.0
456 if abab > 0 {
457 s = clamp(dot(sub(b.p, a), ab)/abab, 0, 1)
458 }
459 q := add(a, scale(ab, s))
460 d := sub(b.p, q)
461 d2 := dot(d, d)
462 minD := ballR + r
463 if d2 >= minD*minD {
464 return hit{}
465 }
466 dist := fsqrt(d2)
467 n := vec{0, 0, 1}
468 if dist > 1e-9 {
469 n = scale(d, 1/dist)
470 }
471 sv := vec{0, 0, 0}
472 if moving {
473 sv = add(va, scale(sub(vb, va), s))
474 }
475 rel := sub(b.v, sv)
476 vn := dot(rel, n)
477 if vn >= 0 {
478 b.p = add(q, scale(n, minD))
479 return hit{}
480 }
481 relN := scale(n, vn)
482 relT := sub(rel, relN)
483 needed := -(1 + e) * vn
484 if give >= 0 && needed > give {
485 b.v = add(sv, add(scale(relT, 1-mu), scale(n, vn+give)))
486 b.w = scale(b.w, spinKeep)
487 return hit{ok: true, impactSpeed: -vn, gaveWay: true}
488 }
489 b.p = add(q, scale(n, minD))
490 b.v = add(sv, sub(scale(relT, 1-mu), scale(relN, e)))
491 b.w = scale(b.w, spinKeep)
492 return hit{ok: true, impactSpeed: -vn}
493}
494
495// slowerThan is length(v) < s, taking the root only when it could be true.
496func slowerThan(v vec, s float64) bool {
497 d := v.x*v.x + v.y*v.y + v.z*v.z
498 if d > s*s*1.01 {
499 return false
500 }
501 return fsqrt(d) < s
502}
503
504// pounces reports whether the keeper gets to a loose ball before it is
505// wholly over the line: slower than kPounceSpeed, within kPounceReach of the
506// hips and below kPounceHeight, the keeper not mid-dive, and the ball needing
507// longer to cross than the keeper needs to go down on it.
508func pounces(k *keeper, b *ball) bool {
509 if k.holding || k.phase == phasePush || k.phase == phaseAir || b.p.y > kPounceHeight {
510 return false
511 }
512 dx, dy, dz := b.p.x-k.pos.x, b.p.y-k.pos.y, b.p.z-k.pos.z
513 d2 := dx*dx + dy*dy + dz*dz
514 if d2 >= kPounceReach*kPounceReach {
515 return false
516 }
517 u := b.v
518 if u.x*u.x+u.y*u.y+u.z*u.z >= kPounceSpeed*kPounceSpeed {
519 return false
520 }
521 if u.z >= 0 {
522 return true
523 }
524 // The keeper needs kPounceDelay plus the distance beyond kPounceNear at
525 // kPounceMove: compared squared, as the browser does.
526 over := goalZ - postR - ballR
527 spare := (b.p.z-over)/-u.z - kPounceDelay
528 if spare <= 0 {
529 return false
530 }
531 reach := kPounceNear + spare*kPounceMove
532 return d2 < reach*reach
533}
534
535// ------------------------------------------------------------ catch or parry
536
537func segDist(p, a, b vec) float64 {
538 ab := sub(b, a)
539 abab := dot(ab, ab)
540 s := 0.0
541 if abab > 0 {
542 s = clamp(dot(sub(p, a), ab)/abab, 0, 1)
543 }
544 return length(sub(p, add(a, scale(ab, s))))
545}
546
547func clamp01(x float64) float64 {
548 if x < 0 {
549 return 0
550 }
551 if x > 1 {
552 return 1
553 }
554 return x
555}
556
557type decision struct {
558 held bool
559 hands float64
560 toBody float64
561}
562
563func (e *engine) catchDecision(cs *caps, part *capsule, relSpeed float64, ballP vec) decision {
564 S := e.scale
565 gl := cs[3].a
566 gr := cs[7].a
567 mid := scale(add(gl, gr), 0.5)
568 hx := ballP.x - mid.x
569 hy := ballP.y - mid.y
570 hands := 1 - clamp01((fsqrt(hx*hx+hy*hy)-kHandsGap*S)/((kHandsSpan-kHandsGap)*S))
571 toBody := math.Inf(1)
572 for i := range cs {
573 if cs[i].kind == kindTorso {
574 toBody = jsMin(toBody, segDist(ballP, cs[i].a, cs[i].b)-cs[i].r)
575 }
576 }
577 body := 1 - clamp01((toBody-kBodyNear*S)/((kBodyFar-kBodyNear)*S))
578 quality := kHandsWeight*hands + (1-kHandsWeight)*body
579 limit := kCatchMin + (kCatchMax-kCatchMin)*quality
580 if part.kind == kindGlove && hands < 0.5 {
581 limit = jsMin(limit, kOneHandMax)
582 }
583 catchable := part.kind == kindGlove || part.kind == kindTorso
584 if part.kind == kindLeg || part.kind == kindArm {
585 catchable = true
586 limit = kGatherSpeed
587 } else if catchable && relSpeed <= kGatherSpeed && ballP.y < 0.6 {
588 limit = jsMax(limit, kGatherSpeed)
589 }
590 return decision{held: catchable && relSpeed <= limit, hands: hands, toBody: toBody}
591}
592
593func (e *engine) holdPoint(cs *caps) vec {
594 gl := cs[3].a
595 gr := cs[7].a
596 p := add(scale(add(gl, gr), 0.5), vec{0, 0, 0.06 * e.scale})
597 return vec{p.x, jsMax(ballR, p.y), p.z}
598}
599
600// r3 is the browser's millimetre rounding: Math.round(x * 1000) / 1000.
601func r3(x float64) float64 {
602 return jsRound(x*1000) / 1000
603}
604
605// ---------------------------------------------------------------- simulate
606
607// Work done only near the keeper or the goal frame (the browser engine has
608// the same ZONES). A keeper part at full reach can touch the ball only
609// within 1.97 m of the hip: the farthest part is a glove, 1.29 x scale from
610// it, plus 0.35 m of collision margin, reaction reach and late hand reach,
611// plus the ball's radius; the hip used is the keeper block's end pose, at
612// most 14 cm from the interpolated one. Contacts are checked from probeZone
613// out, posts and bar only within a centimetre of reach (nearFrame). Skipping checks that would find
614// nothing leaves every result unchanged.
615var (
616 probeZone = 2.2
617 keeperBlock = 20 // steps per keeper step: it moves at 50 Hz
618 ballBlock = 5 // steps per ball step in free flight
619)
620
621// Simulate plays a kick to its verdict. plan is ignored with opt.NoKeeper.
622func Simulate(in Shot, pl Plan, prm Params, opt Options) Result {
623 e := newEngine(prm)
624 withKeeper := !opt.NoKeeper
625 ln := e.computeLaunch(&in)
626 b := ball{p: ln.pos, v: ln.vel, w: ln.spin}
627 res := Result{
628 Touch: -1,
629 Launch: Launch{
630 Vel: [3]float64{ln.vel.x, ln.vel.y, ln.vel.z},
631 Spin: [3]float64{ln.spin.x, ln.spin.y, ln.spin.z},
632 Speed: ln.speed, FuzzX: ln.fuzz.x, FuzzY: ln.fuzz.y,
633 ContactX: ln.contact.x, ContactY: ln.contact.y,
634 },
635 }
636 var k keeper
637 var kb blocks
638 if withKeeper {
639 var m misread
640 if in.HasMishit && !opt.NoMisread {
641 m = keeperMisread(&in.Mishit)
642 res.Misread = [4]float64{m.x, m.y, m.vx, m.jump}
643 }
644 k = e.keeperInit(m)
645 e.initCaps(&kb.cbuf[0])
646 e.initCaps(&kb.cbuf[1])
647 kb.pose[1] = poseOf(&k)
648 }
649 every := opt.TraceEvery
650 if every <= 0 {
651 every = 25
652 }
653 trace := uint32(0x811c9dc5)
654
655 step := 0
656 t := 0.0
657 verdict := 0
658 lastContact := -1.0
659 var gaveWay [nParts]float64
660 for i := range gaveWay {
661 gaveWay[i] = negInf
662 }
663 woodworkSet := false
664 touched := false // res.Touch >= 0 || woodworkSet
665 tracing := opt.Trace
666
667 // The bounds the loop tests against, worked out once with the same
668 // operations as the browser's checks.
669 near2 := probeZone * probeZone
670 far := probeZone + 0.3
671 far2 := far * far
672 lowY := ballR + 0.15
673 fr25 := ballR + postR + 0.25 // nearFrame(p, 0.25)
674 nfr25 := -fr25
675 fr01 := ballR + postR + 0.01 // nearFrame(p, 0.01)
676 nfr01 := -fr01
677 sideX, roofY := e.netSideX, e.netRoofY
678 side25 := sideX + fr25
679 side01 := sideX + fr01
680 boardsIn := boardsZ - 0.3
681 netZ := goalZ - postR
682 netZ2 := goalZ - postR - 0.5
683 crossZ := goalZ - postR - ballR
684 awayZ := goalZ + 0.8
685 wideX := sideX + ballR
686 liftY := ballR + 0.005
687
688 for verdict == 0 {
689 if tracing && step%every == 0 {
690 trace = foldState(trace, &b, withKeeper, &k)
691 }
692 px, py, pz := b.p.x, b.p.y, b.p.z
693 // In free flight the ball moves in steps of ballBlock; this pass then
694 // covers steps first..step. The browser's freeFlight, written out:
695 // nothing the ball can touch within reach during the step, and the
696 // step inside one keeper block.
697 adv := 1
698 if !b.held && !b.inGoal {
699 // Comparisons are kept in if conditions: as operands of && and ||
700 // gno converts each to bool at a cost.
701 ff := true
702 if withKeeper {
703 dx, dy, dz := px-k.pos.x, py-k.pos.y, pz-k.pos.z
704 if dx*dx+dy*dy+dz*dz < far2 {
705 ff = false
706 }
707 }
708 if !ff {
709 } else if b.grounded {
710 } else if py < lowY {
711 ff = false
712 }
713 if !ff {
714 } else if dz := pz - goalZ; dz > fr25 {
715 } else if dz < nfr25 {
716 } else {
717 ax := px
718 if px < 0 {
719 ax = -px
720 }
721 if dx := ax - sideX; dx < fr25 {
722 if dx > nfr25 {
723 ff = false
724 }
725 }
726 if !ff {
727 } else if dy := py - roofY; dy < fr25 {
728 if dy > nfr25 {
729 if ax < side25 {
730 ff = false
731 }
732 }
733 }
734 }
735 if !ff {
736 } else if withKeeper {
737 first := step + 1
738 end := kb.s0 + kb.n - 1
739 if first >= kb.s0+kb.n {
740 end = first + blockLen(&k, &pl, first) - 1
741 }
742 ff = first+ballBlock-1 <= end
743 }
744 if ff {
745 adv = ballBlock
746 }
747 }
748 first := step + 1
749 step += adv
750 t = float64(step) * simDT
751
752 // The keeper moves in 50 Hz blocks; its shapes are only needed once
753 // the ball is within reach (probeZone).
754 near := false
755 num := 0 // steps into the keeper's current block
756 if withKeeper {
757 if first >= kb.s0+kb.n {
758 e.keeperBlock(&k, &pl, first, &b, &kb)
759 }
760 num = step - kb.s0 + 1
761 dx, dy, dz := px-k.pos.x, py-k.pos.y, pz-k.pos.z
762 if dx*dx+dy*dy+dz*dz < near2 {
763 near = true
764 }
765 }
766
767 if b.held {
768 all := e.blockCaps(&kb, num)
769 target := e.holdPoint(&all)
770 f := jsMin(1, kHoldPull*simDT)
771 np := add(b.p, scale(sub(target, b.p), f))
772 b.v = scale(sub(np, b.p), 1/simDT)
773 b.p = vec{np.x, jsMax(ballR, np.y), np.z}
774 b.w = scale(b.w, jsMax(0, 1-20*simDT))
775 continue
776 }
777
778 if adv == 1 {
779 e.integrateBall(&b, simDT)
780 } else if b.grounded {
781 e.integrateBall(&b, float64(adv)*simDT)
782 } else {
783 heunBall(&b, float64(adv)*simDT)
784 }
785 if b.p.y < ballR {
786 groundContact(&b)
787 }
788 px, py, pz = b.p.x, b.p.y, b.p.z
789
790 // Posts and bar: only near them (nearFrame(p, 0.01), written out).
791 frameNear := false
792 if dz := pz - goalZ; dz > fr01 {
793 } else if dz < nfr01 {
794 } else {
795 ax := px
796 if px < 0 {
797 ax = -px
798 }
799 if dx := ax - sideX; dx < fr01 {
800 if dx > nfr01 {
801 frameNear = true
802 }
803 }
804 if frameNear {
805 } else if dy := py - roofY; dy < fr01 {
806 if dy > nfr01 {
807 if ax < side01 {
808 frameNear = true
809 }
810 }
811 }
812 }
813 if frameNear {
814 for i := range e.frame {
815 c := &e.frame[i]
816 h := collideCapsule(&b, c.a, c.b, c.r, false, vec{}, vec{}, ePost, 0.1, 0.7, -1)
817 if h.ok && h.impactSpeed > 0.5 && !woodworkSet {
818 woodworkSet = true
819 touched = true
820 res.Woodwork = c.part
821 }
822 }
823 px, py, pz = b.p.x, b.p.y, b.p.z
824 }
825
826 near0 := near // within probeZone of the hips; pouncing needs kPounceReach, less
827 // Clear of the whole body over the block, at any part's full reach:
828 // every per-part check below would find nothing.
829 if near {
830 bd := e.blockBody(&kb)
831 if px < bd[0] {
832 near = false
833 } else if px > bd[1] {
834 near = false
835 } else if py < bd[2] {
836 near = false
837 } else if py > bd[3] {
838 near = false
839 } else if pz < bd[4] {
840 near = false
841 } else if pz > bd[5] {
842 near = false
843 }
844 }
845 if near {
846 sp := ballSpeed(&b, b.v)
847 react := 0.0
848 if t >= kPerceive && sp < kReactSpeed {
849 react = kReactReach * (kReactSpeed - sp) / kReactSpeed
850 }
851 late := jsMin(kHandLateMax, kHandLateRate*jsMax(0, t-kPerceive-0.1))
852 inAir := k.phase == phaseAir
853 capsB := e.blockCapsB(&kb)
854 cand := e.blockCands(&kb, b.p)
855 for i := 0; i < nCaps; i++ {
856 if !cand[i] {
857 continue
858 }
859 cb := &capsB[i]
860 if gaveWay[cb.part] > t {
861 continue
862 }
863 // The browser adds 0 for the parts without the extra reach,
864 // which leaves the (positive) radius as it is.
865 reach := cb.hit
866 if cb.kind != kindHead {
867 reach += react
868 }
869 if cb.kind == kindGlove {
870 reach += late
871 }
872 // Outside the box of the part's positions over the block by
873 // more than the contact distance (plus a centimetre against
874 // rounding) on any axis: the probe could only find nothing.
875 bx := e.blockBox(&kb, i, num)
876 m := ballR + reach + 0.01
877 if px < bx[0]-m {
878 continue
879 } else if px > bx[1]+m {
880 continue
881 } else if py < bx[2]-m {
882 continue
883 } else if py > bx[3]+m {
884 continue
885 } else if pz < bx[4]-m {
886 continue
887 } else if pz > bx[5]+m {
888 continue
889 }
890 cc := e.blockCap(&kb, i, num)
891 c := &cc
892 va, vb := e.blockVel(&kb, i)
893 pr := probeCapsule(&b, c.a, c.b, reach, va, vb)
894 if !pr.ok {
895 continue
896 }
897 if res.Touch < 0 {
898 res.Touch = c.part
899 res.TouchStep = step
900 touched = true
901 }
902 newTouch := t-lastContact > kTouchGap
903 lastContact = t
904 all := e.blockCaps(&kb, num)
905 d := e.catchDecision(&all, c, pr.relSpeed, b.p)
906 if newTouch && d.held && verdict == 0 && pz >= crossZ {
907 res.Contacts = append(res.Contacts, Contact{Step: step, Part: c.part, Catch: true})
908 res.Caught = true
909 b.held = true
910 b.grounded = false
911 k.holding = true
912 verdict, res.DecidedStep, res.DecidedAt = Save, step, t
913 break
914 }
915 var give float64
916 switch c.kind {
917 case kindGlove:
918 give = kGiveGlove
919 if d.hands >= 0.5 {
920 give = kGiveHands
921 }
922 case kindLeg:
923 give = kGiveLeg
924 if inAir {
925 give = kGiveLegInAir
926 }
927 case kindArm:
928 give = kGiveArm
929 case kindTorso:
930 give = kGiveTorso
931 default:
932 give = kGiveHead
933 }
934 if pr.dist > ballR+c.r {
935 give *= kFingertipGive
936 }
937 if c.kind == kindGlove && d.toBody < kReflexBody*e.scale {
938 give = jsMax(give, kGiveTorso)
939 }
940 el := kEBody
941 if c.kind == kindGlove {
942 el = kEGlove
943 }
944 h := collideCapsule(&b, c.a, c.b, reach, true, va, vb, el, kFriction, 0.4, give)
945 px, py, pz = b.p.x, b.p.y, b.p.z
946 if !h.ok {
947 continue
948 }
949 if h.gaveWay {
950 gaveWay[c.part] = t + kTouchGap
951 }
952 if !h.gaveWay && c.kind == kindGlove && pr.relSpeed < kParryControl && b.v.z < kParryOut {
953 wide := -1.0
954 if px >= k.pos.x {
955 wide = 1
956 }
957 b.v = vec{b.v.x + wide*kParryWide, b.v.y, kParryOut}
958 }
959 if newTouch {
960 res.Contacts = append(res.Contacts, Contact{Step: step, Part: c.part, GaveWay: h.gaveWay})
961 }
962 }
963 if b.held {
964 continue
965 }
966 }
967
968 // The boards, the line and the net all lie at or behind the goal line.
969 x, y, z := px, py, pz
970 ax := x
971 if x < 0 {
972 ax = -x
973 }
974 if z <= goalZ {
975 if z-ballR < boardsZ && z > boardsIn && b.v.z < 0 && y-ballR < boardsH && ax < boardsHW {
976 b.p = vec{x, y, boardsZ + ballR}
977 b.v = vec{b.v.x * 0.7, b.v.y * 0.7, -0.3 * b.v.z}
978 z = b.p.z
979 }
980 }
981
982 if b.grounded {
983 if b.v.y > 0.3 {
984 b.grounded = false
985 } else if y > liftY {
986 b.grounded = false
987 }
988 }
989
990 if z <= goalZ {
991 if !res.LineCross {
992 res.LineCross = true
993 res.LineT, res.LineX, res.LineY = r3(t), r3(x), r3(y)
994 }
995 if !b.inGoal && z < netZ && z > netZ2 && ax < sideX && y < roofY {
996 b.inGoal = true
997 }
998 if !res.Crossing && z < crossZ {
999 res.Crossing = true
1000 res.CrossT, res.CrossX, res.CrossY = r3(t), r3(x), r3(y)
1001 if b.inGoal {
1002 verdict = Goal
1003 } else if res.Touch >= 0 {
1004 verdict = Save
1005 } else if woodworkSet {
1006 verdict = Post
1007 } else {
1008 verdict = Miss
1009 }
1010 res.DecidedStep, res.DecidedAt = step, t
1011 }
1012 }
1013
1014 if verdict == 0 {
1015 kept := false // decided without crossing the line
1016 if touched {
1017 if b.v.z > 0 {
1018 if z > awayZ {
1019 kept = true // parried away
1020 }
1021 }
1022 }
1023 if kept {
1024 } else if b.grounded && slowerThan(b.v, 0.15) {
1025 kept = true
1026 } else if ax > wideX {
1027 if x*b.v.x >= 0 {
1028 kept = true // outside a post and moving away from it
1029 }
1030 }
1031 if kept {
1032 } else if touched && withKeeper && near0 && pounces(&k, &b) {
1033 // The keeper pounces on the loose ball.
1034 verdict, res.DecidedStep, res.DecidedAt = Save, step, t
1035 b.v = vec{0, 0, 0}
1036 b.w = vec{0, 0, 0}
1037 res.Pounced = true
1038 } else if t >= simMaxT {
1039 kept = true
1040 }
1041 if kept {
1042 if res.Touch >= 0 {
1043 verdict = Save
1044 } else if woodworkSet {
1045 verdict = Post
1046 } else {
1047 verdict = Miss
1048 }
1049 res.DecidedStep, res.DecidedAt = step, t
1050 }
1051 }
1052 }
1053 if opt.Trace {
1054 trace = foldState(trace, &b, withKeeper, &k)
1055 }
1056 res.Outcome = verdict
1057 res.EndX, res.EndY = b.p.x, b.p.y
1058 res.Steps = step
1059 res.Trace = trace
1060 if withKeeper {
1061 res.Commits = k.commits
1062 }
1063 return res
1064}
1065
1066// foldState hashes the ball and keeper state (FNV-1a over the float64 bits,
1067// big-endian), as the browser-side test harness does.
1068func foldState(h uint32, b *ball, withKeeper bool, k *keeper) uint32 {
1069 h = foldVec(h, b.p)
1070 h = foldVec(h, b.v)
1071 h = foldVec(h, b.w)
1072 if withKeeper {
1073 h = foldVec(h, k.pos)
1074 h = foldVec(h, k.vel)
1075 h = foldFloat(h, k.lean.s)
1076 h = foldFloat(h, k.lean.c)
1077 h = foldVec(h, k.arms.l)
1078 h = foldVec(h, k.arms.r)
1079 }
1080 return h
1081}
1082
1083func foldVec(h uint32, v vec) uint32 {
1084 h = foldFloat(h, v.x)
1085 h = foldFloat(h, v.y)
1086 return foldFloat(h, v.z)
1087}
1088
1089func foldFloat(h uint32, f float64) uint32 {
1090 u := math.Float64bits(f)
1091 for i := 0; i < 8; i++ {
1092 h ^= uint32(byte(u >> (56 - 8*uint(i))))
1093 h *= 0x01000193
1094 }
1095 return h
1096}