simulate.gno
4.93 Kb · 147 lines
1package penalty
2
3// Outcome of a single kick.
4type Outcome uint8
5
6const (
7 OutcomeGoal Outcome = iota + 1
8 OutcomeSave
9 OutcomeMiss
10 OutcomePost
11)
12
13func (o Outcome) String() string {
14 switch o {
15 case OutcomeGoal:
16 return "goal"
17 case OutcomeSave:
18 return "save"
19 case OutcomeMiss:
20 return "miss"
21 case OutcomePost:
22 return "woodwork"
23 }
24 return "unknown"
25}
26
27// Scored reports whether the kick counts as a goal.
28func (o Outcome) Scored() bool { return o == OutcomeGoal }
29
30// Result is what SimulateShot reports.
31type Result struct {
32 Outcome Outcome
33 X, Y int64 // where the ball crosses (or would cross) the goal line, mm
34 ArrivalMS int64 // time from contact to the line
35 BallSpeed int64 // mm/s
36 MishitX int64 // contact error the mishit seed added, milli-radii
37 MishitY int64
38 Keeper Mode
39}
40
41// SimulateShot decides a kick from its ten inputs: the taker's six (five
42// choices plus the mishit seed) and the keeper's four, played with the
43// game's constants. It is a pure function: the same inputs give the same
44// Result on every node, so anyone can re-run it to check a recorded kick.
45//
46// SUPERSEDED. This is the coarse integer model the realm used before the
47// physics engine was ported (gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/penalty/physics/v0, which
48// matches the browser game bit for bit and is what impl/v1 calls). It is kept
49// so that kicks recorded under it can still be re-run.
50func SimulateShot(shot ShotParams, plan KeeperPlan, c Config) Result {
51 power := clampI(shot.Power, 0, 1000)
52
53 // The boot lands off the chosen contact by a seeded, power-dependent error.
54 sigma := c.FuzzBase + c.FuzzPower*power/1000
55 mx := normalPermille(shot.Mishit, 0) * sigma / 1000
56 my := normalPermille(shot.Mishit, 12) * sigma / 1000
57 cx := clampI(shot.ContactX+mx, -800, 800)
58 cy := clampI(shot.ContactY+my, -800, 800)
59
60 // Glancing contact loses pace: about 1 - r²/2 of a centred strike.
61 eff := 1000 - (cx*cx+cy*cy)*225/1000000
62 speed := (c.BallMinSpeed + (c.BallMaxSpeed-c.BallMinSpeed)*power/1000) * eff / 1000
63 t := Distance * 1000 / speed // ms to the line
64
65 // Striking the right side sends the ball left and curls it further left;
66 // hitting below centre lifts it, above centre dips it; gravity drops it.
67 x := shot.AimX - cx*CurlPerMilliR/10
68 drop := Gravity * t * t / 2 / 1000000
69 y := shot.AimY - drop - cy*LiftPerMilliR/10
70 if y < BallRadius {
71 y = BallRadius
72 }
73
74 d := DecodePlan(plan, c)
75 res := Result{X: x, Y: y, ArrivalMS: t, BallSpeed: speed, MishitX: mx, MishitY: my, Keeper: d.Mode}
76
77 halfW := c.GoalWidth / 2
78 ax := absI(x)
79 inside := ax <= halfW-BallRadius && y <= c.GoalHeight-BallRadius
80 clear := ax >= halfW+2*PostRadius+BallRadius || y >= c.GoalHeight+2*PostRadius+BallRadius
81 switch {
82 case clear:
83 res.Outcome = OutcomeMiss
84 case !inside:
85 res.Outcome = OutcomePost
86 case keeperCovers(d, c, shot, x, y, t, cx):
87 res.Outcome = OutcomeSave
88 default:
89 res.Outcome = OutcomeGoal
90 }
91 return res
92}
93
94const (
95 CurlPerMilliR = 45 // tenths of a mm of sideways travel per milli-radius of side contact
96 LiftPerMilliR = 28 // tenths of a mm of lift per milli-radius below centre
97 LaunchShare = 20 // tenths of a mm of that sideways travel visible in the launch direction
98)
99
100// keeperCovers is the placeholder keeper: whether its body and hands reach
101// (x, y) by the time the ball arrives (t ms after contact).
102func keeperCovers(d Plan, c Config, shot ShotParams, x, y, t, cx int64) bool {
103 s := c.KeeperScale
104 reach := 900 * s / 1000 // hips to fingertips, sideways
105 switch d.Mode {
106 case ModeStar:
107 // Spread wide in the middle of the goal, off the ground.
108 return absI(x) <= 1100*s/1000 && y >= 450*s/1000
109 case ModeRead:
110 // Commits after seeing the ball: the slower the shot, the further it
111 // gets. It reads the launch line, not the curl.
112 commit := 160 + d.Reaction
113 avail := t - commit
114 if avail <= 0 {
115 return absI(x) <= 350*s/1000 && y <= 1800*s/1000
116 }
117 predicted := shot.AimX - cx*LaunchShare/10
118 cover := minI(reach+3500*avail/1000, c.GoalWidth/2+200)
119 return absI(predicted) <= cover && absI(x-predicted) <= 450*s/1000
120 }
121 // Committed dive: the hips travel along the heading once the push-off
122 // starts, the hands reach out from there, and the jump sets the height
123 // band it covers.
124 moving := t - d.Reaction - 60
125 if moving < 0 {
126 moving = 0
127 }
128 vx := d.Speed * cosPermille(d.Heading) / 1000
129 hipX := clampI(vx*moving/1000, -2600*s/1000, 2600*s/1000)
130 if absI(x) <= 300 && moving < 150 {
131 return true // straight at a keeper that has barely left the spot
132 }
133 centre := 900 + d.Jump*450/1000
134 band := 650 * s / 1000
135 return absI(x-hipX) <= reach+150 && y >= centre-band && y <= centre+band
136}
137
138// normalPermille turns 12 bytes of a mishit seed into an approximately
139// standard-normal value × 1000: six uint16 uniforms summed (Irwin-Hall,
140// n = 6), centred and scaled.
141func normalPermille(m [24]byte, offset int) int64 {
142 var sum int64
143 for i := 0; i < 6; i++ {
144 sum += int64(u16(m[offset+2*i : offset+2*i+2]))
145 }
146 return (sum - 3*65535) * 1414 / 65535
147}