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.

rpsmatch.gno

6.00 Kb · 245 lines
  1// Package rpsmatch is a best-of-three rock-paper-scissors arena against the
  2// house. Unlike a single stateless throw, a match persists across calls:
  3// the first player to win two rounds takes the match, and every player's
  4// match record accumulates on-chain.
  5package rpsmatch
  6
  7import (
  8	"strconv"
  9	"strings"
 10
 11	"chain"
 12	"chain/runtime"
 13
 14	"gno.land/p/nym-moultest123/kit/ui/v0"
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// Move is one of rock, paper, or scissors, ordered so that
 19// (winner - loser + 3) % 3 == 1 for every winning pair.
 20type Move int
 21
 22const (
 23	Rock Move = iota
 24	Paper
 25	Scissors
 26)
 27
 28// winsNeeded is the number of round wins required to take a match.
 29const winsNeeded = 2
 30
 31// match tracks an in-progress best-of-three series for one player.
 32type match struct {
 33	PlayerWins int
 34	HouseWins  int
 35	Rounds     int
 36}
 37
 38// playerState is the persisted record for one address.
 39type playerState struct {
 40	Active        *match
 41	MatchWins     int
 42	MatchLosses   int
 43	MatchesPlayed int
 44	RoundsPlayed  int
 45}
 46
 47var (
 48	players avl.Tree // address string -> *playerState
 49
 50	nonce       int
 51	totalRounds int
 52
 53	champion     address
 54	championWins int
 55)
 56
 57func moveName(m Move) string {
 58	switch m {
 59	case Rock:
 60		return "rock"
 61	case Paper:
 62		return "paper"
 63	case Scissors:
 64		return "scissors"
 65	default:
 66		return "?"
 67	}
 68}
 69
 70func parseMove(s string) (Move, bool) {
 71	switch strings.ToLower(strings.TrimSpace(s)) {
 72	case "rock", "r":
 73		return Rock, true
 74	case "paper", "p":
 75		return Paper, true
 76	case "scissors", "s":
 77		return Scissors, true
 78	default:
 79		return 0, false
 80	}
 81}
 82
 83// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.
 84func judge(p, h Move) int {
 85	return (int(p) - int(h) + 3) % 3
 86}
 87
 88// pickHouseMove derives a deterministic pseudo-random move from the current
 89// chain height, a monotonic per-realm nonce, and the caller's address, so
 90// repeated throws in the same block still diverge.
 91func pickHouseMove(caller address, n int) Move {
 92	seed := runtime.ChainHeight() + int64(n)
 93	s := caller.String()
 94	for i := 0; i < len(s); i++ {
 95		seed += int64(s[i])
 96	}
 97	if seed < 0 {
 98		seed = -seed
 99	}
100	return Move(seed % 3)
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
113// Throw plays one round of the caller's current match, starting a fresh
114// match if none is in progress. Accepts "rock"/"paper"/"scissors" or the
115// single-letter shorthand "r"/"p"/"s".
116func Throw(cur realm, moveStr string) string {
117	if !cur.IsCurrent() {
118		panic("invalid realm")
119	}
120	caller := cur.Previous().Address()
121
122	playerMove, ok := parseMove(moveStr)
123	if !ok {
124		panic("invalid move: use rock, paper, or scissors (r/p/s)")
125	}
126
127	ps := getOrCreate(caller)
128	if ps.Active == nil {
129		ps.Active = &match{}
130	}
131	m := ps.Active
132
133	nonce++
134	houseMove := pickHouseMove(caller, nonce)
135	round := judge(playerMove, houseMove)
136
137	m.Rounds++
138	totalRounds++
139	ps.RoundsPlayed++
140
141	var roundMsg string
142	switch round {
143	case 1:
144		m.PlayerWins++
145		roundMsg = "you win the round"
146	case 2:
147		m.HouseWins++
148		roundMsg = "house wins the round"
149	default:
150		roundMsg = "round tied"
151	}
152
153	out := "round " + strconv.Itoa(m.Rounds) + ": you played " + moveName(playerMove) +
154		", house played " + moveName(houseMove) + " -> " + roundMsg +
155		" (score " + strconv.Itoa(m.PlayerWins) + "-" + strconv.Itoa(m.HouseWins) + ")"
156
157	if m.PlayerWins >= winsNeeded || m.HouseWins >= winsNeeded {
158		ps.Active = nil
159		ps.MatchesPlayed++
160		playerTookMatch := m.PlayerWins > m.HouseWins
161		if playerTookMatch {
162			ps.MatchWins++
163			if ps.MatchWins > championWins {
164				championWins = ps.MatchWins
165				champion = caller
166			}
167			out += ". MATCH WON!"
168		} else {
169			ps.MatchLosses++
170			out += ". match lost."
171		}
172		chain.Emit("MatchFinished",
173			"player", caller.String(),
174			"playerWins", strconv.Itoa(m.PlayerWins),
175			"houseWins", strconv.Itoa(m.HouseWins),
176		)
177	}
178
179	chain.Emit("RoundPlayed",
180		"player", caller.String(),
181		"playerMove", moveName(playerMove),
182		"houseMove", moveName(houseMove),
183		"result", strconv.Itoa(round),
184	)
185
186	return out
187}
188
189func renderHome() string {
190	var b strings.Builder
191	b.WriteString("# Rock-Paper-Scissors Arena\n\n")
192	b.WriteString("Best-of-three matches against the house. First to " +
193		strconv.Itoa(winsNeeded) + " round wins takes the match.\n\n")
194	b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n")
195
196	if champion.IsValid() {
197		b.WriteString("- Reigning champion: `" + champion.String() +
198			"` (" + strconv.Itoa(championWins) + " match wins)\n")
199	} else {
200		b.WriteString("- No champion yet — be the first to win a match.\n")
201	}
202
203	b.WriteString("\n## How to play\n\n")
204	b.WriteString("Call `Throw(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). " +
205		"Your throw starts a new match if you don't have one in progress, " +
206		"and each call plays one round of it.\n\n")
207	b.WriteString("View your own record at this realm's path plus your address, " +
208		"e.g. `.../rpsmatch:g1youraddress...`\n")
209	return b.String()
210}
211
212func renderPlayer(rawAddr string) string {
213	addr := strings.TrimSpace(rawAddr)
214	safe := ui.Inline(addr)
215
216	v := players.Get(addr)
217	if v == nil {
218		return "# Player " + safe + "\n\nNo recorded throws yet.\n"
219	}
220	ps := v.(*playerState)
221
222	var b strings.Builder
223	b.WriteString("# Player " + safe + "\n\n")
224	b.WriteString("- Matches won: " + strconv.Itoa(ps.MatchWins) + "\n")
225	b.WriteString("- Matches lost: " + strconv.Itoa(ps.MatchLosses) + "\n")
226	b.WriteString("- Matches played: " + strconv.Itoa(ps.MatchesPlayed) + "\n")
227	b.WriteString("- Rounds played: " + strconv.Itoa(ps.RoundsPlayed) + "\n")
228
229	if ps.Active != nil {
230		b.WriteString("\n**Match in progress:** " + strconv.Itoa(ps.Active.PlayerWins) +
231			"-" + strconv.Itoa(ps.Active.HouseWins) + " through " +
232			strconv.Itoa(ps.Active.Rounds) + " round(s).\n")
233	}
234	return b.String()
235}
236
237// Render shows the arena dashboard at "", or one player's record when path
238// is their bech32 address.
239func Render(path string) string {
240	path = strings.TrimPrefix(strings.TrimSpace(path), "/")
241	if path == "" {
242		return renderHome()
243	}
244	return renderPlayer(path)
245}