1package rps23import (4 "chain"5 "chain/runtime"6 "chain/runtime/unsafe"7 "strconv"8 "strings"910 "gno.land/p/nt/avl/v0"11)1213// Moves.14const (15 rock = 016 paper = 117 scissors = 218)1920var moveNames = [3]string{"rock", "paper", "scissors"}2122// Outcomes.23const (24 outLose = 0 // player loses25 outDraw = 126 outWin = 2 // player wins27)2829// tally holds a single player's cumulative record.30type tally struct {31 wins int32 losses int33 draws int34 plays int // total rounds this player has played (drives entropy)35}3637// round is one recorded game (for the recent-rounds table).38type round struct {39 player address40 height int6441 player_m int // player move42 house_m int // house move43 outcome int44}4546// State.47var (48 players = avl.NewTree() // address string -> *tally49 rounds []round // append-only history; Render shows the tail5051 // Global tally (caller-agnostic).52 totalWins int // player wins53 totalLosses int // player losses54 totalDraws int55 totalRounds int56)5758const maxRecent = 85960// 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, true65 case "paper", "p":66 return paper, true67 case "scissors", "s":68 return scissors, true69 }70 return 0, false71}7273// houseMove derives the house move deterministically from the chain height74// 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)*777 m := x % 378 if m < 0 {79 m += 380 }81 return int(m)82}8384// decide returns the outcome from the player's perspective.85func decide(playerMove, houseM int) int {86 if playerMove == houseM {87 return outDraw88 }89 // player beats (player+1)%3 ... rock(0) beats scissors(2), etc.90 if (playerMove+2)%3 == houseM {91 return outWin92 }93 return outLose94}9596func getTally(key string) *tally {97 if v := players.Get(key); v != nil {98 return v.(*tally)99 }100 return nil101}102103// 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 }110111 caller := unsafe.PreviousRealm().Address()112 key := caller.String()113114 t := getTally(key)115 if t == nil {116 t = &tally{}117 players.Set(key, t)118 }119120 height := runtime.ChainHeight()121 hm := houseMove(height, t.plays)122 outcome := decide(mv, hm)123124 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++137138 rounds = append(rounds, round{139 player: caller,140 height: height,141 player_m: mv,142 house_m: hm,143 outcome: outcome,144 })145146 chain.Emit(147 "RoundPlayed",148 "player", key,149 "choice", moveNames[mv],150 "house", moveNames[hm],151 "outcome", outcomeName(outcome),152 )153}154155func 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}165166func winRate(wins, plays int) string {167 if plays == 0 {168 return "0%"169 }170 return strconv.Itoa(wins*100/plays) + "%"171}172173// Render returns a Markdown dashboard: global tally, per-player table, and the174// last few rounds. It is caller-agnostic (no cur).175func Render(path string) string {176 var b strings.Builder177178 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")182183 // 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")192193 // 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 false209 })210 b.WriteString("\n")211 }212213 // 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) - maxRecent221 if start < 0 {222 start = 0223 }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 }234235 return b.String()236}237238// shortAddr abbreviates an address string for display.239func shortAddr(a string) string {240 if len(a) <= 12 {241 return a242 }243 return a[:8] + "…" + a[len(a)-4:]244}245Play(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, choice string)
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.