Search Apps Documentation Source Content File Folder Download Copy Actions Download

rps.gno

5.60 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, ok := players.Get(key); ok {
 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}