Transaction

59E43835399834…97E59A52F020

Block 26,020 · index 0 · indexed

Summary

Hash
59E43835399834B16EAEAD8FE0EA0EDA6A01B0B8EB5C1EB0613997E59A52F020
Block
26,020
Size
9663 bytes
Gas used
14,049,100 / 16,858,980
Fee
1000000ugnot
Memo
gnopublish
Status
success

Messages

Arguments · 9

  1. #1rpsmatch
  2. #2README.md
  3. #3# Rock-Paper-Scissors Arena > ⚠️ **Experimental — generated with no human supervision.** This realm was > produced automatically by an MCP-driven agent to exercise the gno MCP server > and tooling, and to generate test content for gno compilers, linters and > formatters. **Not audited. Not for production.** Full context & folder README: > [r/moul/x/daily](https://github.com/moul/gno-contracts/blob/main/r/moul/x/daily/README.md) --- Best-of-three rock-paper-scissors against the house, played across multiple calls instead of a single stateless throw. Each `Throw` plays one round of your current match (starting a new one if you don't have one in progress); the house's move is derived deterministically from the current block height, a per-realm nonce, and your address. First to two round wins takes the match, and every player's match record (wins, losses, rounds played) is kept on-chain and viewable per-address. **Realm path:** `gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/rpsmatch` ## Example calls ```sh # Play one round of your current (or a new) match gnokey maketx call \ -pkgpath "gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/rpsmatch" \ -func Throw -args "rock" \ -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid sapphire-1 <key> # View the arena dashboard gnokey query vm/qrender \ --data "gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/rpsmatch:" # View one player's record gnokey query vm/qrender \ --data "gno.land/r/g12cs4cehujpffpjpywmkqj43m6u5ya53nj69sjz/rpsmatch:g1youraddress..." ``` ## Toolchain Written against the sapphire (gno 0.9) stdlib split (`chain`, `chain/runtime`, `gno.land/p/nt/avl/v0`). The `gno` binary on `PATH` here is a newer/older mismatch of its own `GNOROOT` (native-function drift), so tests were run against a `gno` built from the exact pseudo-version pinned in that binary's own module info (`github.com/gnolang/gno@v0.0.0-20260605043206-bf5b31eda8b9`). All 5 tests pass (`gno test -v .`) and `gno lint .` is clean. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:** ![gno.land/r/moul/x/daily/rpsmatch/v0 dependency graph](https://raw.githubusercontent.com/moul/gno-contracts/main/_assets/gno.land/r/moul/x/daily/rpsmatch/v0/deps.png) > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
  4. #4gnomod.toml
  5. #5module = "gno.land/r/moul/x/daily/rpsmatch/v0" gno = "0.9"
  6. #6rpsmatch.gno
  7. #7// Package rpsmatch is a best-of-three rock-paper-scissors arena against the // house. Unlike a single stateless throw, a match persists across calls: // the first player to win two rounds takes the match, and every player's // match record accumulates on-chain. package rpsmatch import ( "strconv" "strings" "chain" "chain/runtime" "gno.land/p/nt/avl/v0" ) // Move is one of rock, paper, or scissors, ordered so that // (winner - loser + 3) % 3 == 1 for every winning pair. type Move int const ( Rock Move = iota Paper Scissors ) // winsNeeded is the number of round wins required to take a match. const winsNeeded = 2 // match tracks an in-progress best-of-three series for one player. type match struct { PlayerWins int HouseWins int Rounds int } // playerState is the persisted record for one address. type playerState struct { Active *match MatchWins int MatchLosses int MatchesPlayed int RoundsPlayed int } var ( players avl.Tree // address string -> *playerState nonce int totalRounds int champion address championWins int ) func moveName(m Move) string { switch m { case Rock: return "rock" case Paper: return "paper" case Scissors: return "scissors" default: return "?" } } func parseMove(s string) (Move, bool) { switch strings.ToLower(strings.TrimSpace(s)) { case "rock", "r": return Rock, true case "paper", "p": return Paper, true case "scissors", "s": return Scissors, true default: return 0, false } } // judge returns 0 for a tie, 1 if p beats h, 2 if h beats p. func judge(p, h Move) int { return (int(p) - int(h) + 3) % 3 } // pickHouseMove derives a deterministic pseudo-random move from the current // chain height, a monotonic per-realm nonce, and the caller's address, so // repeated throws in the same block still diverge. func pickHouseMove(caller address, n int) Move { seed := runtime.ChainHeight() + int64(n) s := caller.String() for i := 0; i < len(s); i++ { seed += int64(s[i]) } if seed < 0 { seed = -seed } return Move(seed % 3) } func getOrCreate(addr address) *playerState { key := addr.String() if v := players.Get(key); v != nil { return v.(*playerState) } ps := &playerState{} players.Set(key, ps) return ps } // Throw plays one round of the caller's current match, starting a fresh // match if none is in progress. Accepts "rock"/"paper"/"scissors" or the // single-letter shorthand "r"/"p"/"s". func Throw(cur realm, moveStr string) string { if !cur.IsCurrent() { panic("invalid realm") } caller := cur.Previous().Address() playerMove, ok := parseMove(moveStr) if !ok { panic("invalid move: use rock, paper, or scissors (r/p/s)") } ps := getOrCreate(caller) if ps.Active == nil { ps.Active = &match{} } m := ps.Active nonce++ houseMove := pickHouseMove(caller, nonce) round := judge(playerMove, houseMove) m.Rounds++ totalRounds++ ps.RoundsPlayed++ var roundMsg string switch round { case 1: m.PlayerWins++ roundMsg = "you win the round" case 2: m.HouseWins++ roundMsg = "house wins the round" default: roundMsg = "round tied" } out := "round " + strconv.Itoa(m.Rounds) + ": you played " + moveName(playerMove) + ", house played " + moveName(houseMove) + " -> " + roundMsg + " (score " + strconv.Itoa(m.PlayerWins) + "-" + strconv.Itoa(m.HouseWins) + ")" if m.PlayerWins >= winsNeeded || m.HouseWins >= winsNeeded { ps.Active = nil ps.MatchesPlayed++ playerTookMatch := m.PlayerWins > m.HouseWins if playerTookMatch { ps.MatchWins++ if ps.MatchWins > championWins { championWins = ps.MatchWins champion = caller } out += ". MATCH WON!" } else { ps.MatchLosses++ out += ". match lost." } chain.Emit("MatchFinished", "player", caller.String(), "playerWins", strconv.Itoa(m.PlayerWins), "houseWins", strconv.Itoa(m.HouseWins), ) } chain.Emit("RoundPlayed", "player", caller.String(), "playerMove", moveName(playerMove), "houseMove", moveName(houseMove), "result", strconv.Itoa(round), ) return out } func renderHome() string { var b strings.Builder b.WriteString("# Rock-Paper-Scissors Arena\n\n") b.WriteString("Best-of-three matches against the house. First to " + strconv.Itoa(winsNeeded) + " round wins takes the match.\n\n") b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n") if champion.IsValid() { b.WriteString("- Reigning champion: `" + champion.String() + "` (" + strconv.Itoa(championWins) + " match wins)\n") } else { b.WriteString("- No champion yet — be the first to win a match.\n") } b.WriteString("\n## How to play\n\n") b.WriteString("Call `Throw(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). " + "Your throw starts a new match if you don't have one in progress, " + "and each call plays one round of it.\n\n") b.WriteString("View your own record at this realm's path plus your address, " + "e.g. `.../rpsmatch:g1youraddress...`\n") return b.String() } // escapeInline neutralizes markdown-active characters in untrusted text // before it's embedded inline in Render output. func escapeInline(s string) string { r := strings.NewReplacer( "\\", "\\\\", "`", "\\`", "*", "\\*", "_", "\\_", "[", "\\[", "]", "\\]", "|", "\\|", ) return r.Replace(s) } func renderPlayer(rawAddr string) string { addr := strings.TrimSpace(rawAddr) safe := escapeInline(addr) v := players.Get(addr) if v == nil { return "# Player " + safe + "\n\nNo recorded throws yet.\n" } ps := v.(*playerState) var b strings.Builder b.WriteString("# Player " + safe + "\n\n") b.WriteString("- Matches won: " + strconv.Itoa(ps.MatchWins) + "\n") b.WriteString("- Matches lost: " + strconv.Itoa(ps.MatchLosses) + "\n") b.WriteString("- Matches played: " + strconv.Itoa(ps.MatchesPlayed) + "\n") b.WriteString("- Rounds played: " + strconv.Itoa(ps.RoundsPlayed) + "\n") if ps.Active != nil { b.WriteString("\n**Match in progress:** " + strconv.Itoa(ps.Active.PlayerWins) + "-" + strconv.Itoa(ps.Active.HouseWins) + " through " + strconv.Itoa(ps.Active.Rounds) + " round(s).\n") } return b.String() } // Render shows the arena dashboard at "", or one player's record when path // is their bech32 address. func Render(path string) string { path = strings.TrimPrefix(strings.TrimSpace(path), "/") if path == "" { return renderHome() } return renderPlayer(path) }
  8. #8/gno.MemPackageType
  9. #9 MPUserProd

Result log

msg:0,success:true,log:,events:[]

← Back to block 26,020