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

rps.gno

5.46 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/moul/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}