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

render.gno

8.19 Kb · 228 lines
  1package arena
  2
  3import (
  4	"strings"
  5	"time"
  6
  7	"gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0"
  8)
  9
 10// Render returns the state of the arenas as markdown, for gnoweb:
 11//
 12//	""            the open arenas and the last games played
 13//	"arena/<id>"  one arena: its rules, lobby and game, or its result
 14//	"earnings"    the top winners
 15//
 16// Everything printed here was validated against a narrow charset when it was
 17// written (see spec.gno and validate.gno), or is an address the chain gave
 18// the realm. The only caller-supplied value, the id of an arena page, is
 19// printed only if it is a valid id. Nothing needs markdown escaping.
 20func Render(path string) string {
 21	switch {
 22	case path == "":
 23		return renderHome()
 24	case path == "earnings":
 25		return renderEarnings()
 26	case strings.HasPrefix(path, "arena/"):
 27		id := strings.TrimPrefix(path, "arena/")
 28		if a := getArena(id); a != nil {
 29			return renderArena(id, a)
 30		}
 31		if r, ok := resultOf(id); ok {
 32			return renderResult(id, r)
 33		}
 34	}
 35	return "# gnofly arenas\n\nPage not found. Go back to the [overview](" + link("") + ").\n"
 36}
 37
 38// link returns the gnoweb URL of a Render path of this realm.
 39func link(path string) string {
 40	if path == "" {
 41		return webPath
 42	}
 43	return webPath + ":" + path
 44}
 45
 46func renderHome() string {
 47	var b strings.Builder
 48	b.WriteString("# gnofly arenas\n\n")
 49	b.WriteString("Paid games of gnofly, a multiplayer plane game, created by players at [gnofly.xyz/arena](https://gnofly.xyz/arena).\n")
 50	b.WriteString("A pilot pays the entry price to join, the realm keeps the stakes until the game is over, then credits them by the rule of its mode. Winners claim what they are owed with Claim.\n")
 51	b.WriteString("The house takes " + percent(feeBps) + " of what a settled game pays out.\n\n")
 52
 53	b.WriteString("## Open arenas\n\n")
 54	if arenas.Size() == 0 {
 55		b.WriteString("No arena has players right now.\n\n")
 56	} else {
 57		b.WriteString("| Arena | Mode | Entry | Seats | Lives | Time | Kill target | Pilots |\n|---|---|---|---|---|---|---|---|\n")
 58		arenas.Iterate("", "", func(id string, v any) bool {
 59			a := v.(*arena)
 60			pilots := itoa(int64(len(a.lobby))) + " waiting"
 61			if a.round != nil {
 62				pilots = itoa(int64(len(a.round.players))) + " playing"
 63			}
 64			b.WriteString("| [" + id[:8] + "](" + link("arena/"+id) + ") | " + a.mode + " | " + gnot(a.stake) +
 65				" | " + itoa(a.minSeats) + " to " + itoa(a.maxSeats) + " | " + itoa(a.lives) +
 66				" | " + itoa(a.seconds/60) + " min | " + targetCell(a.killTarget) + " | " + pilots + " |\n")
 67			return false
 68		})
 69		b.WriteByte('\n')
 70	}
 71
 72	b.WriteString("## Last games\n\n")
 73	if len(closed) == 0 {
 74		b.WriteString("No game is over yet.\n\n")
 75	} else {
 76		b.WriteString("| Arena | Mode | Outcome | Pilots | Pot | Fee |\n|---|---|---|---|---|---|\n")
 77		for i := len(closed) - 1; i >= 0 && i >= len(closed)-10; i-- {
 78			r, _ := resultOf(closed[i])
 79			b.WriteString("| [" + closed[i][:8] + "](" + link("arena/"+closed[i]) + ") | " + r.mode + " | " + r.outcome +
 80				" | " + itoa(r.players) + " | " + gnot(r.pot) + " | " + gnot(r.fee) + " |\n")
 81		}
 82		b.WriteByte('\n')
 83	}
 84
 85	b.WriteString("| | |\n|---|---|\n")
 86	b.WriteString("| In escrow | " + gnot(escrowed) + " |\n")
 87	b.WriteString("| Refund delay | " + itoa(refundDelay) + " s |\n\n")
 88	b.WriteString("- [Earnings](" + link("earnings") + "): " + itoa(int64(len(winners.addrs))) + " ranked pilots\n")
 89	return b.String()
 90}
 91
 92func renderArena(id string, a *arena) string {
 93	var b strings.Builder
 94	b.WriteString("# Arena " + id[:8] + "\n\n")
 95	b.WriteString("Mode `" + a.mode + "`: " + modeDoc(a.mode) + "\n\n")
 96
 97	b.WriteString("| | |\n|---|---|\n")
 98	b.WriteString("| Id | `" + id + "` |\n")
 99	b.WriteString("| Entry | " + gnot(a.stake) + " |\n")
100	b.WriteString("| Seats | " + itoa(a.minSeats) + " to " + itoa(a.maxSeats) + " |\n")
101	b.WriteString("| Lives | " + itoa(a.lives) + " |\n")
102	b.WriteString("| Game time | " + itoa(a.seconds/60) + " min |\n")
103	b.WriteString("| Kill target | " + targetCell(a.killTarget) + " |\n")
104	b.WriteString("| Created by | " + a.creator.String() + " |\n")
105	b.WriteString("| Takes entries until | " + clock(a.created+entryWindow) + " |\n\n")
106
107	if a.round == nil {
108		b.WriteString("## Lobby\n\n")
109		b.WriteString("Pilots who paid and wait for the game. They can leave with `Leave` until it starts.\n\n")
110		b.WriteString("| Seat | Pilot |\n|---|---|\n")
111		for i, player := range a.lobby {
112			b.WriteString("| " + itoa(int64(i+1)) + " | " + player.String() + " |\n")
113		}
114	} else {
115		r := a.round
116		b.WriteString("## Game\n\n")
117		b.WriteString("Started at " + clock(r.startedAt) + " with " + itoa(int64(len(r.players))) +
118			" pilots and a pot of " + gnot(a.stake*int64(len(r.players))) + ".\n")
119		b.WriteString("If the referee does not settle it, anyone can send the stakes back with `RefundRound` from " +
120			clock(r.startedAt+a.seconds+refundDelay) + ".\n\n")
121		b.WriteString("| Pilot |\n|---|\n")
122		for _, player := range r.players {
123			b.WriteString("| " + player.String() + " |\n")
124		}
125	}
126	b.WriteString("\n[Earnings](" + link("earnings") + ") | [Overview](" + link("") + ")\n")
127	return b.String()
128}
129
130func renderResult(id string, r result) string {
131	var b strings.Builder
132	b.WriteString("# Arena " + id[:8] + "\n\n")
133	b.WriteString("Mode `" + r.mode + "`, " + itoa(r.players) + " pilots, closed at " + clock(r.closedAt) + ": " + r.outcome + ".\n\n")
134	b.WriteString("| | |\n|---|---|\n")
135	b.WriteString("| Entry | " + gnot(r.stake) + " |\n")
136	b.WriteString("| Pot | " + gnot(r.pot) + " |\n")
137	b.WriteString("| Fee | " + gnot(r.fee) + " |\n")
138	b.WriteString("| Paid | " + paidCell(r) + " |\n")
139	b.WriteString("\n[Overview](" + link("") + ")\n")
140	return b.String()
141}
142
143// targetCell formats a kill target: most arenas end on the clock only.
144func targetCell(n int64) string {
145	if n == 0 {
146		return "none"
147	}
148	return itoa(n) + " kills"
149}
150
151// paidCell lists who was paid what in a round, for a table cell.
152func paidCell(r result) string {
153	if r.outcome != outcomeSettled {
154		return "every stake sent back"
155	}
156	if len(r.payouts) == 0 {
157		return "nobody"
158	}
159	parts := make([]string, len(r.payouts))
160	for i, p := range r.payouts {
161		parts[i] = p.Address.String() + " " + gnot(p.Amount)
162	}
163	return strings.Join(parts, ", ")
164}
165
166func renderEarnings() string {
167	var b strings.Builder
168	b.WriteString("# gnofly arena earnings\n\n")
169	b.WriteString("The top " + itoa(topSize) + " pilots by what they won above their own stakes, over every settled round.\n\n")
170	if len(winners.addrs) == 0 {
171		b.WriteString("Nobody won anything yet.\n")
172	} else {
173		b.WriteString("| Rank | Pilot | Won | Rounds |\n|---|---|---|---|\n")
174		for i, addr := range winners.addrs {
175			t := totalsOf(addr)
176			b.WriteString("| " + itoa(int64(i+1)) + " | " + addr.String() +
177				" | " + gnot(t.won) + " | " + itoa(t.rounds) + " |\n")
178		}
179	}
180	b.WriteString("\n[Back to the overview](" + link("") + ")\n")
181	return b.String()
182}
183
184// modeDoc is one sentence about the payout rule of a mode.
185func modeDoc(mode string) string {
186	switch mode {
187	case payout.ModeBattleRoyale:
188		return "the pot, less the fee, goes to the last pilot flying; when the clock runs out, to the pilot still in with the most kills."
189	case payout.ModeDeathmatch:
190		return "the pot, less the fee, goes to the top three by kills: 60%, 30% and 10%."
191	}
192	return "see the payout package."
193}
194
195// gnot formats an amount of ugnot in GNOT, without trailing zeros:
196// 10000000 gives "10 GNOT", 9500000 gives "9.5 GNOT", 1 gives "0.000001 GNOT".
197func gnot(ugnot int64) string {
198	whole, frac := ugnot/1000000, ugnot%1000000
199	if frac == 0 {
200		return itoa(whole) + " GNOT"
201	}
202	decimals := itoa(1000000 + frac)[1:] // all six of them, zeros included
203	end := len(decimals)
204	for decimals[end-1] == '0' {
205		end--
206	}
207	return itoa(whole) + "." + decimals[:end] + " GNOT"
208}
209
210// percent formats basis points as a percentage, without trailing zeros:
211// 500 gives "5%", 250 gives "2.5%", 1 gives "0.01%".
212func percent(bps int64) string {
213	whole, frac := bps/100, bps%100
214	switch {
215	case frac == 0:
216		return itoa(whole) + "%"
217	case frac%10 == 0:
218		return itoa(whole) + "." + itoa(frac/10) + "%"
219	case frac < 10:
220		return itoa(whole) + ".0" + itoa(frac) + "%"
221	}
222	return itoa(whole) + "." + itoa(frac) + "%"
223}
224
225// clock formats a block time in Unix seconds, in UTC.
226func clock(unix int64) string {
227	return time.Unix(unix, 0).UTC().Format("2006-01-02 15:04:05") + " UTC"
228}