gnodice.gno
7.46 Kb · 221 lines
1// Package gnodice est un jeu de dés décentralisé sur gno.land, « prouvablement
2// équitable » (provably fair).
3//
4// Règles du jeu :
5// - le joueur choisit un chiffre entre 1 et 6 et mise entre 1 et 10 GNOT ;
6// - si le dé tombe sur son chiffre, il reçoit 5 fois sa mise ;
7// - chaque joueur ne peut lancer le dé qu'une fois toutes les 10 minutes.
8//
9// POURQUOI TROIS ÉTAPES (sécurité) :
10// Sur une blockchain, tout est public et une transaction peut contenir
11// plusieurs messages exécutés « tout ou rien ». Si le dé était tiré pendant
12// Play, un tricheur pourrait ajouter un 2e message qui annule toute la
13// transaction quand il perd (ou simuler le tirage avant de miser) : il ne
14// garderait que ses victoires et viderait la banque. On sépare donc le jeu en
15// trois étapes, pour que PERSONNE ne puisse choisir le résultat en connaissant
16// les données de l'autre :
17//
18// 1. Play : le joueur mise et envoie son chiffre CACHÉ (une empreinte
19// SHA-256 de « chiffre + secret aléatoire + adresse »).
20// 2. Resolve : le croupier (un service du site) fournit une graine aléatoire
21// de 32 octets SANS connaître le chiffre. Le dé est calculé à
22// partir de la graine et de l'empreinte, et la graine est publiée.
23// 3. Reveal : on dévoile le chiffre et le secret. Le contrat vérifie
24// l'empreinte et paie si le chiffre est le bon.
25//
26// Garde-fous : si le croupier ne répond pas, le joueur récupère sa mise
27// (Refund) ; si le chiffre n'est jamais dévoilé, la partie est perdue (Expire).
28// La banque réserve à chaque mise le gain maximal possible : elle ne peut
29// jamais promettre plus qu'elle ne possède.
30package gnodice
31
32import (
33 "chain/runtime"
34 "time"
35
36 "gno.land/p/nt/avl/v0"
37)
38
39// Paramètres du jeu. 1 GNOT = 1 000 000 ugnot (la plus petite unité).
40const (
41 Denom = "ugnot"
42 UgnotPerGnot = int64(1_000_000)
43 MinBet = 1 * UgnotPerGnot // mise minimale : 1 GNOT
44 MaxBet = 10 * UgnotPerGnot // mise maximale : 10 GNOT
45 PayoutMultiplier = int64(5) // gain = 5 x la mise
46 CooldownSeconds = int64(10 * 60) // 10 minutes entre deux lancers
47 HistorySize = 10 // nombre de parties gardées par joueur
48 DieFaces = 6
49
50 // Délais (en secondes, heure des blocs)
51 ResolveTimeout = int64(30 * 60) // sans tirage au bout de 30 min : le joueur peut être remboursé
52 RevealWindow = int64(7 * 24 * 3600) // 7 jours pour dévoiler son chiffre après le tirage
53
54 // Limites par défaut (modifiables par le propriétaire avec SetLimits)
55 DefaultDailyPayoutLimit = 500 * UgnotPerGnot // au-delà, le jeu se met en pause tout seul
56 DefaultLowBankroll = 100 * UgnotPerGnot // en dessous, un événement d'alerte est émis
57
58 maxPageSize = 50 // nombre maximal de parties renvoyées par une lecture
59)
60
61// Status : étape d'une partie.
62type Status int
63
64const (
65 StatusPending Status = iota // mise reçue, en attente du tirage
66 StatusRolled // dé tiré, en attente du chiffre dévoilé
67 StatusWon // chiffre dévoilé : gagné et payé
68 StatusLost // chiffre dévoilé : perdu
69 StatusRefunded // croupier trop lent : mise rendue
70 StatusExpired // chiffre jamais dévoilé : perdu
71)
72
73func (s Status) String() string {
74 switch s {
75 case StatusPending:
76 return "pending"
77 case StatusRolled:
78 return "rolled"
79 case StatusWon:
80 return "won"
81 case StatusLost:
82 return "lost"
83 case StatusRefunded:
84 return "refunded"
85 case StatusExpired:
86 return "expired"
87 }
88 return "unknown"
89}
90
91// isOpen : la partie n'est pas terminée (sa réserve est encore bloquée).
92func (s Status) isOpen() bool {
93 return s == StatusPending || s == StatusRolled
94}
95
96// Game décrit une partie.
97type Game struct {
98 ID int64
99 Player address
100 Commitment string // empreinte du chiffre caché (64 caractères hexadécimaux)
101 Bet int64 // mise en ugnot
102 Status Status
103 Seed string // graine du croupier, publiée au tirage (vide avant)
104 Roll int // résultat du dé (0 avant le tirage)
105 Guess int // chiffre dévoilé (0 tant qu'il est caché)
106 Payout int64 // somme versée au joueur (gain, ou mise rendue)
107 PlacedAt int64 // heure du bloc de la mise (secondes Unix)
108 PlacedHeight int64
109 RolledAt int64
110 SettledAt int64
111}
112
113// Won : true si la partie est gagnée.
114func (g *Game) Won() bool { return g.Status == StatusWon }
115
116// reservation : ce que la banque bloque pour cette partie (le gain maximal).
117func (g *Game) reservation() int64 { return g.Bet * PayoutMultiplier }
118
119// playerData regroupe les informations d'un joueur.
120type playerData struct {
121 LastPlay int64 // heure de la dernière mise (secondes Unix)
122 Played int // parties terminées (gagnées, perdues ou expirées)
123 Wins int // victoires
124 Wagered int64 // total misé en ugnot
125 Paid int64 // total gagné en ugnot
126 History []int64 // numéros des HistorySize dernières parties (plus ancienne en premier)
127 Open int // parties en cours (non terminées)
128}
129
130var (
131 owner address // propriétaire : retraits, pause, réglages
132 pendingOwner address // propriétaire proposé, qui doit accepter (TransferOwnership en 2 temps)
133 croupier address // service autorisé à tirer les dés (Resolve)
134 realmAddr address // adresse du contrat = adresse de la banque
135 paused bool
136
137 games = avl.NewTree() // numéro (texte à zéros) -> *Game
138 openGames = avl.NewTree() // parties en cours, même clé -> true
139 players = avl.NewTree() // adresse -> *playerData
140 blocked = avl.NewTree() // adresses interdites de jeu -> true
141 recent []int64 // dernières parties, tous joueurs confondus
142
143 reserved int64 // somme des gains maximaux des parties en cours
144
145 totalGames int64 // parties créées (= dernier numéro)
146 totalWins int
147 totalLosses int
148 totalRefunded int
149 totalExpired int
150 totalWagered int64
151 totalPaid int64
152 totalFunded int64
153 totalWithdrawn int64
154
155 // Coupe-circuit : gains payés aujourd'hui (jour = heure du bloc / 86 400)
156 dailyPayoutLimit = DefaultDailyPayoutLimit
157 lowBankroll = DefaultLowBankroll
158 payoutDay int64
159 payoutToday int64
160)
161
162// init s'exécute une seule fois, au déploiement du contrat.
163// Celui qui déploie devient le propriétaire ET le croupier par défaut.
164func init(cur realm) {
165 owner = cur.Previous().Address()
166 croupier = owner
167 realmAddr = cur.Address()
168}
169
170// --- outils internes partagés ---
171
172func now() int64 { return time.Now().Unix() }
173
174func height() int64 { return runtime.ChainHeight() }
175
176// gameKey : clé de tri des parties (zéros devant pour garder l'ordre).
177func gameKey(id int64) string {
178 s := itoa64(id)
179 for len(s) < 12 {
180 s = "0" + s
181 }
182 return s
183}
184
185func getGame(id int64) *Game {
186 v := games.Get(gameKey(id))
187 if v == nil {
188 return nil
189 }
190 return v.(*Game)
191}
192
193func mustGetGame(id int64) *Game {
194 g := getGame(id)
195 if g == nil {
196 panic("gnodice: partie introuvable")
197 }
198 return g
199}
200
201func getPlayer(addr address) *playerData {
202 v := players.Get(addr.String())
203 if v == nil {
204 return nil
205 }
206 return v.(*playerData)
207}
208
209func isBlocked(addr address) bool {
210 return blocked.Has(addr.String())
211}
212
213// pushID ajoute un numéro de partie en ne gardant que les HistorySize derniers.
214func pushID(list []int64, id int64) []int64 {
215 if len(list) < HistorySize {
216 return append(list, id)
217 }
218 out := make([]int64, 0, HistorySize)
219 out = append(out, list[len(list)-HistorySize+1:]...)
220 return append(out, id)
221}