Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

coinflipduel.gno

7.93 Kb · 321 lines
  1// Package coinflipduel is a one-on-one coin-flip duel: challenge another
  2// address, they call heads or tails, and the block settles it. No stakes,
  3// just bragging rights tracked in an on-chain win/loss record.
  4package coinflipduel
  5
  6import (
  7	"strconv"
  8	"strings"
  9
 10	"chain"
 11	"chain/runtime"
 12
 13	"gno.land/p/nym-moultest123/kit/ui/v0"
 14	"gno.land/p/nt/avl/v0"
 15)
 16
 17// duelStatus tracks the lifecycle of a duel.
 18type duelStatus int
 19
 20const (
 21	statusPending duelStatus = iota
 22	statusResolved
 23	statusCancelled
 24)
 25
 26func (s duelStatus) String() string {
 27	switch s {
 28	case statusPending:
 29		return "pending"
 30	case statusResolved:
 31		return "resolved"
 32	case statusCancelled:
 33		return "cancelled"
 34	default:
 35		return "?"
 36	}
 37}
 38
 39// duel is one challenge from Challenger to Opponent over a single coin flip.
 40// The Challenger calls a side up front; the Opponent's only move is to
 41// accept, which triggers the flip.
 42type duel struct {
 43	ID         string
 44	Challenger address
 45	Opponent   address
 46	Call       string // "heads" or "tails" — the challenger's pick
 47	Status     duelStatus
 48	Result     string // "heads" or "tails" once resolved
 49	Winner     address
 50	CreatedAt  int64
 51	ResolvedAt int64
 52}
 53
 54// playerState is the persisted win/loss record for one address.
 55type playerState struct {
 56	Wins   int
 57	Losses int
 58	Duels  int
 59}
 60
 61var (
 62	duels   avl.Tree // duel ID -> *duel
 63	players avl.Tree // address string -> *playerState
 64
 65	nextID    int
 66	flipNonce int
 67	totalDone int
 68
 69	champion     address
 70	championWins int
 71)
 72
 73func parseCall(s string) (string, bool) {
 74	switch strings.ToLower(strings.TrimSpace(s)) {
 75	case "heads", "h":
 76		return "heads", true
 77	case "tails", "t":
 78		return "tails", true
 79	default:
 80		return "", false
 81	}
 82}
 83
 84// flipCoin derives a deterministic pseudo-random side from the current chain
 85// height, a monotonic per-realm nonce, and both duelists' addresses, so two
 86// duels resolved in the same block still diverge.
 87func flipCoin(d *duel) string {
 88	flipNonce++
 89	seed := runtime.ChainHeight() + int64(flipNonce)
 90	s := d.Challenger.String() + d.Opponent.String() + d.ID
 91	for i := 0; i < len(s); i++ {
 92		seed += int64(s[i])
 93	}
 94	if seed < 0 {
 95		seed = -seed
 96	}
 97	if seed%2 == 0 {
 98		return "heads"
 99	}
100	return "tails"
101}
102
103func getOrCreate(addr address) *playerState {
104	key := addr.String()
105	if v := players.Get(key); v != nil {
106		return v.(*playerState)
107	}
108	ps := &playerState{}
109	players.Set(key, ps)
110	return ps
111}
112
113func recordResult(winner, loser address) {
114	w := getOrCreate(winner)
115	w.Wins++
116	w.Duels++
117	l := getOrCreate(loser)
118	l.Losses++
119	l.Duels++
120
121	if w.Wins > championWins {
122		championWins = w.Wins
123		champion = winner
124	}
125}
126
127// Challenge opens a new duel against opponentAddr, locking in the
128// challenger's call of "heads"/"tails" ("h"/"t" also accepted). The
129// opponent settles it by calling Accept with the returned duel ID.
130func Challenge(cur realm, opponentAddr string, call string) string {
131	challenger := cur.Previous().Address()
132
133	opponent := address(strings.TrimSpace(opponentAddr))
134	if !opponent.IsValid() {
135		panic("invalid opponent address")
136	}
137	if opponent == challenger {
138		panic("cannot duel yourself")
139	}
140
141	pick, ok := parseCall(call)
142	if !ok {
143		panic("call must be heads or tails (h/t)")
144	}
145
146	nextID++
147	id := strconv.Itoa(nextID)
148	d := &duel{
149		ID:         id,
150		Challenger: challenger,
151		Opponent:   opponent,
152		Call:       pick,
153		Status:     statusPending,
154		CreatedAt:  runtime.ChainHeight(),
155	}
156	duels.Set(id, d)
157
158	chain.Emit("DuelChallenged",
159		"id", id,
160		"challenger", challenger.String(),
161		"opponent", opponent.String(),
162		"call", pick,
163	)
164
165	return "duel #" + id + " created: you called " + pick +
166		", waiting for " + opponent.String() + " to accept"
167}
168
169// Accept settles a pending duel. Only the challenged opponent may call it;
170// the flip happens immediately and the result is final.
171func Accept(cur realm, id string) string {
172	caller := cur.Previous().Address()
173
174	v := duels.Get(id)
175	if v == nil {
176		panic("no such duel")
177	}
178	d := v.(*duel)
179
180	if d.Status != statusPending {
181		panic("duel already " + d.Status.String())
182	}
183	if caller != d.Opponent {
184		panic("only the challenged opponent can accept this duel")
185	}
186
187	result := flipCoin(d)
188	d.Result = result
189	d.Status = statusResolved
190	d.ResolvedAt = runtime.ChainHeight()
191
192	if result == d.Call {
193		d.Winner = d.Challenger
194		recordResult(d.Challenger, d.Opponent)
195	} else {
196		d.Winner = d.Opponent
197		recordResult(d.Opponent, d.Challenger)
198	}
199
200	totalDone++
201
202	chain.Emit("DuelResolved",
203		"id", id,
204		"result", result,
205		"winner", d.Winner.String(),
206	)
207
208	return "the coin landed on " + result + " -- " + d.Winner.String() + " wins duel #" + id
209}
210
211// Cancel withdraws a still-pending duel. Only the challenger may cancel,
212// and only before the opponent accepts.
213func Cancel(cur realm, id string) string {
214	caller := cur.Previous().Address()
215
216	v := duels.Get(id)
217	if v == nil {
218		panic("no such duel")
219	}
220	d := v.(*duel)
221
222	if d.Status != statusPending {
223		panic("duel already " + d.Status.String())
224	}
225	if caller != d.Challenger {
226		panic("only the challenger can cancel this duel")
227	}
228
229	d.Status = statusCancelled
230	return "duel #" + id + " cancelled"
231}
232
233func renderHome() string {
234	var b strings.Builder
235	b.WriteString("# Coin-Flip Duel\n\n")
236	b.WriteString("Challenge another address to a coin flip. You call heads or tails " +
237		"up front; they accept and the block decides. No stakes -- just a record.\n\n")
238	b.WriteString("- Total duels resolved: " + strconv.Itoa(totalDone) + "\n")
239
240	if champion.IsValid() {
241		b.WriteString("- Reigning champion: `" + champion.String() +
242			"` (" + strconv.Itoa(championWins) + " wins)\n")
243	} else {
244		b.WriteString("- No champion yet -- be the first to win a duel.\n")
245	}
246
247	b.WriteString("\n## How to play\n\n")
248	b.WriteString("1. `Challenge(opponentAddr, \"heads\"|\"tails\")` -- opens a duel, returns its ID.\n")
249	b.WriteString("2. The opponent calls `Accept(id)` -- flips the coin and settles it on the spot.\n")
250	b.WriteString("3. The challenger may `Cancel(id)` while it's still pending.\n\n")
251	b.WriteString("View a duel at this realm's path plus its ID (e.g. `.../coinflipduel:3`), " +
252		"or a player's record plus their address (e.g. `.../coinflipduel:g1youraddress...`).\n\n")
253
254	b.WriteString("## Pending duels\n\n")
255	pending := 0
256	duels.Iterate("", "", func(key string, value interface{}) bool {
257		d := value.(*duel)
258		if d.Status == statusPending {
259			pending++
260			b.WriteString("- #" + d.ID + ": `" + d.Challenger.String() + "` called " +
261				d.Call + ", waiting on `" + d.Opponent.String() + "`\n")
262		}
263		return false
264	})
265	if pending == 0 {
266		b.WriteString("_none right now_\n")
267	}
268
269	return b.String()
270}
271
272func renderDuel(id string) string {
273	v := duels.Get(id)
274	if v == nil {
275		return "# Duel #" + ui.Inline(id) + "\n\nNo such duel.\n"
276	}
277	d := v.(*duel)
278
279	var b strings.Builder
280	b.WriteString("# Duel #" + d.ID + "\n\n")
281	b.WriteString("- Challenger: `" + d.Challenger.String() + "` called **" + d.Call + "**\n")
282	b.WriteString("- Opponent: `" + d.Opponent.String() + "`\n")
283	b.WriteString("- Status: " + d.Status.String() + "\n")
284
285	if d.Status == statusResolved {
286		b.WriteString("- Coin landed on: **" + d.Result + "**\n")
287		b.WriteString("- Winner: `" + d.Winner.String() + "`\n")
288	}
289	return b.String()
290}
291
292func renderPlayer(rawAddr string) string {
293	addr := strings.TrimSpace(rawAddr)
294	safe := ui.Inline(addr)
295
296	v := players.Get(addr)
297	if v == nil {
298		return "# Player " + safe + "\n\nNo recorded duels yet.\n"
299	}
300	ps := v.(*playerState)
301
302	var b strings.Builder
303	b.WriteString("# Player " + safe + "\n\n")
304	b.WriteString("- Wins: " + strconv.Itoa(ps.Wins) + "\n")
305	b.WriteString("- Losses: " + strconv.Itoa(ps.Losses) + "\n")
306	b.WriteString("- Duels played: " + strconv.Itoa(ps.Duels) + "\n")
307	return b.String()
308}
309
310// Render shows the duel arena at "", a specific duel when path is a numeric
311// ID, or one player's record when path is their bech32 address.
312func Render(path string) string {
313	path = strings.TrimPrefix(strings.TrimSpace(path), "/")
314	if path == "" {
315		return renderHome()
316	}
317	if _, err := strconv.Atoi(path); err == nil {
318		return renderDuel(path)
319	}
320	return renderPlayer(path)
321}