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.

rps.gno

5.48 Kb · 237 lines
  1package rps
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"chain/runtime/unsafe"
  7	"strconv"
  8	"strings"
  9
 10	"gno.land/p/nym-moultest123/kit/ui/v0"
 11	"gno.land/p/nt/avl/v0"
 12)
 13
 14// Moves.
 15const (
 16	rock     = 0
 17	paper    = 1
 18	scissors = 2
 19)
 20
 21var moveNames = [3]string{"rock", "paper", "scissors"}
 22
 23// Outcomes.
 24const (
 25	outLose = 0 // player loses
 26	outDraw = 1
 27	outWin  = 2 // player wins
 28)
 29
 30// tally holds a single player's cumulative record.
 31type tally struct {
 32	wins   int
 33	losses int
 34	draws  int
 35	plays  int // total rounds this player has played (drives entropy)
 36}
 37
 38// round is one recorded game (for the recent-rounds table).
 39type round struct {
 40	player   address
 41	height   int64
 42	player_m int // player move
 43	house_m  int // house move
 44	outcome  int
 45}
 46
 47// State.
 48var (
 49	players = avl.NewTree() // address string -> *tally
 50	rounds  []round         // append-only history; Render shows the tail
 51
 52	// Global tally (caller-agnostic).
 53	totalWins   int // player wins
 54	totalLosses int // player losses
 55	totalDraws  int
 56	totalRounds int
 57)
 58
 59const maxRecent = 8
 60
 61// parseChoice maps a choice string to a move index; ok=false if invalid.
 62func parseChoice(choice string) (int, bool) {
 63	switch strings.ToLower(strings.TrimSpace(choice)) {
 64	case "rock", "r":
 65		return rock, true
 66	case "paper", "p":
 67		return paper, true
 68	case "scissors", "s":
 69		return scissors, true
 70	}
 71	return 0, false
 72}
 73
 74// houseMove derives the house move deterministically from the chain height
 75// and the caller's play count (so repeated calls in the same block differ).
 76func houseMove(height int64, playCount int) int {
 77	x := height + int64(playCount)*7
 78	m := x % 3
 79	if m < 0 {
 80		m += 3
 81	}
 82	return int(m)
 83}
 84
 85// decide returns the outcome from the player's perspective.
 86func decide(playerMove, houseM int) int {
 87	if playerMove == houseM {
 88		return outDraw
 89	}
 90	// player beats (player+1)%3 ... rock(0) beats scissors(2), etc.
 91	if (playerMove+2)%3 == houseM {
 92		return outWin
 93	}
 94	return outLose
 95}
 96
 97func getTally(key string) *tally {
 98	if v := players.Get(key); v != nil {
 99		return v.(*tally)
100	}
101	return nil
102}
103
104// Play plays one round against the chain. choice is "rock"|"paper"|"scissors"
105// (single-letter shortcuts accepted). It panics on an invalid choice.
106func Play(cur realm, choice string) {
107	mv, ok := parseChoice(choice)
108	if !ok {
109		panic("invalid choice: use rock, paper, or scissors")
110	}
111
112	caller := unsafe.PreviousRealm().Address()
113	key := caller.String()
114
115	t := getTally(key)
116	if t == nil {
117		t = &tally{}
118		players.Set(key, t)
119	}
120
121	height := runtime.ChainHeight()
122	hm := houseMove(height, t.plays)
123	outcome := decide(mv, hm)
124
125	t.plays++
126	switch outcome {
127	case outWin:
128		t.wins++
129		totalWins++
130	case outLose:
131		t.losses++
132		totalLosses++
133	default:
134		t.draws++
135		totalDraws++
136	}
137	totalRounds++
138
139	rounds = append(rounds, round{
140		player:   caller,
141		height:   height,
142		player_m: mv,
143		house_m:  hm,
144		outcome:  outcome,
145	})
146
147	chain.Emit(
148		"RoundPlayed",
149		"player", key,
150		"choice", moveNames[mv],
151		"house", moveNames[hm],
152		"outcome", outcomeName(outcome),
153	)
154}
155
156func outcomeName(o int) string {
157	switch o {
158	case outWin:
159		return "win"
160	case outLose:
161		return "lose"
162	default:
163		return "draw"
164	}
165}
166
167func winRate(wins, plays int) string {
168	if plays == 0 {
169		return "0%"
170	}
171	return strconv.Itoa(wins*100/plays) + "%"
172}
173
174// Render returns a Markdown dashboard: global tally, per-player table, and the
175// last few rounds. It is caller-agnostic (no cur).
176func Render(path string) string {
177	var b strings.Builder
178
179	b.WriteString("# Rock-Paper-Scissors — play vs the chain\n\n")
180	b.WriteString("Call `Play(cur, \"rock\"|\"paper\"|\"scissors\")`. ")
181	b.WriteString("The house move is derived deterministically from the current block height ")
182	b.WriteString("and your personal play count.\n\n")
183
184	// Global tally.
185	b.WriteString("## Global tally\n\n")
186	b.WriteString("| Rounds | Player wins | Player losses | Draws | Player win rate |\n")
187	b.WriteString("|---|---|---|---|---|\n")
188	b.WriteString("| " + strconv.Itoa(totalRounds))
189	b.WriteString(" | " + strconv.Itoa(totalWins))
190	b.WriteString(" | " + strconv.Itoa(totalLosses))
191	b.WriteString(" | " + strconv.Itoa(totalDraws))
192	b.WriteString(" | " + winRate(totalWins, totalRounds) + " |\n\n")
193
194	// Per-player table (deterministic order via avl iteration).
195	b.WriteString("## Players\n\n")
196	if players.Size() == 0 {
197		b.WriteString("_No games played yet. Be the first!_\n\n")
198	} else {
199		b.WriteString("| Player | W | L | D | Rounds | Win rate |\n")
200		b.WriteString("|---|---|---|---|---|---|\n")
201		players.Iterate("", "", func(key string, v interface{}) bool {
202			t := v.(*tally)
203			b.WriteString("| " + ui.AddrOf(key))
204			b.WriteString(" | " + strconv.Itoa(t.wins))
205			b.WriteString(" | " + strconv.Itoa(t.losses))
206			b.WriteString(" | " + strconv.Itoa(t.draws))
207			b.WriteString(" | " + strconv.Itoa(t.plays))
208			b.WriteString(" | " + winRate(t.wins, t.plays) + " |\n")
209			return false
210		})
211		b.WriteString("\n")
212	}
213
214	// Recent rounds (tail).
215	b.WriteString("## Recent rounds\n\n")
216	if len(rounds) == 0 {
217		b.WriteString("_None yet._\n")
218	} else {
219		b.WriteString("| Height | Player | Choice | House | Result |\n")
220		b.WriteString("|---|---|---|---|---|\n")
221		start := len(rounds) - maxRecent
222		if start < 0 {
223			start = 0
224		}
225		// Show newest first.
226		for i := len(rounds) - 1; i >= start; i-- {
227			r := rounds[i]
228			b.WriteString("| " + strconv.FormatInt(r.height, 10))
229			b.WriteString(" | " + ui.Addr(r.player))
230			b.WriteString(" | " + moveNames[r.player_m])
231			b.WriteString(" | " + moveNames[r.house_m])
232			b.WriteString(" | " + outcomeName(r.outcome) + " |\n")
233		}
234	}
235
236	return b.String()
237}