config.gno
5.52 Kb · 200 lines
1// Package penalty holds everything about a penalty kick that is pure
2// computation: the constants a game is played with, the ten parameters of a
3// kick, how 32 bytes of entropy map onto them, the shootout rules, and
4// SimulateShot.
5//
6// It has no state and no realm dependencies, so every version of the game
7// realm agrees on these types, and a client can run the same code to check a
8// result. All quantities are integers: millimetres, mm/s, milliseconds,
9// per-mille (0..1000) and milli-radii (thousandths of the ball's radius), so
10// the outcome is identical on every node.
11package penalty
12
13import (
14 "errors"
15 "strconv"
16 "strings"
17)
18
19// Config is the set of keeper and ball constants a game is created with.
20type Config struct {
21 GoalWidth int64 // inside width, mm
22 GoalHeight int64 // inside height, mm
23
24 KeeperScale int64 // keeper size relative to a ~1.85 m keeper, per-mille
25 ReactionMin int64 // keeper reaction delay range, ms
26 ReactionMax int64
27 StarShare int64 // share of the keeper's speed range that means "star jump", per-mille
28 ReadShare int64 // share that means "wait and read the shot", per-mille
29 DiveMin int64 // committed dive speed range, mm/s
30 DiveMax int64
31 JumpMin int64 // keeper vertical launch speed range, mm/s (negative = collapse low)
32 JumpMax int64
33
34 BallMinSpeed int64 // ball speed at 0% power, mm/s
35 BallMaxSpeed int64 // ball speed at 100% power, mm/s
36 FuzzBase int64 // std-dev of the mishit at 0% power, milli-radii
37 FuzzPower int64 // extra std-dev at 100% power, milli-radii
38 MaxContact int64 // farthest contact offset a taker may choose, milli-radii
39}
40
41// DefaultConfig returns the constants the reference simulation is tuned for.
42func DefaultConfig() Config {
43 return Config{
44 GoalWidth: 6555,
45 GoalHeight: 2424,
46 KeeperScale: 1176,
47 ReactionMin: 20,
48 ReactionMax: 120,
49 StarShare: 60,
50 ReadShare: 240, // as the browser engine (PLAN.READ_SHARE)
51 DiveMin: 2600,
52 DiveMax: 5200,
53 JumpMin: -1400,
54 JumpMax: 4000,
55 BallMinSpeed: 10000,
56 BallMaxSpeed: 34000,
57 FuzzBase: 30,
58 FuzzPower: 100,
59 MaxContact: 500,
60 }
61}
62
63type bound struct {
64 name string
65 lo, hi int64
66}
67
68// The range each constant may be set to, so a game cannot be created with
69// physics the simulation was never meant to handle. Order matches field().
70var bounds = []bound{
71 {"goalWidth", 5000, 7320},
72 {"goalHeight", 1800, 2440},
73 {"keeperScale", 900, 1300},
74 {"reactionMin", 0, 300},
75 {"reactionMax", 0, 400},
76 {"starShare", 0, 300},
77 {"readShare", 0, 700},
78 {"diveMin", 1500, 7000},
79 {"diveMax", 1500, 7000},
80 {"jumpMin", -2500, 0},
81 {"jumpMax", 0, 5000},
82 {"ballMinSpeed", 5000, 40000},
83 {"ballMaxSpeed", 5000, 40000},
84 {"fuzzBase", 0, 150},
85 {"fuzzPower", 0, 300},
86 {"maxContact", 200, 800},
87}
88
89func (c *Config) field(i int) *int64 {
90 switch i {
91 case 0:
92 return &c.GoalWidth
93 case 1:
94 return &c.GoalHeight
95 case 2:
96 return &c.KeeperScale
97 case 3:
98 return &c.ReactionMin
99 case 4:
100 return &c.ReactionMax
101 case 5:
102 return &c.StarShare
103 case 6:
104 return &c.ReadShare
105 case 7:
106 return &c.DiveMin
107 case 8:
108 return &c.DiveMax
109 case 9:
110 return &c.JumpMin
111 case 10:
112 return &c.JumpMax
113 case 11:
114 return &c.BallMinSpeed
115 case 12:
116 return &c.BallMaxSpeed
117 case 13:
118 return &c.FuzzBase
119 case 14:
120 return &c.FuzzPower
121 case 15:
122 return &c.MaxContact
123 }
124 panic("penalty: no config field " + strconv.Itoa(i))
125}
126
127var (
128 ErrConfigRange = errors.New("penalty: config value out of range")
129 ErrConfigOrder = errors.New("penalty: config range is inverted")
130 ErrConfigShares = errors.New("penalty: starShare + readShare must not exceed 900")
131 ErrConfigUnknown = errors.New("penalty: unknown config key")
132 ErrConfigSyntax = errors.New("penalty: config must be key=value pairs separated by commas")
133)
134
135// Validate checks every constant against its allowed range.
136func (c Config) Validate() error {
137 for i, b := range bounds {
138 v := *c.field(i)
139 if v < b.lo || v > b.hi {
140 return errors.New(ErrConfigRange.Error() + ": " + b.name + "=" + strconv.FormatInt(v, 10) +
141 " (allowed " + strconv.FormatInt(b.lo, 10) + ".." + strconv.FormatInt(b.hi, 10) + ")")
142 }
143 }
144 if c.ReactionMin > c.ReactionMax || c.DiveMin > c.DiveMax ||
145 c.JumpMin > c.JumpMax || c.BallMinSpeed > c.BallMaxSpeed {
146 return ErrConfigOrder
147 }
148 if c.StarShare+c.ReadShare > 900 {
149 return ErrConfigShares
150 }
151 return nil
152}
153
154// ParseConfig applies "key=value,key=value" overrides to DefaultConfig and
155// validates the result. An empty string gives the defaults. Keys are the
156// lower-camel names String prints, e.g. "goalWidth=6400,reactionMax=150".
157func ParseConfig(s string) (Config, error) {
158 c := DefaultConfig()
159 s = strings.TrimSpace(s)
160 if s != "" {
161 for _, kv := range strings.Split(s, ",") {
162 parts := strings.Split(strings.TrimSpace(kv), "=")
163 if len(parts) != 2 {
164 return Config{}, ErrConfigSyntax
165 }
166 key := strings.TrimSpace(parts[0])
167 v, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
168 if err != nil {
169 return Config{}, ErrConfigSyntax
170 }
171 found := false
172 for i, b := range bounds {
173 if b.name == key {
174 *c.field(i) = v
175 found = true
176 break
177 }
178 }
179 if !found {
180 return Config{}, errors.New(ErrConfigUnknown.Error() + ": " + key)
181 }
182 }
183 }
184 if err := c.Validate(); err != nil {
185 return Config{}, err
186 }
187 return c, nil
188}
189
190// String prints the config in the form ParseConfig accepts.
191func (c Config) String() string {
192 var b strings.Builder
193 for i, bd := range bounds {
194 if i > 0 {
195 b.WriteString(",")
196 }
197 b.WriteString(bd.name + "=" + strconv.FormatInt(*c.field(i), 10))
198 }
199 return b.String()
200}