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

rps.gno

5.61 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 := players.Get(key); v != nil {
 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}