1// Package rpsoracle is a rock-paper-scissors opponent that doesn't roll2// dice: it studies each player's move history and always throws the3// counter to whichever move that player has favored most. Play a fixed4// pattern and the oracle punishes it; play close to a uniform 1/3-1/3-1/35// mix and it can't out-guess you better than chance.6package rpsoracle78import (9 "strconv"10 "strings"1112 "chain"13 "chain/runtime"1415 "gno.land/p/nt/avl/v0"16)1718// Move is one of rock, paper, or scissors, ordered so that19// (winner - loser + 3) % 3 == 1 for every winning pair.20type Move int2122const (23 Rock Move = iota24 Paper25 Scissors26)2728func moveName(m Move) string {29 switch m {30 case Rock:31 return "rock"32 case Paper:33 return "paper"34 case Scissors:35 return "scissors"36 default:37 return "?"38 }39}4041func parseMove(s string) (Move, bool) {42 switch strings.ToLower(strings.TrimSpace(s)) {43 case "rock", "r":44 return Rock, true45 case "paper", "p":46 return Paper, true47 case "scissors", "s":48 return Scissors, true49 default:50 return 0, false51 }52}5354// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.55func judge(p, h Move) int {56 return (int(p) - int(h) + 3) % 357}5859// beats returns the move that defeats m.60func beats(m Move) Move {61 return Move((int(m) + 1) % 3)62}6364// playerState is the persisted record for one address.65type playerState struct {66 Counts [3]int // history tally per move, what the oracle predicts from67 Rounds int68 Wins int // player beat the oracle69 Losses int // oracle beat the player70 Draws int71 BestStreak int72 streak int // current player win streak against the oracle73}7475var (76 players avl.Tree // address string -> *playerState7778 nonce int79 totalRounds int80 oracleCorrect int // rounds the oracle won by successfully countering8182 topOutwitter address83 topOutwitterWins int84)8586func getOrCreate(addr address) *playerState {87 key := addr.String()88 if v := players.Get(key); v != nil {89 return v.(*playerState)90 }91 ps := &playerState{}92 players.Set(key, ps)93 return ps94}9596// predict guesses the player's next move as the most-played move in their97// history so far. Ties (including a fresh player's all-zero history) fall98// back to a chain-height-derived seed so the oracle doesn't always break99// ties the same way.100func predict(ps *playerState, seed int64) Move {101 best := Rock102 bestCount := ps.Counts[Rock]103 tied := []Move{Rock}104 for _, m := range []Move{Paper, Scissors} {105 switch {106 case ps.Counts[m] > bestCount:107 bestCount = ps.Counts[m]108 best = m109 tied = []Move{m}110 case ps.Counts[m] == bestCount:111 tied = append(tied, m)112 }113 }114 if len(tied) > 1 {115 if seed < 0 {116 seed = -seed117 }118 best = tied[int(seed)%len(tied)]119 }120 return best121}122123// Play pits the caller against the oracle: it predicts your next move from124// your own move history and throws the counter. Accepts125// "rock"/"paper"/"scissors" or the single-letter shorthand "r"/"p"/"s".126func Play(cur realm, moveStr string) string {127 if !cur.IsCurrent() {128 panic("invalid realm")129 }130 caller := cur.Previous().Address()131132 playerMove, ok := parseMove(moveStr)133 if !ok {134 panic("invalid move: use rock, paper, or scissors (r/p/s)")135 }136137 ps := getOrCreate(caller)138139 nonce++140 seed := runtime.ChainHeight() + int64(nonce)141 predicted := predict(ps, seed)142 oracleMove := beats(predicted)143144 result := judge(playerMove, oracleMove)145146 ps.Counts[playerMove]++147 ps.Rounds++148 totalRounds++149150 var msg string151 switch result {152 case 1:153 ps.Wins++154 ps.streak++155 if ps.streak > ps.BestStreak {156 ps.BestStreak = ps.streak157 }158 if ps.Wins > topOutwitterWins {159 topOutwitterWins = ps.Wins160 topOutwitter = caller161 }162 msg = "you outwitted the oracle!"163 case 2:164 ps.Losses++165 ps.streak = 0166 oracleCorrect++167 msg = "the oracle read you like a book."168 default:169 ps.Draws++170 ps.streak = 0171 msg = "a draw — you and the oracle picked the same move."172 }173174 chain.Emit("RoundPlayed",175 "player", caller.String(),176 "playerMove", moveName(playerMove),177 "oraclePredicted", moveName(predicted),178 "oracleMove", moveName(oracleMove),179 "result", strconv.Itoa(result),180 )181182 return "you played " + moveName(playerMove) + ", the oracle predicted " +183 moveName(predicted) + " and threw " + moveName(oracleMove) + " -> " + msg184}185186func renderHome() string {187 var b strings.Builder188 b.WriteString("# Rock-Paper-Scissors Oracle\n\n")189 b.WriteString("An adaptive opponent: it doesn't roll dice, it studies you. ")190 b.WriteString("Every throw is logged, and the oracle always counters whichever ")191 b.WriteString("move you've played most often. Play a uniform mixed strategy and ")192 b.WriteString("it can't out-guess you; fall into a habit and it will.\n\n")193194 b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n")195 if totalRounds > 0 {196 pct := oracleCorrect * 100 / totalRounds197 b.WriteString("- Oracle win rate: " + strconv.Itoa(pct) + "%\n")198 }199 if topOutwitter.IsValid() {200 b.WriteString("- Top outwitter: `" + topOutwitter.String() + "` (" +201 strconv.Itoa(topOutwitterWins) + " wins against the oracle)\n")202 } else {203 b.WriteString("- No one has beaten the oracle yet.\n")204 }205206 b.WriteString("\n## How to play\n\n")207 b.WriteString("Call `Play(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). ")208 b.WriteString("View your own record at this realm's path plus your address, ")209 b.WriteString("e.g. `.../rpsoracle:g1youraddress...`\n")210 return b.String()211}212213// escapeInline neutralizes markdown-active characters in untrusted text214// before it's embedded inline in Render output.215func escapeInline(s string) string {216 r := strings.NewReplacer(217 "\\", "\\\\",218 "`", "\\`",219 "*", "\\*",220 "_", "\\_",221 "[", "\\[",222 "]", "\\]",223 "|", "\\|",224 )225 return r.Replace(s)226}227228func renderPlayer(rawAddr string) string {229 addr := strings.TrimSpace(rawAddr)230 safe := escapeInline(addr)231232 v := players.Get(addr)233 if v == nil {234 return "# Player " + safe + "\n\nNo recorded rounds yet.\n"235 }236 ps := v.(*playerState)237238 var b strings.Builder239 b.WriteString("# Player " + safe + "\n\n")240 b.WriteString("- Rounds played: " + strconv.Itoa(ps.Rounds) + "\n")241 b.WriteString("- Beat the oracle: " + strconv.Itoa(ps.Wins) + "\n")242 b.WriteString("- Lost to the oracle: " + strconv.Itoa(ps.Losses) + "\n")243 b.WriteString("- Draws: " + strconv.Itoa(ps.Draws) + "\n")244 b.WriteString("- Best win streak vs oracle: " + strconv.Itoa(ps.BestStreak) + "\n")245 b.WriteString("- Move history — rock: " + strconv.Itoa(ps.Counts[Rock]) +246 ", paper: " + strconv.Itoa(ps.Counts[Paper]) +247 ", scissors: " + strconv.Itoa(ps.Counts[Scissors]) + "\n")248249 if ps.Rounds > 0 {250 maxCount := ps.Counts[Rock]251 for _, c := range ps.Counts[1:] {252 if c > maxCount {253 maxCount = c254 }255 }256 predictability := maxCount * 100 / ps.Rounds257 b.WriteString("- Predictability score: " + strconv.Itoa(predictability) +258 "% (lower is harder for the oracle to read)\n")259 }260 return b.String()261}262263// Render shows the oracle's dashboard at "", or one player's record when264// path is their bech32 address.265func Render(path string) string {266 path = strings.TrimPrefix(strings.TrimSpace(path), "/")267 if path == "" {268 return renderHome()269 }270 return renderPlayer(path)271}272Play(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
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.