Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}