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

game.gno

8.15 Kb · 281 lines
  1package gnodice
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime/unsafe"
  7	"strconv"
  8)
  9
 10// Les étapes d'une partie : Play -> Resolve -> Reveal, avec deux sorties de
 11// secours, Refund (croupier trop lent) et Expire (chiffre jamais dévoilé).
 12// Règle d'or suivie partout : on vérifie, puis on met à jour l'état, et on
 13// envoie l'argent en tout dernier (contrôles -> effets -> paiement).
 14
 15// Play mise sur un chiffre caché. Le joueur envoie sa mise (1 à 10 GNOT) avec
 16// la transaction et l'empreinte de son chiffre (voir Commitment).
 17// Renvoie le numéro de la partie, par exemple "id=42".
 18func Play(cur realm, commitment string) string {
 19	caller := cur.Previous()
 20
 21	// La mise est lue dans « l'enveloppe » de la transaction (OriginSend) :
 22	// ce n'est fiable que pour un appel direct depuis un wallet (MsgCall).
 23	// Un script (MsgRun) ou un autre contrat pourrait détourner ces GNOT.
 24	if !caller.IsUserCall() {
 25		panic("gnodice: Play doit être appelé directement depuis un wallet")
 26	}
 27	player := caller.Address()
 28	if paused {
 29		panic("gnodice: le jeu est en pause")
 30	}
 31	if isBlocked(player) {
 32		panic("gnodice: cette adresse n'est pas autorisée à jouer")
 33	}
 34	if !isHex64(commitment) {
 35		panic("gnodice: empreinte invalide (64 caractères hexadécimaux attendus)")
 36	}
 37	bet := receivedBet()
 38
 39	t := now()
 40	rollDay(t)
 41	if payoutToday >= dailyPayoutLimit {
 42		panic("gnodice: limite de gains du jour atteinte, réessaie demain")
 43	}
 44
 45	p := getPlayer(player)
 46	if wait := cooldownRemaining(p, t); wait > 0 {
 47		panic("gnodice: attends encore " + formatDuration(wait) + " avant de rejouer")
 48	}
 49
 50	// La banque (qui contient déjà la mise reçue) doit pouvoir couvrir le gain
 51	// maximal EN PLUS de tout ce qu'elle a déjà promis aux parties en cours.
 52	game := &Game{
 53		Player:       player,
 54		Commitment:   commitment,
 55		Bet:          bet,
 56		Status:       StatusPending,
 57		PlacedAt:     t,
 58		PlacedHeight: height(),
 59	}
 60	if available() < game.reservation() {
 61		panic("gnodice: la banque ne peut pas couvrir ce gain, essaie une mise plus petite")
 62	}
 63
 64	// Effets
 65	totalGames++
 66	game.ID = totalGames
 67	games.Set(gameKey(game.ID), game)
 68	openGames.Set(gameKey(game.ID), true)
 69	reserved += game.reservation()
 70
 71	if p == nil {
 72		p = &playerData{}
 73		players.Set(player.String(), p)
 74	}
 75	p.LastPlay = t
 76	p.Open++
 77	p.Wagered += bet
 78	totalWagered += bet
 79
 80	chain.Emit(
 81		"GamePlaced",
 82		"id", itoa64(game.ID),
 83		"player", player.String(),
 84		"bet", itoa64(bet),
 85		"commitment", commitment,
 86	)
 87	return "id=" + itoa64(game.ID)
 88}
 89
 90// Resolve tire le dé d'une partie en attente. Réservé au croupier, qui fournit
 91// une graine aléatoire de 32 octets (64 caractères hexadécimaux) sans connaître
 92// le chiffre du joueur. La graine est publiée : chacun peut refaire le calcul.
 93func Resolve(cur realm, id int64, seed string) {
 94	caller := cur.Previous()
 95	if !caller.IsUserCall() || caller.Address() != croupier {
 96		panic("gnodice: seul le croupier peut tirer les dés")
 97	}
 98	if !isHex64(seed) {
 99		panic("gnodice: graine invalide (64 caractères hexadécimaux attendus)")
100	}
101	g := mustGetGame(id)
102	if g.Status != StatusPending {
103		panic("gnodice: cette partie n'attend pas de tirage")
104	}
105	// Fenêtres disjointes : avant la limite, seul le tirage est possible ;
106	// après, seul le remboursement. Sinon un joueur qui voit passer un tirage
107	// tardif (et sa graine) pourrait le devancer avec Refund s'il a perdu.
108	if now() >= g.PlacedAt+ResolveTimeout {
109		panic("gnodice: délai de tirage dépassé, la partie doit être remboursée")
110	}
111
112	g.Seed = seed
113	g.Roll = rollFromSeed(seed, g.Commitment, g.ID)
114	g.Status = StatusRolled
115	g.RolledAt = now()
116
117	chain.Emit("DiceRolled", "id", itoa64(g.ID), "player", g.Player.String(), "roll", strconv.Itoa(g.Roll), "seed", seed)
118}
119
120// Reveal dévoile le chiffre caché et termine la partie. N'importe qui peut
121// l'appeler (le joueur ou le service du site) : le gain va toujours au joueur.
122// Renvoie le résultat, par exemple "roll=4;won=true;payout=50000000".
123func Reveal(cur realm, id int64, guess int, salt string) string {
124	g := mustGetGame(id)
125	if g.Status != StatusRolled {
126		panic("gnodice: cette partie ne peut pas être dévoilée maintenant")
127	}
128	if now() > g.RolledAt+RevealWindow {
129		panic("gnodice: délai dépassé, la partie a expiré")
130	}
131	if guess < 1 || guess > DieFaces {
132		panic("gnodice: le chiffre doit être entre 1 et 6")
133	}
134	if !isHex64(salt) {
135		panic("gnodice: secret invalide (64 caractères hexadécimaux attendus)")
136	}
137	if Commitment(g.Player, guess, salt) != g.Commitment {
138		panic("gnodice: le chiffre ou le secret ne correspond pas à l'empreinte")
139	}
140
141	g.Guess = guess
142	won := guess == g.Roll
143	payout := int64(0)
144	if won {
145		payout = g.reservation()
146		g.Status = StatusWon
147	} else {
148		g.Status = StatusLost
149	}
150	g.Payout = payout
151	finish(g)
152
153	p := getPlayer(g.Player)
154	p.Played++
155	p.Paid += payout
156	if won {
157		p.Wins++
158		totalWins++
159	} else {
160		totalLosses++
161	}
162	totalPaid += payout
163
164	chain.Emit(
165		"GameSettled",
166		"id", itoa64(g.ID),
167		"player", g.Player.String(),
168		"guess", strconv.Itoa(guess),
169		"roll", strconv.Itoa(g.Roll),
170		"bet", itoa64(g.Bet),
171		"payout", itoa64(payout),
172		"won", strconv.FormatBool(won),
173	)
174
175	if won {
176		recordPayout(payout)
177		pay(cur, g.Player, payout) // en dernier
178	}
179	checkLowBankroll()
180	return "roll=" + strconv.Itoa(g.Roll) + ";won=" + strconv.FormatBool(won) + ";payout=" + itoa64(payout)
181}
182
183// Refund rend sa mise au joueur si le croupier n'a pas tiré le dé à temps
184// (ResolveTimeout). N'importe qui peut le déclencher : l'argent va au joueur.
185func Refund(cur realm, id int64) {
186	g := mustGetGame(id)
187	if g.Status != StatusPending {
188		panic("gnodice: seule une partie en attente de tirage peut être remboursée")
189	}
190	if now() < g.PlacedAt+ResolveTimeout {
191		panic("gnodice: le croupier a encore " + formatDuration(g.PlacedAt+ResolveTimeout-now()) + " pour tirer le dé")
192	}
193
194	g.Status = StatusRefunded
195	g.Payout = g.Bet
196	finish(g)
197
198	p := getPlayer(g.Player)
199	p.Wagered -= g.Bet
200	totalWagered -= g.Bet
201	totalRefunded++
202
203	chain.Emit("GameRefunded", "id", itoa64(g.ID), "player", g.Player.String(), "amount", itoa64(g.Bet))
204	pay(cur, g.Player, g.Bet) // en dernier
205}
206
207// Expire termine une partie dont le chiffre n'a jamais été dévoilé
208// (RevealWindow) : elle compte comme perdue et la réserve est libérée.
209// N'importe qui peut l'appeler.
210func Expire(cur realm, id int64) {
211	g := mustGetGame(id)
212	if g.Status != StatusRolled {
213		panic("gnodice: seule une partie tirée et non dévoilée peut expirer")
214	}
215	if now() <= g.RolledAt+RevealWindow {
216		panic("gnodice: le joueur peut encore dévoiler son chiffre")
217	}
218
219	g.Status = StatusExpired
220	finish(g)
221
222	p := getPlayer(g.Player)
223	p.Played++
224	totalExpired++
225
226	chain.Emit("GameExpired", "id", itoa64(g.ID), "player", g.Player.String())
227}
228
229// --- outils internes ---
230
231// receivedBet vérifie la mise envoyée avec la transaction et renvoie son montant.
232func receivedBet() int64 {
233	sent := unsafe.OriginSend()
234	if len(sent) != 1 || sent[0].Denom != Denom {
235		panic("gnodice: envoie ta mise en GNOT uniquement")
236	}
237	amount := sent[0].Amount
238	if amount < MinBet || amount > MaxBet {
239		panic("gnodice: la mise doit être entre 1 et 10 GNOT")
240	}
241	return amount
242}
243
244// finish : partie terminée. Libère la réserve, la retire des parties en cours
245// et l'ajoute aux historiques.
246func finish(g *Game) {
247	g.SettledAt = now()
248	reserved -= g.reservation()
249	openGames.Remove(gameKey(g.ID))
250	p := getPlayer(g.Player)
251	p.Open--
252	p.History = pushID(p.History, g.ID)
253	recent = pushID(recent, g.ID)
254}
255
256// pay envoie des GNOT de la banque vers une adresse.
257func pay(cur realm, to address, amount int64) {
258	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
259	bk.SendCoins(realmAddr, to, chain.NewCoins(chain.NewCoin(Denom, amount)))
260}
261
262// cooldownRemaining renvoie le nombre de secondes à attendre avant de pouvoir
263// rejouer (0 si le joueur peut jouer maintenant).
264func cooldownRemaining(p *playerData, t int64) int64 {
265	if p == nil || p.LastPlay == 0 {
266		return 0
267	}
268	wait := p.LastPlay + CooldownSeconds - t
269	if wait < 0 {
270		return 0
271	}
272	return wait
273}
274
275// formatDuration transforme un nombre de secondes en texte, ex. "7 min 12 s".
276func formatDuration(seconds int64) string {
277	if seconds < 60 {
278		return itoa64(seconds) + " s"
279	}
280	return itoa64(seconds/60) + " min " + itoa64(seconds%60) + " s"
281}