1// Package rpsmatch is a best-of-three rock-paper-scissors arena against the2// 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's4// match record accumulates on-chain.5package rpsmatch67import (8 "strconv"9 "strings"1011 "chain"12 "chain/runtime"1314 "gno.land/p/nt/avl/v0"15)1617// Move is one of rock, paper, or scissors, ordered so that18// (winner - loser + 3) % 3 == 1 for every winning pair.19type Move int2021const (22 Rock Move = iota23 Paper24 Scissors25)2627// winsNeeded is the number of round wins required to take a match.28const winsNeeded = 22930// match tracks an in-progress best-of-three series for one player.31type match struct {32 PlayerWins int33 HouseWins int34 Rounds int35}3637// playerState is the persisted record for one address.38type playerState struct {39 Active *match40 MatchWins int41 MatchLosses int42 MatchesPlayed int43 RoundsPlayed int44}4546var (47 players avl.Tree // address string -> *playerState4849 nonce int50 totalRounds int5152 champion address53 championWins int54)5556func moveName(m Move) string {57 switch m {58 case Rock:59 return "rock"60 case Paper:61 return "paper"62 case Scissors:63 return "scissors"64 default:65 return "?"66 }67}6869func parseMove(s string) (Move, bool) {70 switch strings.ToLower(strings.TrimSpace(s)) {71 case "rock", "r":72 return Rock, true73 case "paper", "p":74 return Paper, true75 case "scissors", "s":76 return Scissors, true77 default:78 return 0, false79 }80}8182// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.83func judge(p, h Move) int {84 return (int(p) - int(h) + 3) % 385}8687// pickHouseMove derives a deterministic pseudo-random move from the current88// chain height, a monotonic per-realm nonce, and the caller's address, so89// repeated throws in the same block still diverge.90func pickHouseMove(caller address, n int) Move {91 seed := runtime.ChainHeight() + int64(n)92 s := caller.String()93 for i := 0; i < len(s); i++ {94 seed += int64(s[i])95 }96 if seed < 0 {97 seed = -seed98 }99 return Move(seed % 3)100}101102func getOrCreate(addr address) *playerState {103 key := addr.String()104 if v := players.Get(key); v != nil {105 return v.(*playerState)106 }107 ps := &playerState{}108 players.Set(key, ps)109 return ps110}111112// Throw plays one round of the caller's current match, starting a fresh113// match if none is in progress. Accepts "rock"/"paper"/"scissors" or the114// single-letter shorthand "r"/"p"/"s".115func Throw(cur realm, moveStr string) string {116 if !cur.IsCurrent() {117 panic("invalid realm")118 }119 caller := cur.Previous().Address()120121 playerMove, ok := parseMove(moveStr)122 if !ok {123 panic("invalid move: use rock, paper, or scissors (r/p/s)")124 }125126 ps := getOrCreate(caller)127 if ps.Active == nil {128 ps.Active = &match{}129 }130 m := ps.Active131132 nonce++133 houseMove := pickHouseMove(caller, nonce)134 round := judge(playerMove, houseMove)135136 m.Rounds++137 totalRounds++138 ps.RoundsPlayed++139140 var roundMsg string141 switch round {142 case 1:143 m.PlayerWins++144 roundMsg = "you win the round"145 case 2:146 m.HouseWins++147 roundMsg = "house wins the round"148 default:149 roundMsg = "round tied"150 }151152 out := "round " + strconv.Itoa(m.Rounds) + ": you played " + moveName(playerMove) +153 ", house played " + moveName(houseMove) + " -> " + roundMsg +154 " (score " + strconv.Itoa(m.PlayerWins) + "-" + strconv.Itoa(m.HouseWins) + ")"155156 if m.PlayerWins >= winsNeeded || m.HouseWins >= winsNeeded {157 ps.Active = nil158 ps.MatchesPlayed++159 playerTookMatch := m.PlayerWins > m.HouseWins160 if playerTookMatch {161 ps.MatchWins++162 if ps.MatchWins > championWins {163 championWins = ps.MatchWins164 champion = caller165 }166 out += ". MATCH WON!"167 } else {168 ps.MatchLosses++169 out += ". match lost."170 }171 chain.Emit("MatchFinished",172 "player", caller.String(),173 "playerWins", strconv.Itoa(m.PlayerWins),174 "houseWins", strconv.Itoa(m.HouseWins),175 )176 }177178 chain.Emit("RoundPlayed",179 "player", caller.String(),180 "playerMove", moveName(playerMove),181 "houseMove", moveName(houseMove),182 "result", strconv.Itoa(round),183 )184185 return out186}187188func renderHome() string {189 var b strings.Builder190 b.WriteString("# Rock-Paper-Scissors Arena\n\n")191 b.WriteString("Best-of-three matches against the house. First to " +192 strconv.Itoa(winsNeeded) + " round wins takes the match.\n\n")193 b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n")194195 if champion.IsValid() {196 b.WriteString("- Reigning champion: `" + champion.String() +197 "` (" + strconv.Itoa(championWins) + " match wins)\n")198 } else {199 b.WriteString("- No champion yet — be the first to win a match.\n")200 }201202 b.WriteString("\n## How to play\n\n")203 b.WriteString("Call `Throw(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). " +204 "Your throw starts a new match if you don't have one in progress, " +205 "and each call plays one round of it.\n\n")206 b.WriteString("View your own record at this realm's path plus your address, " +207 "e.g. `.../rpsmatch:g1youraddress...`\n")208 return b.String()209}210211// escapeInline neutralizes markdown-active characters in untrusted text212// before it's embedded inline in Render output.213func escapeInline(s string) string {214 r := strings.NewReplacer(215 "\\", "\\\\",216 "`", "\\`",217 "*", "\\*",218 "_", "\\_",219 "[", "\\[",220 "]", "\\]",221 "|", "\\|",222 )223 return r.Replace(s)224}225226func renderPlayer(rawAddr string) string {227 addr := strings.TrimSpace(rawAddr)228 safe := escapeInline(addr)229230 v := players.Get(addr)231 if v == nil {232 return "# Player " + safe + "\n\nNo recorded throws yet.\n"233 }234 ps := v.(*playerState)235236 var b strings.Builder237 b.WriteString("# Player " + safe + "\n\n")238 b.WriteString("- Matches won: " + strconv.Itoa(ps.MatchWins) + "\n")239 b.WriteString("- Matches lost: " + strconv.Itoa(ps.MatchLosses) + "\n")240 b.WriteString("- Matches played: " + strconv.Itoa(ps.MatchesPlayed) + "\n")241 b.WriteString("- Rounds played: " + strconv.Itoa(ps.RoundsPlayed) + "\n")242243 if ps.Active != nil {244 b.WriteString("\n**Match in progress:** " + strconv.Itoa(ps.Active.PlayerWins) +245 "-" + strconv.Itoa(ps.Active.HouseWins) + " through " +246 strconv.Itoa(ps.Active.Rounds) + " round(s).\n")247 }248 return b.String()249}250251// Render shows the arena dashboard at "", or one player's record when path252// is their bech32 address.253func Render(path string) string {254 path = strings.TrimPrefix(strings.TrimSpace(path), "/")255 if path == "" {256 return renderHome()257 }258 return renderPlayer(path)259}260Render(path string) string
Throw(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}, moveStr 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.