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

course.gno

18.89 Kb · 575 lines
  1// Package course is what golf and a hole agree on: a hole as data (Simple,
  2// GG1), its weather, and how a stroke is launched and drops.
  3package course
  4
  5import (
  6	"math"
  7	"strconv"
  8
  9	"gno.land/p/nym-alexiscolin000/gnogolf/physics"
 10)
 11
 12// BoardW and BoardH are the board author.Fit gives a hole declared without W
 13// and H, in board units (nothing deployed reads them: Decode holds W and H to
 14// 1..MaxBoard).
 15const BoardW, BoardH = 32, 16
 16
 17// WearW and WearH are the wear grid, stretched over any board: 128 counters.
 18const WearW, WearH = 16, 8
 19
 20// MaxBoard is the longest side a board may have: it bounds the text board
 21// and the flood a client runs.
 22const MaxBoard = 96
 23
 24// WearIndexOn maps a position on a w x h board to its wear cell, clamped.
 25func WearIndexOn(p physics.Vec2, w, h int) int {
 26	x := int(p.X * WearW / float64(w))
 27	y := int(p.Y * WearH / float64(h))
 28	if x < 0 {
 29		x = 0
 30	}
 31	if x >= WearW {
 32		x = WearW - 1
 33	}
 34	if y < 0 {
 35		y = 0
 36	}
 37	if y >= WearH {
 38		y = WearH - 1
 39	}
 40	return y*WearW + x
 41}
 42
 43// Pulse is pieces that come and go: present on the strokes where
 44// (stroke+Phase) % Every < On, a timed wall's rule (physics.There) applied per
 45// stroke. The clock is the stroke number in the player's own round (0 is the
 46// first shot), never shared between players: previewing shot N and
 47// committing it meet the same course.
 48type Pulse struct {
 49	Every, On, Phase int
 50	Walls            []physics.Wall
 51	Posts            []physics.Post
 52	Zones            []physics.Zone
 53}
 54
 55// The weather kinds. Each but Clear is also the skin of its whole-board zones.
 56const (
 57	Clear = ""      // Clear is no weather at all
 58	Wind  = "wind"  // Wind is a Slope: a steady push, Vec per substep
 59	Rain  = "rain"  // Rain is a Surface a little quicker than dry grass
 60	Fog   = "fog"   // Fog is a Surface of scale 1: it only hides the far end
 61	Storm = "storm" // Storm is rain and wind in gusts, and lightning to look at
 62	Snow  = "snow"  // Snow is a Surface a little slower
 63)
 64
 65// chance is one kind of weather in a climate, out of 100.
 66type chance struct {
 67	percent int
 68	kind    string
 69}
 70
 71// climates is each world's chance of each kind, out of 100; a world not
 72// listed has the garden's. Unexported so nothing can change it.
 73var climates = map[string][]chance{
 74	"garden":   {{55, Clear}, {20, Wind}, {15, Rain}, {10, Fog}},
 75	"island":   {{50, Clear}, {25, Wind}, {15, Rain}, {10, Storm}},
 76	"town":     {{50, Clear}, {20, Rain}, {15, Fog}, {15, Wind}},
 77	"mountain": {{40, Clear}, {25, Snow}, {20, Wind}, {15, Fog}},
 78}
 79
 80// The weather's strengths.
 81const (
 82	// WindMin is the weakest wind, per substep.
 83	WindMin = 0.08
 84	// WindMax is the strongest wind, per substep.
 85	WindMax = 0.15
 86	// RainScale is how much quicker the green is in the rain: a touch, not
 87	// ice (much more and every board turns to pinball).
 88	RainScale = 1.025
 89	// WetIce is how much quicker again the rain makes ice.
 90	WetIce = 1.06
 91	// SnowScale is how much slower the green is in the snow.
 92	SnowScale = 0.9
 93)
 94
 95// Forecast is one hole's weather for one period. Whole-board zones lie under
 96// the hole's own; the rest (puddles, wet ice) lie over it.
 97type Forecast struct {
 98	Period int64
 99	Kind   string
100	Wind   physics.Vec2
101	Zones  []physics.Zone
102	// Work is what drawing it cost, in physics.MaxWork's units (about a
103	// thousand gas); golf counts it in a commit's budget.
104	Work int
105}
106
107// hash is 64-bit FNV-1a: small, deterministic, the same on every validator.
108func hash(s string) uint64 {
109	h := uint64(14695981039346656037)
110	for i := 0; i < len(s); i++ {
111		h ^= uint64(s[i])
112		h *= 1099511628211
113	}
114	return h
115}
116
117// ForecastFor is the weather of hole id (in world) for a period: a hash of
118// the two looked up in the world's climate. Nobody picks it.
119func ForecastFor(id, world string, h *Simple, period int64) Forecast {
120	seed := hash(id + "#" + strconv.FormatInt(period, 10))
121	table, ok := climates[world]
122	if !ok {
123		table = climates["garden"]
124	}
125	roll, kind := int(seed%100), Clear
126	for _, c := range table {
127		if roll < c.percent {
128			kind = c.kind
129			break
130		}
131		roll -= c.percent
132	}
133	f := Forecast{Period: period, Kind: kind, Work: forecastWork}
134	f.Wind, f.Zones = weatherFor(kind, h, seed, &f.Work)
135	return f
136}
137
138// rng is a tiny deterministic generator from a seed (Knuth's LCG).
139type rng struct{ s uint64 }
140
141func (r *rng) next() uint64 {
142	r.s = r.s*6364136223846793005 + 1442695040888963407
143	return r.s >> 11
144}
145
146func (r *rng) unit() float64 { return float64(r.next()%1000000) / 1000000 }
147
148// WeatherFor is the wind and zones of one kind of weather on a hole, drawn
149// from seed: the wind's side and strength, where the rain leaves its puddles.
150func WeatherFor(kind string, h *Simple, seed uint64) (physics.Vec2, []physics.Zone) {
151	var work int
152	return weatherFor(kind, h, seed, &work)
153}
154
155// weatherFor is WeatherFor, adding what it cost to *work (see wet).
156func weatherFor(kind string, h *Simple, seed uint64, work *int) (physics.Vec2, []physics.Zone) {
157	W, H := float64(h.W), float64(h.H)
158	all := func(k physics.ZoneKind, skin string, scale float64, vec physics.Vec2) physics.Zone {
159		// the wind is air, and never speeds the ball up
160		air := k == physics.Slope
161		return physics.Zone{Kind: k, Min: physics.V(0, 0), Max: physics.V(W, H), Scale: scale, Vec: vec, Skin: skin, Air: air, Capped: air}
162	}
163	r := &rng{s: seed}
164	blow := func() physics.Vec2 {
165		s := WindMin + r.unit()*(WindMax-WindMin)
166		if h.Shelter > 0 && s > h.Shelter {
167			s = h.Shelter
168		}
169		return physics.FromPolar(r.unit()*2*math.Pi, s)
170	}
171	switch kind {
172	case Wind:
173		v := blow()
174		return v, []physics.Zone{all(physics.Slope, Wind, 0, v)}
175	case Rain:
176		return physics.Vec2{}, wet(h, r, []physics.Zone{all(physics.Surface, Rain, RainScale, physics.Vec2{})}, work)
177	case Storm:
178		// rain, and the wind in two alternating gusts
179		*work += stormWork
180		v := blow()
181		a, s := math.Atan2(v.Y, v.X), v.Len()
182		g1, g2 := all(physics.Slope, Wind, 0, physics.FromPolar(a+gustTurn, s)), all(physics.Slope, Wind, 0, physics.FromPolar(a-gustTurn, s))
183		g1.Every, g1.On, g1.Phase = gustEvery, gustEvery/2, 0
184		g2.Every, g2.On, g2.Phase = gustEvery, gustEvery/2, gustEvery/2
185		zs := []physics.Zone{all(physics.Surface, Storm, 1, physics.Vec2{}), all(physics.Surface, Rain, RainScale, physics.Vec2{}), g1, g2}
186		return v, wet(h, r, zs, work)
187	case Fog:
188		return physics.Vec2{}, []physics.Zone{all(physics.Surface, Fog, 1, physics.Vec2{})}
189	case Snow:
190		return physics.Vec2{}, []physics.Zone{all(physics.Surface, Snow, SnowScale, physics.Vec2{})}
191	}
192	return physics.Vec2{}, nil
193}
194
195// A storm's gusts turn gustTurn radians (about 40°) off its wind, one way
196// then the other, each for half of gustEvery substeps.
197const (
198	gustTurn  = 0.7
199	gustEvery = 6
200)
201
202// Where rain leaves its puddles: minPuddles plus up to morePuddles-1 more,
203// in at most puddleTries tries; each puddleR plus up to puddleGrow wide,
204// puddleAspect as deep, and puddleScale as quick as the grass, kept
205// puddleGap clear of the one before.
206const (
207	minPuddles   = 2
208	morePuddles  = 3
209	puddleTries  = 60
210	puddleR      = 1.1
211	puddleGrow   = 0.7
212	puddleAspect = 0.8
213	puddleScale  = 0.6
214	puddleGap    = 0.5
215)
216
217// The work of a forecast, in physics.MaxWork's units, measured on GnoVM gas:
218// any forecast, a storm's gusts, a puddle try, and in it each post, zone,
219// polygon edge and fixed wall dryLand tests, and each zone wet checks for ice.
220const (
221	forecastWork = 3000 // drawing any forecast, before the rain's tries
222	stormWork    = 5000 // a storm's gusts, on top
223	tryWork      = 35
224	dryPostWork  = 22
225	dryZoneWork  = 20
226	dryEdgeWork  = 16
227	dryWallWork  = 100
228	iceWork      = 5
229)
230
231// WorstForecastWork bounds the Work of h's forecast in any period and world:
232// a storm's, whose puddle tries all fail, each after dryLand has tested every
233// piece (a try that fails sooner costs less, and one that places a puddle
234// ends them sooner). Golf refuses a hole whose worst forecast leaves a commit
235// no room for its heaviest shot.
236func WorstForecastWork(h *Simple) int {
237	f := h.Field()
238	try := tryWork + dryPostWork*len(f.Posts)
239	for _, z := range f.Zones {
240		try += dryZoneTest(z)
241	}
242	for _, wl := range f.Walls {
243		if wl.Every <= 0 {
244			try += dryWallWork
245		}
246	}
247	return forecastWork + stormWork + iceWork*len(f.Zones) + puddleTries*try
248}
249
250// dryZoneTest is the work of dryLand's test of one zone: its polygon's edges
251// too for any zone but plain ground (Contains, at its dearest).
252func dryZoneTest(z physics.Zone) int {
253	if (z.Kind != physics.Surface && z.Kind != physics.Slope) || (z.Kind == physics.Surface && z.Skin != "") {
254		return dryZoneWork + dryEdgeWork*len(z.Poly)
255	}
256	return dryZoneWork
257}
258
259// wet adds what rain leaves on a hole: its ice quicker still, and two to four
260// puddles on the lane, clear of the kerb, the pieces, the tee and the cup.
261// What it costs goes in *work.
262func wet(h *Simple, r *rng, zs []physics.Zone, work *int) []physics.Zone {
263	f := h.Field()
264	*work += iceWork * len(f.Zones)
265	for _, z := range f.Zones {
266		if z.Kind == physics.Surface && z.Skin == "ice" {
267			z.Scale *= WetIce
268			zs = append(zs, z)
269		}
270	}
271	want, placed := minPuddles+int(r.next()%morePuddles), []physics.Zone{}
272	for try := 0; try < puddleTries && len(placed) < want; try++ {
273		c := physics.V(r.unit()*float64(h.W), r.unit()*float64(h.H))
274		rad := puddleR + r.unit()*puddleGrow
275		*work += tryWork
276		if !dryLand(h, c, rad, work) {
277			continue
278		}
279		clash := false
280		for _, p := range placed {
281			if p.Min.Add(p.Max).Scale(0.5).Sub(c).LenCmp(p.Max.X-p.Min.X+puddleGap) < 0 {
282				clash = true
283			}
284		}
285		if clash {
286			continue
287		}
288		placed = append(placed, physics.Zone{Kind: physics.Surface, Min: c.Sub(physics.V(rad, rad*puddleAspect)), Max: c.Add(physics.V(rad, rad*puddleAspect)), Scale: puddleScale, Round: true, Skin: "puddle"})
289	}
290	return append(zs, placed...)
291}
292
293// dryLand says whether a puddle of radius rad can lie at c: inside the lane's
294// outline (even-odd over the fixed walls, a ray to a far point at a slant no
295// grid wall lies along), off any zone but plain ground, dryGap from any piece
296// and dryTee from tee and cup. Cheap tests first, no square roots: on a lane
297// hole every try fails.
298func dryLand(h *Simple, c physics.Vec2, rad float64, work *int) bool {
299	f := h.Field()
300	if c.Sub(h.Start()).LenCmp(rad+dryTee) < 0 || c.Sub(h.Cup()).LenCmp(rad+dryTee) < 0 {
301		return false
302	}
303	for _, p := range f.Posts {
304		*work += dryPostWork
305		if p.C.Sub(c).LenCmp(p.R+rad+dryGap) < 0 {
306			return false
307		}
308	}
309	for _, z := range f.Zones {
310		*work += dryZoneTest(z)
311		if z.Kind != physics.Surface && z.Kind != physics.Slope && z.Contains(c) {
312			return false
313		}
314		if z.Kind == physics.Surface && z.Skin != "" && z.Contains(c) {
315			return false // not on the sand or the ice: a puddle is on the green
316		}
317	}
318	far, in := c.Add(physics.V(farX, farY)), false
319	for _, wl := range f.Walls {
320		if wl.Every > 0 {
321			continue
322		}
323		*work += dryWallWork
324		if c.Sub(wl.Seg.Closest(c)).LenCmp(rad+dryGap) < 0 {
325			return false
326		}
327		if wl.Seg.Crosses(c, far) {
328			in = !in
329		}
330	}
331	return in
332}
333
334const (
335	dryTee     = 2
336	dryGap     = 0.3
337	farX, farY = 1e4, 37
338)
339
340// WithWeather lays weather over a field: the sky's whole-board zones go under
341// its own (the sand stays sand in the rain), puddles and wet ice over them.
342// It may return f itself: do not mutate what it returns.
343func WithWeather(f *physics.Field, weather []physics.Zone) *physics.Field {
344	if len(weather) == 0 {
345		return f
346	}
347	under, over := []physics.Zone{}, []physics.Zone{}
348	for _, z := range weather {
349		switch z.Skin {
350		case Wind, Rain, Fog, Storm, Snow:
351			under = append(under, z)
352		default:
353			over = append(over, z)
354		}
355	}
356	g := *f
357	g.Zones = append(append(append(make([]physics.Zone, 0, len(f.Zones)+len(weather)), under...), f.Zones...), over...)
358	return &g
359}
360
361// WithZones is the field with zones put first (a puddle over the sand is the
362// puddle). It may return f itself: do not mutate what it returns.
363func WithZones(f *physics.Field, zones []physics.Zone) *physics.Field {
364	if len(zones) == 0 {
365		return f
366	}
367	g := *f
368	g.Zones = append(append(make([]physics.Zone, 0, len(f.Zones)+len(zones)), zones...), f.Zones...)
369	return &g
370}
371
372// WithExtras is the field plus one stroke's pieces, the hole's own left as
373// is. It may return f itself: do not mutate what it returns.
374func WithExtras(f *physics.Field, walls []physics.Wall, posts []physics.Post) *physics.Field {
375	if len(walls) == 0 && len(posts) == 0 {
376		return f
377	}
378	g := *f
379	g.Walls = append(append(make([]physics.Wall, 0, len(f.Walls)+len(walls)), f.Walls...), walls...)
380	g.Posts = append(append(make([]physics.Post, 0, len(f.Posts)+len(posts)), f.Posts...), posts...)
381	return &g
382}
383
384// Simple is a hole: its geometry, what Decode gives golf. Fields and methods
385// pair up (Strokes/Par, Tee/Start, Pin/Cup, Course/Field) because a field and
386// a method cannot share a name.
387type Simple struct {
388	W, H      int // the board, 1 to MaxBoard a side (Decode holds data to it, author.Fit sets it)
389	Title     string
390	Strokes   int // par: the strokes a good round takes (0 means 3)
391	Tee, Pin  physics.Vec2
392	Course    *physics.Field
393	Pulses    []Pulse // pieces that come and go with the stroke number
394	Substeps  int
395	CupRadius float64
396	// World is the hole's world (1 to MaxWorld letters a-z, as Decode holds
397	// it); Order is its place there, 0 to MaxOrder (a course hole 1 or more).
398	World string
399	Order float64
400	// Shelter caps the wind the hole is played in (0: the world's own).
401	Shelter float64
402}
403
404// ExtraZones is the zones the hole's pulses lay down at a stroke.
405func (h *Simple) ExtraZones(stroke int) []physics.Zone {
406	var zs []physics.Zone
407	for _, p := range h.Pulses {
408		if physics.There(stroke, p.Every, p.On, p.Phase) {
409			zs = append(zs, p.Zones...)
410		}
411	}
412	return zs
413}
414
415// Varies says whether the hole has pulses: pieces that come and go.
416func (h *Simple) Varies() bool { return len(h.Pulses) > 0 }
417
418// Extras is the walls and posts the hole's pulses add at a stroke.
419func (h *Simple) Extras(stroke int) ([]physics.Wall, []physics.Post) {
420	var ws []physics.Wall
421	var ps []physics.Post
422	for _, p := range h.Pulses {
423		if physics.There(stroke, p.Every, p.On, p.Phase) {
424			ws = append(ws, p.Walls...)
425			ps = append(ps, p.Posts...)
426		}
427	}
428	return ws, ps
429}
430
431// PreviewWith plays a stroke of a round in some weather, read-only, the timed
432// pieces starting at tick. A pulse piece that came back on the ball pushes it
433// out, never across the hole's own walls. A ball left where the next stroke
434// brings a hazard back returns to where this stroke started, as under a timed
435// hazard (else that hazard would cost a stroke for nothing).
436func (h *Simple) PreviewWith(ball physics.Vec2, angle, power float64, stroke, tick int, weather []physics.Zone) (physics.Shot, bool) {
437	ws, ps := h.Extras(stroke)
438	ball = physics.UnstickIn(ball, ws, ps, h.Course.Radius, h.Course.Walls)
439	f := WithZones(WithExtras(h.Course, ws, ps), h.ExtraZones(stroke))
440	f = physics.WithTick(WithWeather(f, weather), tick)
441	shot, holed := Sink(f.Step(ball, Launch(angle, power), h.Substeps), h.Pin, h.CupRadius, h.Course.Radius)
442	if !holed && len(shot.Path) > 0 {
443		for _, z := range h.ExtraZones(stroke + 1) {
444			if z.Kind == physics.Hazard && z.Contains(shot.Rest()) {
445				shot.Path = append(shot.Path, ball)
446				shot.Air = append(shot.Air, false)
447				shot.Cause = append(shot.Cause, '-')
448				break
449			}
450		}
451	}
452	return shot, holed
453}
454
455// Par is the strokes a good round takes: Strokes, 3 unless it says 1 to 19.
456func (h *Simple) Par() int {
457	if h.Strokes > 0 && h.Strokes < 20 {
458		return h.Strokes
459	}
460	return 3
461}
462
463// Start is the tee.
464func (h *Simple) Start() physics.Vec2 { return h.Tee }
465
466// Cup is where the cup is.
467func (h *Simple) Cup() physics.Vec2 { return h.Pin }
468
469// Field is the course as the physics sees it.
470func (h *Simple) Field() *physics.Field { return h.Course }
471
472// Kick turns a stroke's power (0 to 10) into the ball's speed per substep,
473// Kick·p^(3/4), so the roll (v²/2a) grows as p^(3/2). A full stroke rolls 44
474// on the usual green, a board's length. Calibrated to keep every hole's par.
475const Kick = 0.79
476
477// Launch is the ball's starting velocity for a stroke. Every hole should use
478// it, so that the same pull means the same shot on every course.
479func Launch(angle, power float64) physics.Vec2 {
480	if !(power > 0) {
481		return physics.Vec2{}
482	}
483	return physics.FromPolar(angle, Kick*math.Sqrt(power*math.Sqrt(power)))
484}
485
486// minDrop is the least a ball falls to drop: a smaller ball drops as one of
487// 0.5, else a point ball would drop into any cup at any speed.
488const minDrop = 0.5
489
490// Capture is the fastest a ball of radius ball crossing a cup of radius cup,
491// off its centre by off, can go and still drop, in board units per substep;
492// 0 when it cannot drop at all. Holmes's criterion (Am. J. Phys. 59, 1991):
493// (2·sqrt(R² − b²) − r) · sqrt(G / 2r).
494func Capture(cup, ball, off float64) float64 {
495	c := cup*cup - off*off
496	if !(c > 0) {
497		return 0
498	}
499	if !(ball >= minDrop) {
500		ball = minDrop
501	}
502	chord := 2*math.Sqrt(c) - ball
503	if !(chord > 0) {
504		return 0
505	}
506	return chord * math.Sqrt(physics.G/(2*ball))
507}
508
509// jumpStep is how much longer than the fastest drop a step must be for the
510// one after it to be the rest of a jump, not a slow ball.
511const jumpStep = 1.5
512
513// Sink decides whether a shot ends in the cup: it stops within radius of the
514// pin, or crosses the cup no faster than Capture allows a ball of radius
515// ball. When it drops, the path is cut there and ends on the pin.
516func Sink(shot physics.Shot, pin physics.Vec2, radius, ball float64) (physics.Shot, bool) {
517	p := shot.Path
518	top := Capture(radius, ball, 0) // over the middle: the fastest drop
519	for i := 0; i+1 < len(p); i++ {
520		a, d := p[i], p[i+1].Sub(p[i])
521		if d.LenCmp(top) > 0 {
522			continue // too fast, or a tunnel jump
523		}
524		if airborne(shot, i) || airborne(shot, i+1) {
525			continue // flying over the cup is not dropping into it
526		}
527		// a short step right after a long one is not a slow ball: it is the
528		// rest of a substep after a tunnel exit or a bounce
529		if i > 0 && p[i].Sub(p[i-1]).LenCmp(top*jumpStep) > 0 {
530			continue
531		}
532		// a step whose box is more than radius from the pin cannot drop
533		if b, r := p[i+1], radius+1e-9; (pin.X < a.X-r && pin.X < b.X-r) || (pin.X > a.X+r && pin.X > b.X+r) ||
534			(pin.Y < a.Y-r && pin.Y < b.Y-r) || (pin.Y > a.Y+r && pin.Y > b.Y+r) {
535			continue
536		}
537		l := d.Len()
538		t := 0.0
539		if l > 0 {
540			t = pin.Sub(a).Dot(d) / (l * l)
541			if t < 0 {
542				t = 0
543			} else if t > 1 {
544				t = 1
545			}
546		}
547		at := a.Add(d.Scale(t))
548		// the step's nearest point to the pin is where it crosses the cup
549		if off := at.Sub(pin).Len(); off <= radius && l <= Capture(radius, ball, off) {
550			cut := make([]physics.Vec2, 0, i+3)
551			cut = append(cut, p[:i+1]...)
552			shot.Path = append(cut, at, pin)
553			if len(shot.Air) > i {
554				shot.Air = append(append(make([]bool, 0, i+3), shot.Air[:i+1]...), false, false)
555			}
556			if len(shot.Cause) > i {
557				shot.Cause = append(append(make([]byte, 0, i+3), shot.Cause[:i+1]...), '-', '-')
558			}
559			return shot, true
560		}
561	}
562	if shot.Rest().Sub(pin).LenCmp(radius) <= 0 {
563		shot.Path = append(shot.Path, pin)
564		if shot.Air != nil {
565			shot.Air = append(shot.Air, false)
566		}
567		if shot.Cause != nil {
568			shot.Cause = append(shot.Cause, '-')
569		}
570		return shot, true
571	}
572	return shot, false
573}
574
575func airborne(s physics.Shot, i int) bool { return i < len(s.Air) && s.Air[i] }