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.

json.gno

16.95 Kb · 470 lines
  1package gnoslinger
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"time"
  7
  8	"gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/engine/v0"
  9	profiles "gno.land/r/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/profiles/v0"
 10)
 11
 12// JSON views for the web client, read through Render("json/...") so the
 13// string comes back raw from vm/qrender. Built by hand: the shapes are fixed
 14// and small, and the chain has no encoding/json.
 15
 16const listLimit = 50
 17
 18// liveScan is how many of the newest games GamesJSON looks through for the
 19// ones being played: what the view costs stays bounded however many games
 20// there are.
 21const liveScan = 200
 22
 23// endedLimit is how many of the last finished games GamesJSON lists.
 24const endedLimit = 10
 25
 26// GameJSON is the full view of a game:
 27//
 28//	{"id","rev","phase","turn","maxTurns","timeout" (seconds),"board",
 29//	 "players":[{"addr" ("" for a seat nobody took yet),"name","char","hex" ("" until the seat set up),"facing","down","delay","status","committed","revealed","locked" (its timelocked plan, "" once revealed),"missed" (turns in a row it let time out),"playedFoot","playedRun","ranLastTurn",
 30//	             "aim","aimAt" (the seat the AIM markers follow, -1 when they sit on a hex or there is no aim),"aimHex" (the hex they sit on, "" when they follow a seat),"endurance","serious","gunArm","otherArm","leg",
 31//	             "guns":[{"id","type","name","location","cocked","shells","capacity","jammed","exploded"}]}]   (one entry per seat, 2 to 4),
 32//	 "ground":[{"id" (ground id, "g<id>" in a plan),"hex","guns":[{..}]}],
 33//	 "winner","vp":[victory points per seat],"awards":[{"p","reason","points"}] (rule 15, once a played game ends),
 34//	 "stake" (ugnot each player puts in, 0 for a free showdown),"pot" (the stakes paid so far),"payouts":[ugnot each seat got, [] until paid],"houseCut" (stakes.gno),"endReason",
 35//	 "createdAt","updatedAt","timeoutAt" (when the turn in progress can be claimed, 0 unless planning or revealing),
 36//	 "commitUntil" (the turn's deadline),"revealUntil" (when unrevealed seats can be claimed out),"lockRound" (the drand round the plans are timelocked to) (0 unless planning or revealing),
 37//	 "lastTurn":null|{"turn","seed","cards":[result card numbers drawn, in order],"plans":[..],
 38//	             "events":[{"seg","p","kind","action","from","to","facing","down","n","delay","reason","card","result","endurance","target","hit","range","gun","gunId","step","chart","status"}],
 39//	             "start":[{"hex","facing","down","delay","status","aim","aimAt","aimHex","endurance","serious","gunArm","otherArm","leg","guns":[..]},..],
 40//	             "ground":[the weapons on the ground at the turn's start, as "ground" above]}}
 41//
 42// Times are unix seconds; winner, events[].p and events[].target are seat indexes, -1 for none.
 43// The plans of the turn in progress are hidden until revealed: only their
 44// commits show, as flags and timelocked plans.
 45func GameJSON(id string) string {
 46	g := getGame(id)
 47	var b strings.Builder
 48	b.WriteString(`{"id":` + quote(g.ID))
 49	b.WriteString(`,"rev":` + strconv.Itoa(g.Rev))
 50	b.WriteString(`,"phase":` + quote(g.Phase.String()))
 51	b.WriteString(`,"turn":` + strconv.Itoa(g.Turn))
 52	b.WriteString(`,"maxTurns":` + strconv.Itoa(g.MaxTurns))
 53	b.WriteString(`,"timeout":` + strconv.FormatInt(int64(g.Timeout/time.Second), 10))
 54	b.WriteString(`,"board":"A"`)
 55	b.WriteString(`,"players":[`)
 56	for i := range g.Players {
 57		if i > 0 {
 58			b.WriteByte(',')
 59		}
 60		writePlayer(&b, g, i)
 61	}
 62	b.WriteByte(']')
 63	writeGround(&b, g.State.Ground)
 64	b.WriteString(`,"winner":` + strconv.Itoa(g.Winner))
 65	writeVP(&b, g)
 66	b.WriteString(`,"awards":[`)
 67	for i, a := range g.Awards {
 68		if i > 0 {
 69			b.WriteByte(',')
 70		}
 71		b.WriteString(`{"p":` + strconv.Itoa(a.Player) + `,"reason":` + quote(a.Reason) + `,"points":` + strconv.Itoa(a.Points) + `}`)
 72	}
 73	b.WriteByte(']')
 74	writeStakes(&b, g)
 75	b.WriteString(`,"endReason":` + quote(g.EndReason))
 76	b.WriteString(`,"createdAt":` + strconv.FormatInt(g.CreatedAt.Unix(), 10))
 77	b.WriteString(`,"updatedAt":` + strconv.FormatInt(g.UpdatedAt.Unix(), 10))
 78	writeTimeoutAt(&b, g)
 79	commitUntil, revealUntil, lockRound := int64(0), int64(0), int64(0)
 80	if g.Phase == PhasePlanning || g.Phase == PhaseRevealing {
 81		commitUntil, revealUntil, lockRound = g.commitUntil().Unix(), g.revealUntil().Unix(), g.lockRound()
 82	}
 83	b.WriteString(`,"commitUntil":` + strconv.FormatInt(commitUntil, 10))
 84	b.WriteString(`,"revealUntil":` + strconv.FormatInt(revealUntil, 10))
 85	b.WriteString(`,"lockRound":` + strconv.FormatInt(lockRound, 10))
 86	b.WriteString(`,"lastTurn":`)
 87	writeTurnResult(&b, g.lastTurn())
 88	b.WriteByte('}')
 89	return b.String()
 90}
 91
 92// PlanJSON is where an address's plan for the turn in progress stands; the
 93// plan itself shows once revealed:
 94//
 95//	{"id","turn","seat" (-1 when addr is not a player),"committed","revealed","plan" ("" until revealed)}
 96func PlanJSON(id, addr string) string {
 97	g := getGame(id)
 98	seat := g.seat(address(addr))
 99	var b strings.Builder
100	b.WriteString(`{"id":` + quote(g.ID))
101	b.WriteString(`,"turn":` + strconv.Itoa(g.Turn))
102	b.WriteString(`,"seat":` + strconv.Itoa(seat))
103	committed, revealed, plan := false, false, ""
104	if seat >= 0 {
105		committed, revealed, plan = g.committed(seat), g.Revealed[seat], g.Plans[seat]
106	}
107	b.WriteString(`,"committed":` + strconv.FormatBool(committed))
108	b.WriteString(`,"revealed":` + strconv.FormatBool(revealed))
109	b.WriteString(`,"plan":` + quote(plan))
110	b.WriteByte('}')
111	return b.String()
112}
113
114// TurnJSON is one resolved turn of a game (from 1), the turn result as
115// json/game writes its last one: what replays read, a turn per query (the
116// "json/history/{id}/{turn}" view), so that what a query costs does not
117// grow with the game. Each turn is self-contained (its start and ground).
118func TurnJSON(id string, turn int) string {
119	g := getGame(id)
120	if turn < 1 || turn > len(g.Turns) {
121		panic("gnoslinger: turn " + strconv.Itoa(turn) + " is not resolved")
122	}
123	var b strings.Builder
124	writeTurnResult(&b, g.Turns[turn-1])
125	return b.String()
126}
127
128// HistoryJSON is every resolved turn of a game, oldest first, for tests and
129// tools (the tutorial's showdown, make gen-tutorial); no view serves it: a
130// long game's history may cost more than a query can.
131//
132//	{"id","turn","phase","players":[{"addr","char"}],"turns":[turn result...]}
133func HistoryJSON(id string) string {
134	g := getGame(id)
135	var b strings.Builder
136	b.WriteString(`{"id":` + quote(g.ID))
137	b.WriteString(`,"turn":` + strconv.Itoa(g.Turn))
138	b.WriteString(`,"phase":` + quote(g.Phase.String()))
139	b.WriteString(`,"players":[`)
140	for i, p := range g.Players {
141		if i > 0 {
142			b.WriteByte(',')
143		}
144		b.WriteString(`{"addr":` + quote(p.Addr.String()) + `,"name":` + quote(profiles.Name(p.Addr)) + `,"char":` + quote(p.Char) + `}`)
145	}
146	b.WriteString(`],"turns":[`)
147	for i, r := range g.Turns {
148		if i > 0 {
149			b.WriteByte(',')
150		}
151		writeTurnResult(&b, r)
152	}
153	b.WriteString("]}")
154	return b.String()
155}
156
157func writePlayer(b *strings.Builder, g *Game, i int) {
158	p, c := g.Players[i], g.State.Chars[i]
159	b.WriteString(`{"addr":` + quote(p.Addr.String()))
160	b.WriteString(`,"name":` + quote(profiles.Name(p.Addr)))
161	b.WriteString(`,"char":` + quote(p.Char))
162	b.WriteString(`,"hex":` + quote(c.Hex))
163	b.WriteString(`,"facing":` + strconv.Itoa(int(c.Facing)))
164	b.WriteString(`,"down":` + strconv.FormatBool(c.Down))
165	b.WriteString(`,"delay":` + strconv.Itoa(c.Delay))
166	b.WriteString(`,"status":` + quote(c.Status.String()))
167	b.WriteString(`,"committed":` + strconv.FormatBool(g.committed(i)))
168	b.WriteString(`,"revealed":` + strconv.FormatBool(g.Revealed[i]))
169	b.WriteString(`,"locked":` + quote(g.Commits[i].Locked))
170	b.WriteString(`,"missed":` + strconv.Itoa(g.Missed[i]))
171	b.WriteString(`,"playedFoot":` + strconv.FormatBool(c.PlayedFoot))
172	b.WriteString(`,"playedRun":` + strconv.FormatBool(c.PlayedRun))
173	b.WriteString(`,"ranLastTurn":` + strconv.FormatBool(c.RanLastTurn))
174	writeWounds(b, c)
175	writeGuns(b, c.Guns)
176	b.WriteByte('}')
177}
178
179// writeWounds writes the aim and wound members of a character.
180func writeWounds(b *strings.Builder, c engine.Character) {
181	b.WriteString(`,"aim":` + strconv.Itoa(c.Aim))
182	aimAt := engine.NoSeat
183	if c.Aim > 0 {
184		aimAt = c.AimAt
185	}
186	b.WriteString(`,"aimAt":` + strconv.Itoa(aimAt))
187	b.WriteString(`,"aimHex":` + quote(c.AimHex))
188	b.WriteString(`,"endurance":` + strconv.Itoa(c.Endurance))
189	b.WriteString(`,"serious":` + strconv.Itoa(c.Serious))
190	b.WriteString(`,"gunArm":` + strconv.Itoa(c.GunArm))
191	b.WriteString(`,"otherArm":` + strconv.Itoa(c.OtherArm))
192	b.WriteString(`,"leg":` + strconv.Itoa(c.Leg))
193}
194
195// writeTimeoutAt writes the ,"timeoutAt" member: when the turn in progress
196// can be claimed, 0 unless the game is planning or revealing.
197func writeTimeoutAt(b *strings.Builder, g *Game) {
198	timeoutAt := int64(0)
199	if g.Phase == PhasePlanning || g.Phase == PhaseRevealing {
200		timeoutAt = g.timeoutAt().Unix()
201	}
202	b.WriteString(`,"timeoutAt":` + strconv.FormatInt(timeoutAt, 10))
203}
204
205// writeGround writes the ,"ground":[...] member: the weapons lying in hexes.
206func writeGround(b *strings.Builder, ground []engine.GroundGun) {
207	b.WriteString(`,"ground":[`)
208	for i, gg := range ground {
209		if i > 0 {
210			b.WriteByte(',')
211		}
212		b.WriteString(`{"id":` + strconv.Itoa(gg.ID))
213		b.WriteString(`,"hex":` + quote(gg.Hex))
214		writeGuns(b, engine.Guns{gg.Gun})
215		b.WriteByte('}')
216	}
217	b.WriteByte(']')
218}
219
220// writeGuns writes the ,"guns":[...] member of a character.
221func writeGuns(b *strings.Builder, guns engine.Guns) {
222	b.WriteString(`,"guns":[`)
223	n := 0
224	for _, g := range guns {
225		if g.Type == engine.NoGun {
226			continue
227		}
228		if n > 0 {
229			b.WriteByte(',')
230		}
231		n++
232		spec := g.Type.Spec()
233		b.WriteString(`{"id":` + strconv.Itoa(g.ID))
234		b.WriteString(`,"type":` + quote(string(g.Type)))
235		b.WriteString(`,"name":` + quote(spec.Name))
236		b.WriteString(`,"location":` + quote(g.Location.String()))
237		b.WriteString(`,"cocked":` + strconv.FormatBool(g.Cocked))
238		b.WriteString(`,"shells":` + strconv.Itoa(g.Shells))
239		b.WriteString(`,"capacity":` + strconv.Itoa(spec.Capacity))
240		b.WriteString(`,"jammed":` + strconv.FormatBool(g.Jammed))
241		b.WriteString(`,"exploded":` + strconv.FormatBool(g.Exploded))
242		b.WriteByte('}')
243	}
244	b.WriteByte(']')
245}
246
247func writeTurnResult(b *strings.Builder, r *TurnResult) {
248	if r == nil {
249		b.WriteString("null")
250		return
251	}
252	b.WriteString(`{"turn":` + strconv.Itoa(r.Turn))
253	b.WriteString(`,"seed":` + quote(r.Seed))
254	b.WriteString(`,"cards":[`)
255	for i, n := range r.Cards() {
256		if i > 0 {
257			b.WriteByte(',')
258		}
259		b.WriteString(strconv.Itoa(n))
260	}
261	b.WriteString(`],"plans":[`)
262	for i, p := range r.Plans {
263		if i > 0 {
264			b.WriteByte(',')
265		}
266		b.WriteString(quote(p))
267	}
268	b.WriteString(`],"events":[`)
269	for i, e := range r.Events() {
270		if i > 0 {
271			b.WriteByte(',')
272		}
273		writeEvent(b, e)
274	}
275	b.WriteString(`],"start":[`)
276	for i, c := range r.Start() {
277		if i > 0 {
278			b.WriteByte(',')
279		}
280		b.WriteString(`{"hex":` + quote(c.Hex))
281		b.WriteString(`,"facing":` + strconv.Itoa(int(c.Facing)))
282		b.WriteString(`,"down":` + strconv.FormatBool(c.Down))
283		b.WriteString(`,"delay":` + strconv.Itoa(c.Delay))
284		b.WriteString(`,"status":` + quote(c.Status.String()))
285		writeWounds(b, c)
286		writeGuns(b, c.Guns)
287		b.WriteByte('}')
288	}
289	b.WriteByte(']')
290	writeGround(b, r.Ground())
291	b.WriteByte('}')
292}
293
294func writeEvent(b *strings.Builder, e engine.Event) {
295	b.WriteString(`{"seg":` + strconv.Itoa(e.Seg))
296	b.WriteString(`,"p":` + strconv.Itoa(e.Player))
297	b.WriteString(`,"kind":` + quote(e.Kind))
298	b.WriteString(`,"action":` + quote(string(e.Action)))
299	b.WriteString(`,"from":` + quote(e.From))
300	b.WriteString(`,"to":` + quote(e.To))
301	b.WriteString(`,"facing":` + strconv.Itoa(int(e.Facing)))
302	b.WriteString(`,"down":` + strconv.FormatBool(e.Down))
303	b.WriteString(`,"n":` + strconv.Itoa(e.N))
304	b.WriteString(`,"delay":` + strconv.Itoa(e.Delay))
305	b.WriteString(`,"reason":` + quote(e.Reason))
306	b.WriteString(`,"card":` + strconv.Itoa(e.Card))
307	b.WriteString(`,"result":` + quote(e.Result))
308	b.WriteString(`,"endurance":` + strconv.Itoa(e.Endurance))
309	b.WriteString(`,"target":` + strconv.Itoa(e.Target))
310	b.WriteString(`,"hit":` + quote(e.Hit))
311	b.WriteString(`,"range":` + strconv.Itoa(e.Range))
312	b.WriteString(`,"gun":` + quote(string(e.Gun)))
313	b.WriteString(`,"gunId":` + strconv.Itoa(e.GunID))
314	b.WriteString(`,"step":` + strconv.Itoa(e.Step))
315	b.WriteString(`,"chart":` + quote(e.Chart))
316	b.WriteString(`,"status":` + quote(e.Status))
317	b.WriteByte('}')
318}
319
320// GamesJSON lists, for an address, the games with a free seat, the games
321// being played (live: planning or revealing, for anyone to watch), the
322// last endedLimit games finished (ended, for anyone to replay) and the
323// games it plays or played, newest first (at most 50 each), with the
324// address's registered name ("" if none). live and ended are found among
325// the liveScan newest games, a running game being recent: a turn times
326// out within days.
327//
328//	{"name","open":[summary...],"live":[summary...],"ended":[summary...],"mine":[summary...]}
329//	summary = {"id","rev","phase","turn","maxTurns","timeout","winner","endReason","vp","stake","pot","payouts","houseCut","createdAt","updatedAt","timeoutAt","lockRound","players":[{"addr","name","char","status","committed","revealed"}]}
330//
331// lockRound (0 unless planning or revealing) tells the lobby when the
332// timelocked plans of a seat committed and not revealed can be opened.
333//
334// players has one entry per seat, "addr" "" for a seat nobody took yet.
335func GamesJSON(addr string) string {
336	var b strings.Builder
337	b.WriteString(`{"name":` + quote(profiles.Name(address(addr))))
338	b.WriteString(`,"open":[`)
339	n := 0
340	openGames.ReverseIterate("", "", func(_ string, v any) bool {
341		if n > 0 {
342			b.WriteByte(',')
343		}
344		writeSummary(&b, v.(*Game))
345		n++
346		return n >= listLimit
347	})
348	var live, ended strings.Builder
349	nLive, nEnded, scanned := 0, 0, 0
350	games.ReverseIterate("", "", func(_ string, v any) bool {
351		scanned++
352		g := v.(*Game)
353		switch {
354		case (g.Phase == PhasePlanning || g.Phase == PhaseRevealing) && nLive < listLimit:
355			if nLive > 0 {
356				live.WriteByte(',')
357			}
358			writeSummary(&live, g)
359			nLive++
360		case g.Phase == PhaseFinished && nEnded < endedLimit:
361			if nEnded > 0 {
362				ended.WriteByte(',')
363			}
364			writeSummary(&ended, g)
365			nEnded++
366		}
367		return (nLive >= listLimit && nEnded >= endedLimit) || scanned >= liveScan
368	})
369	b.WriteString(`],"live":[` + live.String() + `],"ended":[` + ended.String() + `],"mine":[`)
370	if v := byPlayer.Get(addr); v != nil {
371		for i, id := range v.([]string) {
372			if i >= listLimit {
373				break
374			}
375			if i > 0 {
376				b.WriteByte(',')
377			}
378			writeSummary(&b, games.Get(id).(*Game))
379		}
380	}
381	b.WriteString("]}")
382	return b.String()
383}
384
385func writeSummary(b *strings.Builder, g *Game) {
386	b.WriteString(`{"id":` + quote(g.ID))
387	b.WriteString(`,"rev":` + strconv.Itoa(g.Rev))
388	b.WriteString(`,"phase":` + quote(g.Phase.String()))
389	b.WriteString(`,"turn":` + strconv.Itoa(g.Turn))
390	b.WriteString(`,"maxTurns":` + strconv.Itoa(g.MaxTurns))
391	b.WriteString(`,"timeout":` + strconv.FormatInt(int64(g.Timeout/time.Second), 10))
392	b.WriteString(`,"winner":` + strconv.Itoa(g.Winner))
393	writeVP(b, g)
394	writeStakes(b, g)
395	b.WriteString(`,"endReason":` + quote(g.EndReason))
396	b.WriteString(`,"createdAt":` + strconv.FormatInt(g.CreatedAt.Unix(), 10))
397	b.WriteString(`,"updatedAt":` + strconv.FormatInt(g.UpdatedAt.Unix(), 10))
398	writeTimeoutAt(b, g)
399	lockRound := int64(0)
400	if g.Phase == PhasePlanning || g.Phase == PhaseRevealing {
401		lockRound = g.lockRound()
402	}
403	b.WriteString(`,"lockRound":` + strconv.FormatInt(lockRound, 10))
404	b.WriteString(`,"players":[`)
405	for i, p := range g.Players {
406		if i > 0 {
407			b.WriteByte(',')
408		}
409		b.WriteString(`{"addr":` + quote(p.Addr.String()) + `,"name":` + quote(profiles.Name(p.Addr)) + `,"char":` + quote(p.Char) +
410			`,"status":` + quote(g.State.Chars[i].Status.String()) + `,"committed":` + strconv.FormatBool(g.committed(i)) + `,"revealed":` + strconv.FormatBool(g.Revealed[i]) + `}`)
411	}
412	b.WriteString("]}")
413}
414
415// writeVP writes the victory points of each seat.
416func writeVP(b *strings.Builder, g *Game) {
417	b.WriteString(`,"vp":[`)
418	for i, v := range g.VP {
419		if i > 0 {
420			b.WriteByte(',')
421		}
422		b.WriteString(strconv.Itoa(v))
423	}
424	b.WriteByte(']')
425}
426
427// writeStakes writes the stake, the pot and, once paid, what each seat got.
428func writeStakes(b *strings.Builder, g *Game) {
429	b.WriteString(`,"stake":` + strconv.FormatInt(g.Stake, 10))
430	b.WriteString(`,"pot":` + strconv.FormatInt(g.Pot, 10))
431	b.WriteString(`,"payouts":[`)
432	for i, v := range g.Payouts {
433		if i > 0 {
434			b.WriteByte(',')
435		}
436		b.WriteString(strconv.FormatInt(v, 10))
437	}
438	b.WriteString(`],"houseCut":` + strconv.FormatInt(g.HouseCut, 10))
439}
440
441// quote encodes s as a JSON string.
442func quote(s string) string {
443	var b strings.Builder
444	b.WriteByte('"')
445	for i := 0; i < len(s); i++ {
446		c := s[i]
447		switch {
448		case c == '"' || c == '\\':
449			b.WriteByte('\\')
450			b.WriteByte(c)
451		case c == '\n':
452			b.WriteString(`\n`)
453		case c == '\r':
454			b.WriteString(`\r`)
455		case c == '\t':
456			b.WriteString(`\t`)
457		case c < 0x20:
458			b.WriteString(`\u00`)
459			b.WriteByte("0123456789abcdef"[c>>4])
460			b.WriteByte("0123456789abcdef"[c&0xf])
461		default:
462			b.WriteByte(c)
463		}
464	}
465	b.WriteByte('"')
466	return b.String()
467}
468
469// GameRev is the cheapest way to poll a game for changes.
470func GameRev(id string) int { return getGame(id).Rev }