Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

rpsoracle.gno

6.74 Kb · 257 lines
  1// Package rpsoracle is a rock-paper-scissors opponent that doesn't roll
  2// dice: it studies each player's move history and always throws the
  3// counter to whichever move that player has favored most. Play a fixed
  4// pattern and the oracle punishes it; play close to a uniform 1/3-1/3-1/3
  5// mix and it can't out-guess you better than chance.
  6package rpsoracle
  7
  8import (
  9	"strconv"
 10	"strings"
 11
 12	"chain"
 13	"chain/runtime"
 14
 15	"gno.land/p/nym-moultest123/kit/ui/v0"
 16	"gno.land/p/nt/avl/v0"
 17)
 18
 19// Move is one of rock, paper, or scissors, ordered so that
 20// (winner - loser + 3) % 3 == 1 for every winning pair.
 21type Move int
 22
 23const (
 24	Rock Move = iota
 25	Paper
 26	Scissors
 27)
 28
 29func moveName(m Move) string {
 30	switch m {
 31	case Rock:
 32		return "rock"
 33	case Paper:
 34		return "paper"
 35	case Scissors:
 36		return "scissors"
 37	default:
 38		return "?"
 39	}
 40}
 41
 42func parseMove(s string) (Move, bool) {
 43	switch strings.ToLower(strings.TrimSpace(s)) {
 44	case "rock", "r":
 45		return Rock, true
 46	case "paper", "p":
 47		return Paper, true
 48	case "scissors", "s":
 49		return Scissors, true
 50	default:
 51		return 0, false
 52	}
 53}
 54
 55// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.
 56func judge(p, h Move) int {
 57	return (int(p) - int(h) + 3) % 3
 58}
 59
 60// beats returns the move that defeats m.
 61func beats(m Move) Move {
 62	return Move((int(m) + 1) % 3)
 63}
 64
 65// playerState is the persisted record for one address.
 66type playerState struct {
 67	Counts     [3]int // history tally per move, what the oracle predicts from
 68	Rounds     int
 69	Wins       int // player beat the oracle
 70	Losses     int // oracle beat the player
 71	Draws      int
 72	BestStreak int
 73	streak     int // current player win streak against the oracle
 74}
 75
 76var (
 77	players avl.Tree // address string -> *playerState
 78
 79	nonce         int
 80	totalRounds   int
 81	oracleCorrect int // rounds the oracle won by successfully countering
 82
 83	topOutwitter     address
 84	topOutwitterWins int
 85)
 86
 87func getOrCreate(addr address) *playerState {
 88	key := addr.String()
 89	if v := players.Get(key); v != nil {
 90		return v.(*playerState)
 91	}
 92	ps := &playerState{}
 93	players.Set(key, ps)
 94	return ps
 95}
 96
 97// predict guesses the player's next move as the most-played move in their
 98// history so far. Ties (including a fresh player's all-zero history) fall
 99// back to a chain-height-derived seed so the oracle doesn't always break
100// ties the same way.
101func predict(ps *playerState, seed int64) Move {
102	best := Rock
103	bestCount := ps.Counts[Rock]
104	tied := []Move{Rock}
105	for _, m := range []Move{Paper, Scissors} {
106		switch {
107		case ps.Counts[m] > bestCount:
108			bestCount = ps.Counts[m]
109			best = m
110			tied = []Move{m}
111		case ps.Counts[m] == bestCount:
112			tied = append(tied, m)
113		}
114	}
115	if len(tied) > 1 {
116		if seed < 0 {
117			seed = -seed
118		}
119		best = tied[int(seed)%len(tied)]
120	}
121	return best
122}
123
124// Play pits the caller against the oracle: it predicts your next move from
125// your own move history and throws the counter. Accepts
126// "rock"/"paper"/"scissors" or the single-letter shorthand "r"/"p"/"s".
127func Play(cur realm, moveStr string) string {
128	if !cur.IsCurrent() {
129		panic("invalid realm")
130	}
131	caller := cur.Previous().Address()
132
133	playerMove, ok := parseMove(moveStr)
134	if !ok {
135		panic("invalid move: use rock, paper, or scissors (r/p/s)")
136	}
137
138	ps := getOrCreate(caller)
139
140	nonce++
141	seed := runtime.ChainHeight() + int64(nonce)
142	predicted := predict(ps, seed)
143	oracleMove := beats(predicted)
144
145	result := judge(playerMove, oracleMove)
146
147	ps.Counts[playerMove]++
148	ps.Rounds++
149	totalRounds++
150
151	var msg string
152	switch result {
153	case 1:
154		ps.Wins++
155		ps.streak++
156		if ps.streak > ps.BestStreak {
157			ps.BestStreak = ps.streak
158		}
159		if ps.Wins > topOutwitterWins {
160			topOutwitterWins = ps.Wins
161			topOutwitter = caller
162		}
163		msg = "you outwitted the oracle!"
164	case 2:
165		ps.Losses++
166		ps.streak = 0
167		oracleCorrect++
168		msg = "the oracle read you like a book."
169	default:
170		ps.Draws++
171		ps.streak = 0
172		msg = "a draw — you and the oracle picked the same move."
173	}
174
175	chain.Emit("RoundPlayed",
176		"player", caller.String(),
177		"playerMove", moveName(playerMove),
178		"oraclePredicted", moveName(predicted),
179		"oracleMove", moveName(oracleMove),
180		"result", strconv.Itoa(result),
181	)
182
183	return "you played " + moveName(playerMove) + ", the oracle predicted " +
184		moveName(predicted) + " and threw " + moveName(oracleMove) + " -> " + msg
185}
186
187func renderHome() string {
188	var b strings.Builder
189	b.WriteString("# Rock-Paper-Scissors Oracle\n\n")
190	b.WriteString("An adaptive opponent: it doesn't roll dice, it studies you. ")
191	b.WriteString("Every throw is logged, and the oracle always counters whichever ")
192	b.WriteString("move you've played most often. Play a uniform mixed strategy and ")
193	b.WriteString("it can't out-guess you; fall into a habit and it will.\n\n")
194
195	b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n")
196	if totalRounds > 0 {
197		pct := oracleCorrect * 100 / totalRounds
198		b.WriteString("- Oracle win rate: " + strconv.Itoa(pct) + "%\n")
199	}
200	if topOutwitter.IsValid() {
201		b.WriteString("- Top outwitter: `" + topOutwitter.String() + "` (" +
202			strconv.Itoa(topOutwitterWins) + " wins against the oracle)\n")
203	} else {
204		b.WriteString("- No one has beaten the oracle yet.\n")
205	}
206
207	b.WriteString("\n## How to play\n\n")
208	b.WriteString("Call `Play(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). ")
209	b.WriteString("View your own record at this realm's path plus your address, ")
210	b.WriteString("e.g. `.../rpsoracle:g1youraddress...`\n")
211	return b.String()
212}
213
214func renderPlayer(rawAddr string) string {
215	addr := strings.TrimSpace(rawAddr)
216	safe := ui.Inline(addr)
217
218	v := players.Get(addr)
219	if v == nil {
220		return "# Player " + safe + "\n\nNo recorded rounds yet.\n"
221	}
222	ps := v.(*playerState)
223
224	var b strings.Builder
225	b.WriteString("# Player " + safe + "\n\n")
226	b.WriteString("- Rounds played: " + strconv.Itoa(ps.Rounds) + "\n")
227	b.WriteString("- Beat the oracle: " + strconv.Itoa(ps.Wins) + "\n")
228	b.WriteString("- Lost to the oracle: " + strconv.Itoa(ps.Losses) + "\n")
229	b.WriteString("- Draws: " + strconv.Itoa(ps.Draws) + "\n")
230	b.WriteString("- Best win streak vs oracle: " + strconv.Itoa(ps.BestStreak) + "\n")
231	b.WriteString("- Move history — rock: " + strconv.Itoa(ps.Counts[Rock]) +
232		", paper: " + strconv.Itoa(ps.Counts[Paper]) +
233		", scissors: " + strconv.Itoa(ps.Counts[Scissors]) + "\n")
234
235	if ps.Rounds > 0 {
236		maxCount := ps.Counts[Rock]
237		for _, c := range ps.Counts[1:] {
238			if c > maxCount {
239				maxCount = c
240			}
241		}
242		predictability := maxCount * 100 / ps.Rounds
243		b.WriteString("- Predictability score: " + strconv.Itoa(predictability) +
244			"% (lower is harder for the oracle to read)\n")
245	}
246	return b.String()
247}
248
249// Render shows the oracle's dashboard at "", or one player's record when
250// path is their bech32 address.
251func Render(path string) string {
252	path = strings.TrimPrefix(strings.TrimSpace(path), "/")
253	if path == "" {
254		return renderHome()
255	}
256	return renderPlayer(path)
257}