sonotone.gno
5.41 Kb · 193 lines
1// Realm (smart contract) Gno pour Sonotone.
2// - Chaque wallet peut s'inscrire UNE fois avec un pseudo unique.
3// - Un joueur inscrit envoie son score uniquement quand il bat son record
4// (ça lui évite de payer des frais à chaque partie).
5// - Le jeu lit les données avec GetPlayerJSON / GetTopJSON (gratuit).
6// - Render() affiche le classement sur gnoweb (la page du realm sur gno.land).
7
8package sonotone
9
10import (
11 "strconv"
12 "strings"
13
14 "gno.land/p/nt/avl/v0"
15)
16
17// Score maximum accepté (protection basique contre les valeurs absurdes).
18const MaxScore = 1000000
19
20// Player = un joueur inscrit et son meilleur résultat.
21type Player struct {
22 Pseudo string
23 Address address
24 BestScore int
25 Level int // niveau max atteint lors du meilleur score
26 Instrument string // instrument utilisé lors du meilleur score
27}
28
29// Les variables globales d'un realm sont sauvegardées automatiquement
30// sur la blockchain après chaque transaction.
31var (
32 players avl.Tree // clé : adresse du wallet -> *Player
33 pseudos avl.Tree // clé : pseudo en minuscules -> adresse du wallet
34)
35
36// ---------------------------------------------------------------------
37// Fonctions appelées par les joueurs (transactions signées avec Adena)
38// ---------------------------------------------------------------------
39
40// Register relie un pseudo au wallet qui signe la transaction.
41// Le premier paramètre "cur realm" est obligatoire pour qu'un utilisateur
42// puisse appeler la fonction et pour savoir QUI l'appelle.
43func Register(cur realm, pseudo string) {
44 caller := cur.Previous()
45 if !caller.IsUser() {
46 panic("seul un wallet peut s'inscrire")
47 }
48 key := caller.Address().String()
49
50 if players.Has(key) {
51 panic("ce wallet a déjà un pseudo")
52 }
53 checkPseudo(pseudo)
54
55 lower := strings.ToLower(pseudo)
56 if pseudos.Has(lower) {
57 panic("ce pseudo est déjà pris")
58 }
59
60 // On n'écrit qu'après toutes les vérifications.
61 players.Set(key, &Player{
62 Pseudo: pseudo,
63 Address: caller.Address(),
64 })
65 pseudos.Set(lower, key)
66}
67
68// SubmitScore enregistre un nouveau record pour le wallet qui signe.
69func SubmitScore(cur realm, score int, level int, instrument string) {
70 key := cur.Previous().Address().String()
71
72 v := players.Get(key)
73 if v == nil {
74 panic("inscris d'abord un pseudo")
75 }
76 if score <= 0 || score > MaxScore {
77 panic("score invalide")
78 }
79 if level < 1 || level > 1000 {
80 panic("niveau invalide")
81 }
82 if !validInstrument(instrument) {
83 panic("instrument inconnu")
84 }
85
86 p := v.(*Player)
87 if score <= p.BestScore {
88 panic("ce n'est pas un nouveau record")
89 }
90 p.BestScore = score
91 p.Level = level
92 p.Instrument = instrument
93}
94
95// ---------------------------------------------------------------------
96// Fonctions de lecture (gratuites, utilisées par le jeu)
97// ---------------------------------------------------------------------
98
99// GetPlayerJSON renvoie le profil d'une adresse en JSON, ou "null".
100func GetPlayerJSON(addr string) string {
101 v := players.Get(addr)
102 if v == nil {
103 return "null"
104 }
105 return playerToJSON(v.(*Player))
106}
107
108// GetTopJSON renvoie les n meilleurs joueurs (ayant un score) en JSON.
109func GetTopJSON(n int) string {
110 top := topPlayers(n)
111 out := "["
112 for i, p := range top {
113 if i > 0 {
114 out += ","
115 }
116 out += playerToJSON(p)
117 }
118 return out + "]"
119}
120
121// Render affiche le classement en Markdown sur gnoweb.
122func Render(_ string) string {
123 out := "# 🎧 Sonotone — Classement\n\n"
124 top := topPlayers(20)
125 if len(top) == 0 {
126 return out + "Aucun score pour l'instant.\n"
127 }
128 out += "| # | Pseudo | Score | Niveau | Instrument |\n"
129 out += "|---|--------|-------|--------|------------|\n"
130 for i, p := range top {
131 out += "| " + strconv.Itoa(i+1) +
132 " | " + p.Pseudo +
133 " | " + strconv.Itoa(p.BestScore) +
134 " | " + strconv.Itoa(p.Level) +
135 " | " + p.Instrument + " |\n"
136 }
137 return out
138}
139
140// ---------------------------------------------------------------------
141// Fonctions internes (minuscule = non accessibles de l'extérieur)
142// ---------------------------------------------------------------------
143
144// checkPseudo : 3 à 20 caractères, lettres, chiffres, _ et - uniquement.
145// (Ça évite aussi les caractères qui casseraient le JSON ou la page.)
146func checkPseudo(p string) {
147 if len(p) < 3 || len(p) > 20 {
148 panic("le pseudo doit faire entre 3 et 20 caractères")
149 }
150 for _, c := range p {
151 ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
152 (c >= '0' && c <= '9') || c == '_' || c == '-'
153 if !ok {
154 panic("pseudo : seulement lettres (sans accent), chiffres, _ et -")
155 }
156 }
157}
158
159func validInstrument(i string) bool {
160 return i == "piano" || i == "bass" || i == "guitar" || i == "organ"
161}
162
163func playerToJSON(p *Player) string {
164 return `{"pseudo":"` + p.Pseudo +
165 `","address":"` + p.Address.String() +
166 `","score":` + strconv.Itoa(p.BestScore) +
167 `,"level_max":` + strconv.Itoa(p.Level) +
168 `,"instrument":"` + p.Instrument + `"}`
169}
170
171// topPlayers trie les joueurs ayant un score, du plus grand au plus petit.
172func topPlayers(n int) []*Player {
173 var list []*Player
174 players.Iterate("", "", func(_ string, value any) bool {
175 p := value.(*Player)
176 if p.BestScore > 0 {
177 list = append(list, p)
178 }
179 return false // false = continuer à parcourir
180 })
181
182 // Tri par insertion : simple à lire, suffisant pour un petit jeu.
183 for i := 1; i < len(list); i++ {
184 for j := i; j > 0 && list[j].BestScore > list[j-1].BestScore; j-- {
185 list[j], list[j-1] = list[j-1], list[j]
186 }
187 }
188
189 if n > 0 && len(list) > n {
190 list = list[:n]
191 }
192 return list
193}