PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/moul/x/daily/rpsmatch/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
moul / x / daily / rpsmatch
Files
3 (README)(gnomod.toml)
Exported functions
2
Module
gno.land/r/moul/x/daily/rpsmatch/v0
gno
0.9

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • rpsmatch.gnogno
rpsmatch.gnogno
1// Package rpsmatch is a best-of-three rock-paper-scissors arena against the2// house. Unlike a single stateless throw, a match persists across calls:3// the first player to win two rounds takes the match, and every player's4// match record accumulates on-chain.5package rpsmatch67import (8	"strconv"9	"strings"1011	"chain"12	"chain/runtime"1314	"gno.land/p/nt/avl/v0"15)1617// Move is one of rock, paper, or scissors, ordered so that18// (winner - loser + 3) % 3 == 1 for every winning pair.19type Move int2021const (22	Rock Move = iota23	Paper24	Scissors25)2627// winsNeeded is the number of round wins required to take a match.28const winsNeeded = 22930// match tracks an in-progress best-of-three series for one player.31type match struct {32	PlayerWins int33	HouseWins  int34	Rounds     int35}3637// playerState is the persisted record for one address.38type playerState struct {39	Active        *match40	MatchWins     int41	MatchLosses   int42	MatchesPlayed int43	RoundsPlayed  int44}4546var (47	players avl.Tree // address string -> *playerState4849	nonce       int50	totalRounds int5152	champion     address53	championWins int54)5556func moveName(m Move) string {57	switch m {58	case Rock:59		return "rock"60	case Paper:61		return "paper"62	case Scissors:63		return "scissors"64	default:65		return "?"66	}67}6869func parseMove(s string) (Move, bool) {70	switch strings.ToLower(strings.TrimSpace(s)) {71	case "rock", "r":72		return Rock, true73	case "paper", "p":74		return Paper, true75	case "scissors", "s":76		return Scissors, true77	default:78		return 0, false79	}80}8182// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.83func judge(p, h Move) int {84	return (int(p) - int(h) + 3) % 385}8687// pickHouseMove derives a deterministic pseudo-random move from the current88// chain height, a monotonic per-realm nonce, and the caller's address, so89// repeated throws in the same block still diverge.90func pickHouseMove(caller address, n int) Move {91	seed := runtime.ChainHeight() + int64(n)92	s := caller.String()93	for i := 0; i < len(s); i++ {94		seed += int64(s[i])95	}96	if seed < 0 {97		seed = -seed98	}99	return Move(seed % 3)100}101102func getOrCreate(addr address) *playerState {103	key := addr.String()104	if v := players.Get(key); v != nil {105		return v.(*playerState)106	}107	ps := &playerState{}108	players.Set(key, ps)109	return ps110}111112// Throw plays one round of the caller's current match, starting a fresh113// match if none is in progress. Accepts "rock"/"paper"/"scissors" or the114// single-letter shorthand "r"/"p"/"s".115func Throw(cur realm, moveStr string) string {116	if !cur.IsCurrent() {117		panic("invalid realm")118	}119	caller := cur.Previous().Address()120121	playerMove, ok := parseMove(moveStr)122	if !ok {123		panic("invalid move: use rock, paper, or scissors (r/p/s)")124	}125126	ps := getOrCreate(caller)127	if ps.Active == nil {128		ps.Active = &match{}129	}130	m := ps.Active131132	nonce++133	houseMove := pickHouseMove(caller, nonce)134	round := judge(playerMove, houseMove)135136	m.Rounds++137	totalRounds++138	ps.RoundsPlayed++139140	var roundMsg string141	switch round {142	case 1:143		m.PlayerWins++144		roundMsg = "you win the round"145	case 2:146		m.HouseWins++147		roundMsg = "house wins the round"148	default:149		roundMsg = "round tied"150	}151152	out := "round " + strconv.Itoa(m.Rounds) + ": you played " + moveName(playerMove) +153		", house played " + moveName(houseMove) + " -> " + roundMsg +154		" (score " + strconv.Itoa(m.PlayerWins) + "-" + strconv.Itoa(m.HouseWins) + ")"155156	if m.PlayerWins >= winsNeeded || m.HouseWins >= winsNeeded {157		ps.Active = nil158		ps.MatchesPlayed++159		playerTookMatch := m.PlayerWins > m.HouseWins160		if playerTookMatch {161			ps.MatchWins++162			if ps.MatchWins > championWins {163				championWins = ps.MatchWins164				champion = caller165			}166			out += ". MATCH WON!"167		} else {168			ps.MatchLosses++169			out += ". match lost."170		}171		chain.Emit("MatchFinished",172			"player", caller.String(),173			"playerWins", strconv.Itoa(m.PlayerWins),174			"houseWins", strconv.Itoa(m.HouseWins),175		)176	}177178	chain.Emit("RoundPlayed",179		"player", caller.String(),180		"playerMove", moveName(playerMove),181		"houseMove", moveName(houseMove),182		"result", strconv.Itoa(round),183	)184185	return out186}187188func renderHome() string {189	var b strings.Builder190	b.WriteString("# Rock-Paper-Scissors Arena\n\n")191	b.WriteString("Best-of-three matches against the house. First to " +192		strconv.Itoa(winsNeeded) + " round wins takes the match.\n\n")193	b.WriteString("- Total rounds played: " + strconv.Itoa(totalRounds) + "\n")194195	if champion.IsValid() {196		b.WriteString("- Reigning champion: `" + champion.String() +197			"` (" + strconv.Itoa(championWins) + " match wins)\n")198	} else {199		b.WriteString("- No champion yet — be the first to win a match.\n")200	}201202	b.WriteString("\n## How to play\n\n")203	b.WriteString("Call `Throw(\"rock\"|\"paper\"|\"scissors\")` (or `r`/`p`/`s`). " +204		"Your throw starts a new match if you don't have one in progress, " +205		"and each call plays one round of it.\n\n")206	b.WriteString("View your own record at this realm's path plus your address, " +207		"e.g. `.../rpsmatch:g1youraddress...`\n")208	return b.String()209}210211// escapeInline neutralizes markdown-active characters in untrusted text212// before it's embedded inline in Render output.213func escapeInline(s string) string {214	r := strings.NewReplacer(215		"\\", "\\\\",216		"`", "\\`",217		"*", "\\*",218		"_", "\\_",219		"[", "\\[",220		"]", "\\]",221		"|", "\\|",222	)223	return r.Replace(s)224}225226func renderPlayer(rawAddr string) string {227	addr := strings.TrimSpace(rawAddr)228	safe := escapeInline(addr)229230	v := players.Get(addr)231	if v == nil {232		return "# Player " + safe + "\n\nNo recorded throws yet.\n"233	}234	ps := v.(*playerState)235236	var b strings.Builder237	b.WriteString("# Player " + safe + "\n\n")238	b.WriteString("- Matches won: " + strconv.Itoa(ps.MatchWins) + "\n")239	b.WriteString("- Matches lost: " + strconv.Itoa(ps.MatchLosses) + "\n")240	b.WriteString("- Matches played: " + strconv.Itoa(ps.MatchesPlayed) + "\n")241	b.WriteString("- Rounds played: " + strconv.Itoa(ps.RoundsPlayed) + "\n")242243	if ps.Active != nil {244		b.WriteString("\n**Match in progress:** " + strconv.Itoa(ps.Active.PlayerWins) +245			"-" + strconv.Itoa(ps.Active.HouseWins) + " through " +246			strconv.Itoa(ps.Active.Rounds) + " round(s).\n")247	}248	return b.String()249}250251// Render shows the arena dashboard at "", or one player's record when path252// is their bech32 address.253func Render(path string) string {254	path = strings.TrimPrefix(strings.TrimSpace(path), "/")255	if path == "" {256		return renderHome()257	}258	return renderPlayer(path)259}260

Functions

  • Render(path string) string

  • Throw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, moveStr string) string

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.