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