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

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}