views.gno
6.69 Kb · 205 lines
1package shots
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/penalty/v0"
8 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/shootout/v0"
9)
10
11// ---------------------------------------------------------------- JSON
12
13func q(s string) string { return strconv.Quote(s) }
14
15func kv(k, v string) string { return q(k) + ":" + v }
16
17func obj(fields ...string) string { return "{" + strings.Join(fields, ",") + "}" }
18
19func gameJSON(g shootout.Game) string {
20 return obj(
21 kv("id", strconv.FormatUint(g.ID, 10)),
22 kv("creator", q(string(g.Creator))),
23 kv("status", q(g.Status.String())),
24 kv("pot", itoa(g.Pot)),
25 kv("entryFee", itoa(g.EntryFee)),
26 kv("timeoutBlocks", itoa(g.TimeoutBlocks)),
27 kv("config", q(g.Config.String())),
28 kv("sessions", strconv.Itoa(g.Sessions)),
29 kv("winner", q(string(g.Winner))),
30 kv("createdHeight", itoa(g.CreatedHeight)),
31 kv("closedHeight", itoa(g.ClosedHeight)),
32 )
33}
34
35func resultJSON(r penalty.Result) string {
36 return obj(
37 kv("outcome", q(r.Outcome.String())),
38 kv("x", itoa(r.X)),
39 kv("y", itoa(r.Y)),
40 kv("arrivalMs", itoa(r.ArrivalMS)),
41 kv("ballSpeed", itoa(r.BallSpeed)),
42 kv("mishitX", itoa(r.MishitX)),
43 kv("mishitY", itoa(r.MishitY)),
44 kv("keeper", q(r.Keeper.String())),
45 )
46}
47
48func shotJSON(s penalty.ShotInput) string {
49 return obj(kv("aimX", itoa(s.AimX)), kv("aimY", itoa(s.AimY)), kv("contactX", itoa(s.ContactX)),
50 kv("contactY", itoa(s.ContactY)), kv("power", itoa(s.Power)))
51}
52
53func planJSON(p penalty.KeeperPlan) string {
54 return obj(kv("direction", strconv.Itoa(int(p.Direction))), kv("speed", strconv.Itoa(int(p.Speed))),
55 kv("jump", strconv.Itoa(int(p.Jump))), kv("reaction", strconv.Itoa(int(p.Reaction))))
56}
57
58func kickJSON(k shootout.Kick) string {
59 return obj(
60 kv("round", strconv.Itoa(k.Round)),
61 kv("kicker", q(k.Kicker.String())),
62 kv("shot", shotJSON(k.Shot.Input())),
63 kv("mishit", q(penalty.MishitHex(k.Shot.Mishit))),
64 kv("plan", planJSON(k.Plan)),
65 kv("seed", q(k.Seed.Hex())),
66 kv("result", resultJSON(k.Result)),
67 kv("height", itoa(k.Height)),
68 kv("time", itoa(k.Time)),
69 )
70}
71
72func sessionJSON(s shootout.Session) string {
73 kicks := make([]string, len(s.Kicks))
74 for i, k := range s.Kicks {
75 kicks[i] = kickJSON(k)
76 }
77 sc := s.Score
78 return obj(
79 kv("gameId", strconv.FormatUint(s.GameID, 10)),
80 kv("no", strconv.Itoa(s.No)),
81 kv("player", q(string(s.Player))),
82 kv("status", q(s.Status.String())),
83 kv("paid", itoa(s.Paid)),
84 kv("score", obj(kv("playerKicks", strconv.Itoa(sc.PlayerKicks)), kv("playerGoals", strconv.Itoa(sc.PlayerGoals)),
85 kv("chainKicks", strconv.Itoa(sc.ChainKicks)), kv("chainGoals", strconv.Itoa(sc.ChainGoals)))),
86 kv("next", q(s.Next())),
87 kv("kicks", "["+strings.Join(kicks, ",")+"]"),
88 kv("joinHeight", itoa(s.JoinHeight)),
89 kv("lastActionHeight", itoa(s.LastActionHeight)),
90 kv("endHeight", itoa(s.EndHeight)),
91 )
92}
93
94// ---------------------------------------------------------------- Render
95
96// Render shows the games list, one game ("game/<id>") or one shootout
97// ("game/<id>/<session>").
98func Render(path string) string {
99 parts := strings.Split(strings.Trim(path, "/"), "/")
100 if len(parts) >= 2 && parts[0] == "game" {
101 id, err := strconv.ParseUint(parts[1], 10, 64)
102 if err != nil {
103 return "# Not found\n"
104 }
105 if len(parts) >= 3 {
106 no, err := strconv.Atoi(parts[2])
107 if err != nil {
108 return "# Not found\n"
109 }
110 return renderSession(id, no)
111 }
112 return renderGame(id)
113 }
114 return renderHome()
115}
116
117func ugnot(n int64) string {
118 whole := n / 1000000
119 frac := n % 1000000
120 if frac == 0 {
121 return itoa(whole) + " GNOT"
122 }
123 f := strconv.FormatInt(frac+1000000, 10)[1:]
124 return itoa(whole) + "." + strings.TrimRight(f, "0") + " GNOT"
125}
126
127func renderHome() string {
128 var b strings.Builder
129 b.WriteString("# Spot Kick\n\nA pay-to-play penalty shootout against the chain. ")
130 b.WriteString("Join an open game by paying its entry fee, then take five kicks and keep five. ")
131 b.WriteString("Beat the chain and the pot is yours.\n\n")
132 b.WriteString("| Game | Status | Pot | Entry | Challengers |\n|---|---|---:|---:|---:|\n")
133 n := 0
134 games.ReverseIterate("", "", func(_ string, v any) bool {
135 g := v.(*shootout.Game)
136 id := strconv.FormatUint(g.ID, 10)
137 b.WriteString("| [#" + id + "](:game/" + id + ") | " + g.Status.String() + " | " + ugnot(g.Pot) +
138 " | " + ugnot(g.EntryFee) + " | " + strconv.Itoa(g.Sessions) + " |\n")
139 n++
140 return n >= 50
141 })
142 if n == 0 {
143 b.WriteString("| _no games yet_ | | | | |\n")
144 }
145 b.WriteString("\nRules engine: ")
146 if p := proxy.LivePath(); p != "" {
147 b.WriteString("`" + p + "` — " + live().Describe() + "\n")
148 } else {
149 b.WriteString("_none accepted yet_\n")
150 }
151 return b.String()
152}
153
154func renderGame(id uint64) string {
155 g, ok := getGame(id)
156 if !ok {
157 return "# Game not found\n"
158 }
159 sid := strconv.FormatUint(id, 10)
160 var b strings.Builder
161 b.WriteString("# Game #" + sid + "\n\n")
162 b.WriteString("**" + g.Status.String() + "** · pot **" + ugnot(g.Pot) + "** · entry " + ugnot(g.EntryFee) +
163 " · timeout " + itoa(g.TimeoutBlocks) + " blocks\n\n")
164 b.WriteString("Created by `" + string(g.Creator) + "` at height " + itoa(g.CreatedHeight) + ".\n\n")
165 if g.Status == shootout.GameWon {
166 b.WriteString("Won by `" + string(g.Winner) + "` at height " + itoa(g.ClosedHeight) + ".\n\n")
167 }
168 b.WriteString("Constants: `" + g.Config.String() + "`\n\n")
169 if g.Sessions > 0 {
170 b.WriteString("## Challengers\n\n| # | Player | Status | Score |\n|---|---|---|---|\n")
171 for no := g.Sessions; no >= 1 && no > g.Sessions-50; no-- {
172 s, ok := getSession(id, no)
173 if !ok {
174 continue
175 }
176 sn := strconv.Itoa(no)
177 b.WriteString("| [" + sn + "](:game/" + sid + "/" + sn + ") | `" + string(s.Player) + "` | " +
178 s.Status.String() + " | " + score(s.Score) + " |\n")
179 }
180 }
181 return b.String()
182}
183
184func score(s penalty.Shootout) string {
185 return strconv.Itoa(s.PlayerGoals) + "/" + strconv.Itoa(s.PlayerKicks) + " – " +
186 strconv.Itoa(s.ChainGoals) + "/" + strconv.Itoa(s.ChainKicks)
187}
188
189func renderSession(id uint64, no int) string {
190 s, ok := getSession(id, no)
191 if !ok {
192 return "# Shootout not found\n"
193 }
194 var b strings.Builder
195 b.WriteString("# Game #" + strconv.FormatUint(id, 10) + " · challenger " + strconv.Itoa(no) + "\n\n")
196 b.WriteString("`" + string(s.Player) + "` · **" + s.Status.String() + "** · player " + score(s.Score) +
197 " chain · next: " + s.Next() + "\n\n")
198 b.WriteString("| Round | Kicker | Shot | Keeper plan | Outcome | At (mm) |\n|---|---|---|---|---|---|\n")
199 for _, k := range s.Kicks {
200 b.WriteString("| " + strconv.Itoa(k.Round) + " | " + k.Kicker.String() + " | " + k.Shot.Input().String() +
201 " | " + k.Plan.String() + " (" + k.Result.Keeper.String() + ") | **" + k.Result.Outcome.String() +
202 "** | " + itoa(k.Result.X) + ", " + itoa(k.Result.Y) + " |\n")
203 }
204 return b.String()
205}