engine.gno
10.02 Kb · 427 lines
1// Package physics is the Spot Kick penalty engine: a port of the browser
2// game's gno-shots/src/physics.js that produces bit-identical results.
3//
4// The JavaScript engine uses only + - * /, square roots, comparisons and
5// 32-bit integer mixing inside its simulation, all of which IEEE-754 rounds
6// exactly; gno does float64 arithmetic in software with the same rounding,
7// and its math.Sqrt is correctly rounded. So the same inputs give the same
8// bits here as in any browser, provided every operation happens in the same
9// order. That is the rule this file follows, line by line:
10//
11// - every float constant is a variable, never a Go constant: constant
12// expressions are folded at arbitrary precision, JavaScript rounds after
13// each operation;
14// - expressions keep the JavaScript order and grouping;
15// - JavaScript's Math.min, Math.max and Math.round are reproduced exactly
16// (jsMin, jsMax, jsRound), down to the sign of zero.
17//
18// Unlike the browser, which keeps simulating for the replay, Simulate stops
19// as soon as the kick is decided; nothing after that can change the verdict.
20package physics
21
22import "math"
23
24// ---------------------------------------------------------------- constants
25
26var (
27 negInf = math.Inf(-1)
28 negZero = math.Copysign(0, -1)
29
30 simDT = 0.001
31 simMaxT = 2.5
32 simG = 9.81
33
34 ballR = 0.11
35 ballM = 0.43
36 ballEGround = 0.55
37 ballGroundKep = 0.85
38 ballMinBounce = 1.0
39 ballRollDecel = 1.2
40 ballSpinDecay = 0.05
41 ballI float64
42
43 airRho = 1.2
44 airCD = 0.25
45 airCL = 1.0
46 area float64
47 dragK float64
48 magnusK float64
49
50 goalZ = -11.0
51 postR = 0.06
52 goalDepth = 2.0
53 ePost = 0.7
54 netKs = 2500.0
55 netCs = 40.0
56 netRecoil = 0.1
57 netFriction = 3.0
58 boardsZ = -16.0
59 boardsH = 1.0
60 boardsHW = 30.0
61 netBackZ float64
62
63 kickNormalMix = 0.18
64 kickSideSpin = 0.6
65 kickBackSpin = 0.25
66 kickMaxSpin = 110.0
67 kickMaxStrike = 0.8
68 kickAimX = 6.0
69 kickAimY = 4.0
70
71 kLineOffset = 0.25
72 kHipFactor = 0.93
73 kPushTime = 0.12
74 kLeanRate = 8.0
75 kArmRate = 10.0
76 kLeanBias = -0.3
77 kLeanMin = 0.15
78 kSlideDecel = 8.0
79 kStarSpeed = 1.2
80 kEGlove = 0.25
81 kEBody = 0.45
82 kFriction = 0.3
83 kShoulderX = 0.24
84 kShoulderY = 0.5
85 kArmLen = 0.62
86 kGloveReach = 0.73
87 kGloveR = 0.125
88 kReachPivot = 1.8
89 kReachSpan = 2.6
90 kPerceive = 0.16
91 kArmTrackRate = 12.0
92 kReadEvery = 20
93 kReadBase = 0.16
94 kReadReach = 0.75
95 kReadHeightGn = 2.2
96 kReadShuffleT = 0.45
97 kMisreadX = [2]float64{0.05, 1.2}
98 kMisreadY = [2]float64{0.04, 0.5}
99 kMisreadT = [2]float64{0.18, 0.28}
100 kMisjudgeVX = [2]float64{0.12, 0.6}
101 kMisjudgeJump = [2]float64{0.12, 0.6}
102 kPlantExtra = 0.08
103 kPlantStretch = 1.15
104 kRecoverDelay = 0.08
105 kHoldDownT = 0.3
106 kRecoverLeanRt = 9.0
107 kRecoverSpeed = 2.6
108 kRecoverAccel = 9.0
109 kRediveWindow = 0.38
110 kRediveMinDX = 0.45
111 kMaxCommits = 2
112 kCatchMin = 9.0
113 kCatchMax = 25.0
114 kHandsGap = 0.14
115 kHandsSpan = 0.32
116 kBodyNear = 0.25
117 kBodyFar = 0.8
118 kHandsWeight = 0.65
119 kHoldPull = 25.0
120 kTouchGap = 0.08
121 kMarginGlove = 0.03
122 kMarginArm = 0.02
123 kMarginTorso = 0.02
124 kMarginLeg = 0.02
125 kMarginHead = 0.01
126 kReactSpeed = 12.0
127 kReactReach = 0.12
128 kGatherSpeed = 7.5
129 kOneHandMax = 13.0
130 kLowDropSpeed = 1.8
131 kGiveHands = 36.0
132 kGiveGlove = 31.0
133 kGiveArm = 14.0
134 kGiveTorso = 30.0
135 kGiveHead = 30.0
136 kGiveLeg = 32.0
137 kGiveLegInAir = 26.0
138 kReflexBody = 0.25
139 kFingertipGive = 0.75
140 kParryControl = 16.0
141 kParryOut = 3.0
142 kParryWide = 2.0
143 kHandLateMax = 0.07
144 kHandLateRate = 0.25
145 kPounceSpeed = 3.0
146 kPounceReach = 1.8
147 kPounceHeight = 2.2
148 kPounceDelay = 0.2
149 kPounceNear = 0.9
150 kPounceMove = 3.0
151
152 planForwardMax = 22.0
153 planCentreShare = 0.06
154 planStarMaxSpd = 1.15
155
156 halfPi = 1.5707963267948966
157 pi = 3.141592653589793
158 twoPi = 6.283185307179586
159 sqrt2 = 1.4142135623730951
160
161 armsReady, armsStar, armsHold armPair
162 zeroVec vec
163 upVec vec
164)
165
166func init() {
167 two, three := 2.0, 3.0
168 ballI = (two / three) * ballM * ballR * ballR
169 area = pi * ballR * ballR
170 dragK = (0.5 * airRho * airCD * area) / ballM
171 magnusK = (0.5 * airRho * airCL * area * ballR) / ballM
172 netBackZ = goalZ - goalDepth
173 upVec = vec{0, 1, 0}
174 armsReady = armPair{norm(vec{-0.55, -0.45, 0.7}), norm(vec{0.55, -0.45, 0.7})}
175 armsStar = armPair{norm(vec{-0.9, 0.6, 0.15}), norm(vec{0.9, 0.6, 0.15})}
176 armsHold = armPair{norm(vec{0.18, 0.12, 0.95}), norm(vec{-0.18, 0.12, 0.95})}
177}
178
179// Params are the constants a game may change (penalty.Config, in SI units).
180// DefaultParams are the browser engine's.
181type Params struct {
182 GoalHalfWidth float64 // m
183 GoalHeight float64 // m
184 KeeperScale float64
185 ReactionMin float64 // s
186 ReactionMax float64
187 StarShare float64 // of the speed range
188 ReadShare float64
189 DiveMin float64 // m/s
190 DiveMax float64
191 JumpMin float64 // m/s
192 JumpMax float64
193 BallMinSpeed float64 // m/s
194 BallMaxSpeed float64
195 FuzzBase float64 // ball radii
196 FuzzPower float64
197 MaxContact float64 // ball radii
198}
199
200// DefaultParams returns the constants of the browser engine.
201func DefaultParams() Params {
202 return Params{
203 GoalHalfWidth: 3.2775, GoalHeight: 2.424, KeeperScale: 1.176,
204 ReactionMin: 0.02, ReactionMax: 0.12, StarShare: 0.06, ReadShare: 0.24,
205 DiveMin: 2.6, DiveMax: 5.2, JumpMin: -1.4, JumpMax: 4.0,
206 BallMinSpeed: 10, BallMaxSpeed: 34, FuzzBase: 0.03, FuzzPower: 0.1, MaxContact: 0.5,
207 }
208}
209
210// ------------------------------------------------------------- vector maths
211
212type vec struct{ x, y, z float64 }
213
214type armPair struct{ l, r vec }
215
216func add(a, b vec) vec { return vec{a.x + b.x, a.y + b.y, a.z + b.z} }
217func sub(a, b vec) vec { return vec{a.x - b.x, a.y - b.y, a.z - b.z} }
218func scale(a vec, s float64) vec { return vec{a.x * s, a.y * s, a.z * s} }
219func dot(a, b vec) float64 { return a.x*b.x + a.y*b.y + a.z*b.z }
220func crossVec(a, b vec) vec {
221 return vec{a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x}
222}
223func length(a vec) float64 { return fsqrt(dot(a, a)) }
224func norm(a vec) vec {
225 l := length(a)
226 if l > 0 {
227 return scale(a, 1/l)
228 }
229 return vec{0, 0, 0}
230}
231
232func clamp(x, lo, hi float64) float64 {
233 if x < lo {
234 return lo
235 }
236 if x > hi {
237 return hi
238 }
239 return x
240}
241
242// jsMin and jsMax are Math.min and Math.max for two non-NaN numbers: of two
243// zeros, min prefers -0 and max prefers +0.
244func jsMin(a, b float64) float64 {
245 if a < b {
246 return a
247 }
248 if b < a {
249 return b
250 }
251 if math.Signbit(b) {
252 return b
253 }
254 return a
255}
256
257func jsMax(a, b float64) float64 {
258 if a > b {
259 return a
260 }
261 if b > a {
262 return b
263 }
264 if !math.Signbit(b) {
265 return b
266 }
267 return a
268}
269
270// jsRound is Math.round: the nearest integer, halves rounded up, and -0
271// for -0.5 <= x <= -0.
272func jsRound(x float64) float64 {
273 f := floor(x)
274 if x-f >= 0.5 {
275 f++
276 }
277 if f == 0 {
278 if x < 0 {
279 return negZero
280 }
281 if math.Float64bits(x)>>63 != 0 {
282 return negZero
283 }
284 }
285 return f
286}
287
288// floor is math.Floor (gno's is interpreted and dear): below 2^52 in
289// magnitude by truncation, keeping -0; beyond that, and for infinities and
290// NaN, x itself.
291func floor(x float64) float64 {
292 if x < 4503599627370496 {
293 if x > -4503599627370496 {
294 f := float64(int64(x))
295 if f > x {
296 return f - 1
297 }
298 if f == 0 {
299 return x * 0
300 }
301 return f
302 }
303 }
304 return x
305}
306
307// detSin is the JavaScript engine's sine: range reduction and a Taylor series.
308func detSin(x float64) float64 {
309 x -= twoPi * jsRound(x/twoPi)
310 if x > halfPi {
311 x = pi - x
312 } else if x < -halfPi {
313 x = -pi - x
314 }
315 x2 := x * x
316 term := x
317 sum := x
318 for n := 1; n <= 9; n++ {
319 term *= -x2 / (float64(2*n) * float64(2*n+1))
320 sum += term
321 }
322 return sum
323}
324
325func detCos(x float64) float64 { return detSin(x + halfPi) }
326
327// ------------------------------------------------------------ seeded random
328
329type mulberry struct{ a uint32 }
330
331func (m *mulberry) next() float64 {
332 m.a += 0x6d2b79f5
333 t := m.a
334 t = (t ^ (t >> 15)) * (t | 1)
335 t ^= t + (t^(t>>7))*(t|61)
336 return float64(t^(t>>14)) / 4294967296
337}
338
339func fnvBytes(h uint32, b []byte) uint32 {
340 for _, c := range b {
341 h ^= uint32(c)
342 h *= 0x01000193
343 }
344 return h
345}
346
347func u16(b []byte, i int) uint16 { return uint16(b[i])<<8 | uint16(b[i+1]) }
348
349// ------------------------------------------------------------------- plans
350
351// Plan modes.
352const (
353 ModeStar = 1
354 ModeRead = 2
355 ModeDive = 3
356)
357
358// Plan is a keeper plan decoded from its four uint16.
359type Plan struct {
360 Raw [4]uint16
361 Mode int
362 Direction float64 // degrees: 0 = along the line to the taker's right, 180 = left
363 Speed float64 // m/s
364 Jump float64 // m/s
365 Reaction float64 // s
366}
367
368// DecodePlan maps four uint16 onto a plan exactly as the browser does.
369func DecodePlan(raw [4]uint16, p Params) Plan {
370 d := float64(raw[0]) / 65535
371 sp := float64(raw[1]) / 65535
372 j := float64(raw[2]) / 65535
373 r := float64(raw[3]) / 65535
374 pl := Plan{Raw: raw}
375 switch {
376 case sp < p.StarShare:
377 pl.Mode = ModeStar
378 case sp < p.StarShare+p.ReadShare:
379 pl.Mode = ModeRead
380 default:
381 pl.Mode = ModeDive
382 }
383 pl.Direction = planDirection(d)
384 pl.Speed = planSpeed(sp, p)
385 pl.Jump = lin(j, p.JumpMin, p.JumpMax)
386 pl.Reaction = lin(r, p.ReactionMin, p.ReactionMax)
387 return pl
388}
389
390func lin(vv, lo, hi float64) float64 { return lo + (hi-lo)*vv }
391
392func planDirection(vv float64) float64 {
393 s := 2*vv - 1
394 c := planCentreShare
395 F := planForwardMax
396 if s <= -c {
397 return (F * (1 + s)) / (1 - c)
398 }
399 if s >= c {
400 return 180 - (F*(1-s))/(1-c)
401 }
402 return 90 + ((90-F)*s)/c
403}
404
405func planSpeed(vv float64, p Params) float64 {
406 k := p.StarShare
407 r := k + p.ReadShare
408 if vv < k {
409 return (planStarMaxSpd * vv) / k
410 }
411 if vv < r {
412 return 0
413 }
414 return lin((vv-r)/(1-r), p.DiveMin, p.DiveMax)
415}
416
417func (pl *Plan) commitTime() float64 {
418 if pl.Mode == ModeRead {
419 return kReadBase + pl.Reaction
420 }
421 return pl.Reaction
422}
423
424func launchVelocity(pl *Plan) vec {
425 a := pl.Direction * (pi / 180)
426 return vec{pl.Speed * detCos(a), pl.Jump, pl.Speed * detSin(a)}
427}