render.gno
5.61 Kb · 159 lines
1package gnodice
2
3import (
4 "strconv"
5 "strings"
6)
7
8// Render affiche une page lisible sur gnoweb (le site officiel de gno.land).
9// - chemin "" : règles, banque et dernières parties
10// - chemin "player/<g1...>" : statistiques et historique d'un joueur
11// - chemin "game/<numéro>" : une partie, avec de quoi vérifier le tirage
12//
13// Le chemin est choisi par le visiteur : on ne l'affiche jamais tel quel, on
14// n'utilise qu'une adresse ou un numéro validés.
15func Render(path string) string {
16 switch {
17 case strings.HasPrefix(path, "player/"):
18 player := address(strings.TrimPrefix(path, "player/"))
19 if !player.IsValid() {
20 return "# GNO-DICE\n\nAdresse invalide.\n"
21 }
22 return renderPlayer(player)
23 case strings.HasPrefix(path, "game/"):
24 id, err := strconv.ParseInt(strings.TrimPrefix(path, "game/"), 10, 64)
25 if err != nil || getGame(id) == nil {
26 return "# GNO-DICE\n\nPartie introuvable.\n"
27 }
28 return renderGame(getGame(id))
29 }
30 return renderHome()
31}
32
33func renderHome() string {
34 var b strings.Builder
35 b.WriteString("# 🎲 GNO-DICE\n\n")
36 b.WriteString("Choisis un chiffre de 1 à 6, mise entre 1 et 10 GNOT. ")
37 b.WriteString("Si le dé tombe sur ton chiffre, tu gagnes **5 fois ta mise**. ")
38 b.WriteString("Un lancer toutes les 10 minutes.\n\n")
39 b.WriteString("Le jeu est **prouvablement équitable** : ton chiffre reste caché (empreinte SHA-256) pendant que le croupier tire le dé, ")
40 b.WriteString("puis il est dévoilé et vérifié par le contrat. Chaque tirage publie sa graine : `game/<numéro>` pour le vérifier.\n\n")
41
42 if paused {
43 b.WriteString("> ⏸️ Le jeu est actuellement en pause.\n\n")
44 }
45
46 b.WriteString("## Banque\n\n")
47 b.WriteString("- Solde : " + formatGnot(Bankroll()) + " GNOT (dont " + formatGnot(reserved) + " réservés aux parties en cours)\n")
48 b.WriteString("- Mise maximale acceptée en ce moment : " + formatGnot(MaxCoverableBet()) + " GNOT\n")
49 b.WriteString("- Parties : " + itoa64(totalGames) + " (" + strconv.Itoa(totalWins) + " gagnées, " + strconv.Itoa(openGames.Size()) + " en cours)\n")
50 b.WriteString("- Total misé : " + formatGnot(totalWagered) + " GNOT, total payé : " + formatGnot(totalPaid) + " GNOT\n\n")
51
52 b.WriteString("## Dernières parties\n\n")
53 writeGamesTable(&b, recent, true)
54 return b.String()
55}
56
57func renderPlayer(player address) string {
58 var b strings.Builder
59 b.WriteString("# 🎲 GNO-DICE : joueur\n\n")
60 b.WriteString("`" + player.String() + "`\n\n")
61
62 p := getPlayer(player)
63 if p == nil {
64 b.WriteString("Aucune partie jouée pour l'instant.\n")
65 return b.String()
66 }
67 b.WriteString("- Parties terminées : " + strconv.Itoa(p.Played) + " (" + strconv.Itoa(p.Wins) + " gagnées), en cours : " + strconv.Itoa(p.Open) + "\n")
68 b.WriteString("- Total misé : " + formatGnot(p.Wagered) + " GNOT, total gagné : " + formatGnot(p.Paid) + " GNOT\n\n")
69 b.WriteString("## Dernières parties\n\n")
70 writeGamesTable(&b, p.History, false)
71 return b.String()
72}
73
74func renderGame(g *Game) string {
75 var b strings.Builder
76 b.WriteString("# 🎲 GNO-DICE : partie n° " + itoa64(g.ID) + "\n\n")
77 b.WriteString("- Joueur : `" + g.Player.String() + "`\n")
78 b.WriteString("- Mise : " + formatGnot(g.Bet) + " GNOT, bloc " + itoa64(g.PlacedHeight) + "\n")
79 b.WriteString("- Statut : " + g.Status.String() + "\n")
80 b.WriteString("- Empreinte du chiffre : `" + g.Commitment + "`\n")
81 if g.Seed != "" {
82 b.WriteString("- Graine du croupier : `" + g.Seed + "`\n")
83 b.WriteString("- Dé : **" + strconv.Itoa(g.Roll) + "**\n")
84 }
85 if g.Guess != 0 {
86 b.WriteString("- Chiffre dévoilé : **" + strconv.Itoa(g.Guess) + "**\n")
87 }
88 if g.Payout > 0 {
89 b.WriteString("- Versé au joueur : " + formatGnot(g.Payout) + " GNOT\n")
90 }
91 b.WriteString("\n### Vérifier ce tirage\n\n")
92 b.WriteString("dé = 1 + (8 premiers octets de sha256(\"gnodice|v1|roll|\" + graine + \"|\" + empreinte + \"|\" + numéro)) mod 6\n")
93 return b.String()
94}
95
96func writeGamesTable(b *strings.Builder, ids []int64, withPlayer bool) {
97 if len(ids) == 0 {
98 b.WriteString("Aucune partie terminée pour l'instant.\n")
99 return
100 }
101 if withPlayer {
102 b.WriteString("| # | Bloc | Joueur | Mise | Chiffre | Dé | Résultat |\n")
103 b.WriteString("|---|---|---|---|---|---|---|\n")
104 } else {
105 b.WriteString("| # | Bloc | Mise | Chiffre | Dé | Résultat |\n")
106 b.WriteString("|---|---|---|---|---|---|\n")
107 }
108 for i := len(ids) - 1; i >= 0; i-- {
109 g := mustGetGame(ids[i])
110 b.WriteString("| [" + itoa64(g.ID) + "](:game/" + itoa64(g.ID) + ") | " + itoa64(g.PlacedHeight) + " | ")
111 if withPlayer {
112 b.WriteString(shortAddress(g.Player) + " | ")
113 }
114 b.WriteString(formatGnot(g.Bet) + " GNOT | " + digitOrDash(g.Guess) + " | " + digitOrDash(g.Roll) + " | ")
115 switch g.Status {
116 case StatusWon:
117 b.WriteString("✅ +" + formatGnot(g.Payout) + " GNOT |\n")
118 case StatusRefunded:
119 b.WriteString("↩️ remboursé |\n")
120 case StatusExpired:
121 b.WriteString("⌛ expiré |\n")
122 default:
123 b.WriteString("❌ perdu |\n")
124 }
125 }
126}
127
128func digitOrDash(v int) string {
129 if v == 0 {
130 return "-"
131 }
132 return strconv.Itoa(v)
133}
134
135// shortAddress raccourcit une adresse : g1abcd...wxyz
136func shortAddress(a address) string {
137 s := a.String()
138 if len(s) <= 12 {
139 return s
140 }
141 return s[:6] + "..." + s[len(s)-4:]
142}
143
144// formatGnot convertit des ugnot en GNOT lisibles : 1500000 -> "1.5"
145func formatGnot(ugnot int64) string {
146 sign := ""
147 if ugnot < 0 {
148 sign = "-"
149 ugnot = -ugnot
150 }
151 whole := ugnot / UgnotPerGnot
152 frac := ugnot % UgnotPerGnot
153 if frac == 0 {
154 return sign + itoa64(whole)
155 }
156 fracStr := itoa64(frac + UgnotPerGnot)[1:] // complète avec des zéros : 500000 -> "500000"
157 fracStr = strings.TrimRight(fracStr, "0")
158 return sign + itoa64(whole) + "." + fracStr
159}