Search Apps Documentation Source Content File Folder Download Copy Actions Download

rps.gno

4.61 Kb · 222 lines
  1// Package rps implements a simple two-player Rock Paper Scissors game.
  2//
  3// Note: moves are stored in plaintext on-chain. Player 2 can read
  4// Player 1's move before submitting their own. For a fair game, a
  5// commit-reveal scheme would be needed.
  6package rps
  7
  8import (
  9	"strconv"
 10	"strings"
 11
 12	"chain"
 13	"gno.land/p/nt/avl/v0"
 14)
 15
 16const (
 17	Rock     = "rock"
 18	Paper    = "paper"
 19	Scissors = "scissors"
 20)
 21
 22// Game holds state for a single match.
 23type Game struct {
 24	ID      string
 25	Player1 address
 26	Player2 address
 27	Move1   string
 28	Move2   string
 29	Status  string // "waiting" | "finished"
 30	Winner  string // "player1" | "player2" | "draw" | ""
 31}
 32
 33var (
 34	games   avl.Tree
 35	counter int
 36)
 37
 38// NewGame creates a match between the caller (player 1) and opponent (player 2).
 39// Returns the game ID.
 40func NewGame(cur realm, opponent address) string {
 41	if !cur.IsCurrent() {
 42		panic("spoofed realm")
 43	}
 44	if !opponent.IsValid() {
 45		panic("invalid opponent address")
 46	}
 47	caller := cur.Previous().Address()
 48	if caller == opponent {
 49		panic("cannot play against yourself")
 50	}
 51
 52	counter++
 53	gameID := strconv.Itoa(counter)
 54
 55	games.Set(gameID, &Game{
 56		ID:      gameID,
 57		Player1: caller,
 58		Player2: opponent,
 59		Status:  "waiting",
 60	})
 61
 62	chain.Emit("NewGame",
 63		"gameID", gameID,
 64		"player1", caller.String(),
 65		"player2", opponent.String(),
 66	)
 67
 68	return gameID
 69}
 70
 71// Play submits a move for the calling player. move must be "rock", "paper", or "scissors".
 72// The game resolves automatically when both players have submitted.
 73func Play(cur realm, gameID string, move string) {
 74	if !cur.IsCurrent() {
 75		panic("spoofed realm")
 76	}
 77
 78	move = strings.ToLower(strings.TrimSpace(move))
 79	if move != Rock && move != Paper && move != Scissors {
 80		panic("invalid move: must be rock, paper, or scissors")
 81	}
 82
 83	val, ok := games.Get(gameID)
 84	if !ok {
 85		panic("game not found: " + gameID)
 86	}
 87	game := val.(*Game)
 88
 89	if game.Status == "finished" {
 90		panic("game already finished")
 91	}
 92
 93	caller := cur.Previous().Address()
 94	switch caller {
 95	case game.Player1:
 96		if game.Move1 != "" {
 97			panic("already submitted a move")
 98		}
 99		game.Move1 = move
100	case game.Player2:
101		if game.Move2 != "" {
102			panic("already submitted a move")
103		}
104		game.Move2 = move
105	default:
106		panic("not a player in this game")
107	}
108
109	if game.Move1 != "" && game.Move2 != "" {
110		game.Status = "finished"
111		game.Winner = determineWinner(game.Move1, game.Move2)
112		chain.Emit("GameFinished",
113			"gameID", gameID,
114			"winner", game.Winner,
115			"move1", game.Move1,
116			"move2", game.Move2,
117		)
118	}
119}
120
121func determineWinner(move1, move2 string) string {
122	if move1 == move2 {
123		return "draw"
124	}
125	if beats(move1, move2) {
126		return "player1"
127	}
128	return "player2"
129}
130
131func beats(a, b string) bool {
132	return (a == Rock && b == Scissors) ||
133		(a == Paper && b == Rock) ||
134		(a == Scissors && b == Paper)
135}
136
137// Render shows all games on the home path, or a single game on /{gameID}.
138func Render(path string) string {
139	path = strings.TrimPrefix(path, "/")
140
141	if path != "" {
142		return renderGame(path)
143	}
144
145	if games.Size() == 0 {
146		return "# Rock Paper Scissors\n\nNo games yet.\n"
147	}
148
149	out := "# Rock Paper Scissors\n\n"
150	out += "| ID | Player 1 | Player 2 | Status | Winner |\n"
151	out += "|---|---|---|---|---|\n"
152
153	games.ReverseIterate("", "", func(key string, val any) bool {
154		g := val.(*Game)
155		winner := "-"
156		switch g.Winner {
157		case "player1":
158			winner = "Player 1"
159		case "player2":
160			winner = "Player 2"
161		case "draw":
162			winner = "Draw"
163		}
164		out += "| " + g.ID +
165			" | " + shortAddr(g.Player1) +
166			" | " + shortAddr(g.Player2) +
167			" | " + g.Status +
168			" | " + winner + " |\n"
169		return false
170	})
171
172	return out
173}
174
175func renderGame(gameID string) string {
176	val, ok := games.Get(gameID)
177	if !ok {
178		return "> [!WARNING]\n> Game not found: " + gameID + "\n"
179	}
180
181	g := val.(*Game)
182	out := "# Game #" + g.ID + "\n\n"
183	out += "- **Player 1**: " + g.Player1.String() + "\n"
184	out += "- **Player 2**: " + g.Player2.String() + "\n"
185	out += "- **Status**: " + g.Status + "\n\n"
186
187	if g.Status == "finished" {
188		out += "## Result\n\n"
189		out += "- Player 1 played: **" + g.Move1 + "**\n"
190		out += "- Player 2 played: **" + g.Move2 + "**\n\n"
191		switch g.Winner {
192		case "player1":
193			out += "**Player 1 wins!**\n"
194		case "player2":
195			out += "**Player 2 wins!**\n"
196		case "draw":
197			out += "**Draw!**\n"
198		}
199	} else {
200		out += "## Moves\n\n"
201		if g.Move1 == "" {
202			out += "- Player 1: waiting...\n"
203		} else {
204			out += "- Player 1: submitted\n"
205		}
206		if g.Move2 == "" {
207			out += "- Player 2: waiting...\n"
208		} else {
209			out += "- Player 2: submitted\n"
210		}
211	}
212
213	return out
214}
215
216func shortAddr(addr address) string {
217	s := addr.String()
218	if len(s) <= 14 {
219		return s
220	}
221	return s[:6] + "..." + s[len(s)-4:]
222}