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

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}