PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

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/rpsduel/v0

Realm
Open in gnoweb ↗

Overview

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

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • rpsduel.gnogno
rpsduel.gnogno
1// Package rpsduel is an asynchronous, two-player rock-paper-scissors duel2// with a commit-reveal handshake. The challenger locks in a move as a3// sha256 commitment up front so the opponent can't peek at it; the4// opponent then replies in the clear (seeing only a hash gives them no5// edge); the challenger reveals last to settle the round. A challenger who6// tries to dodge a losing reveal can be forfeited by the opponent once the7// reveal window expires.8package rpsduel910import (11	"crypto/sha256"12	"encoding/hex"13	"strconv"14	"strings"1516	"chain"17	"chain/runtime"1819	"gno.land/p/nt/avl/v0"20)2122// Move is one of rock, paper, or scissors, ordered so that23// (winner - loser + 3) % 3 == 1 for every winning pair.24type Move int2526const (27	Rock Move = iota28	Paper29	Scissors30)3132func moveName(m Move) string {33	switch m {34	case Rock:35		return "rock"36	case Paper:37		return "paper"38	case Scissors:39		return "scissors"40	default:41		return "?"42	}43}4445func parseMove(s string) (Move, bool) {46	switch strings.ToLower(strings.TrimSpace(s)) {47	case "rock", "r":48		return Rock, true49	case "paper", "p":50		return Paper, true51	case "scissors", "s":52		return Scissors, true53	default:54		return 0, false55	}56}5758// judge returns 0 for a tie, 1 if p beats h, 2 if h beats p.59func judge(p, h Move) int {60	return (int(p) - int(h) + 3) % 361}6263// revealWindow is how many blocks the challenger gets to reveal after the64// opponent accepts before the opponent can claim a forfeit win.65const revealWindow int64 = 506667type duelStatus int6869const (70	statusPending duelStatus = iota71	statusAwaitingReveal72	statusResolved73	statusForfeited74	statusCancelled75)7677func (s duelStatus) String() string {78	switch s {79	case statusPending:80		return "pending"81	case statusAwaitingReveal:82		return "awaiting reveal"83	case statusResolved:84		return "resolved"85	case statusForfeited:86		return "forfeited"87	case statusCancelled:88		return "cancelled"89	default:90		return "?"91	}92}9394// duel is one asynchronous round between Challenger and Opponent. The95// challenger's move is hidden behind Commit until Reveal; the opponent's96// move is stored in the clear once they Accept.97type duel struct {98	ID             string99	Challenger     address100	Opponent       address101	Commit         string // hex sha256 of "<move>:<salt>"102	OpponentMove   Move103	Status         duelStatus104	Result         string // "challenger" | "opponent" | "draw" once settled105	Winner         address106	OpenedAt       int64107	AcceptedAt     int64108	RevealDeadline int64109}110111// playerState is the persisted record for one address.112type playerState struct {113	Wins     int114	Losses   int115	Draws    int116	Forfeits int // times this player, as challenger, failed to reveal in time117	Duels    int118}119120var (121	duels   avl.Tree // duel ID -> *duel122	players avl.Tree // address string -> *playerState123124	nextID        int125	totalResolved int126127	champion     address128	championWins int129)130131// ComputeCommit hashes a move and salt exactly the way Reveal checks it, so132// a caller can compute their commitment (e.g. via a read-only query) before133// calling Open, then remember the salt to pass to Reveal later.134func ComputeCommit(moveStr, salt string) string {135	m, ok := parseMove(moveStr)136	if !ok {137		panic("invalid move: use rock, paper, or scissors (r/p/s)")138	}139	sum := sha256.Sum256([]byte(moveName(m) + ":" + salt))140	return hex.EncodeToString(sum[:])141}142143func getOrCreate(addr address) *playerState {144	key := addr.String()145	if v := players.Get(key); v != nil {146		return v.(*playerState)147	}148	ps := &playerState{}149	players.Set(key, ps)150	return ps151}152153func bumpChampion(addr address, wins int) {154	if wins > championWins {155		championWins = wins156		champion = addr157	}158}159160// settle scores a revealed round and updates both players' records.161func settle(d *duel, challengerMove Move, result int) {162	cs := getOrCreate(d.Challenger)163	os := getOrCreate(d.Opponent)164	cs.Duels++165	os.Duels++166167	switch result {168	case 1:169		cs.Wins++170		os.Losses++171		d.Winner = d.Challenger172		d.Result = "challenger"173		bumpChampion(d.Challenger, cs.Wins)174	case 2:175		os.Wins++176		cs.Losses++177		d.Winner = d.Opponent178		d.Result = "opponent"179		bumpChampion(d.Opponent, os.Wins)180	default:181		cs.Draws++182		os.Draws++183		d.Result = "draw"184	}185	d.Status = statusResolved186	totalResolved++187}188189// Open challenges opponentAddr to a duel, locking in the challenger's move190// as a commitment (see ComputeCommit) so the opponent can't see it before191// replying.192func Open(cur realm, opponentAddr string, commitHex string) string {193	challenger := cur.Previous().Address()194195	opponent := address(strings.TrimSpace(opponentAddr))196	if !opponent.IsValid() {197		panic("invalid opponent address")198	}199	if opponent == challenger {200		panic("cannot duel yourself")201	}202203	commit := strings.ToLower(strings.TrimSpace(commitHex))204	if len(commit) != sha256.Size*2 {205		panic("commit must be a 64-character hex sha256 digest; build it with ComputeCommit")206	}207	if _, err := hex.DecodeString(commit); err != nil {208		panic("commit must be valid hex")209	}210211	nextID++212	id := strconv.Itoa(nextID)213	d := &duel{214		ID:         id,215		Challenger: challenger,216		Opponent:   opponent,217		Commit:     commit,218		Status:     statusPending,219		OpenedAt:   runtime.ChainHeight(),220	}221	duels.Set(id, d)222223	chain.Emit("DuelOpened",224		"id", id,225		"challenger", challenger.String(),226		"opponent", opponent.String(),227	)228229	return "duel #" + id + " opened against " + opponent.String() + " -- waiting for them to Accept"230}231232// Accept replies to a pending duel with a plain move. Only the challenged233// opponent may call it; going second behind the challenger's hidden234// commitment is what keeps this fair.235func Accept(cur realm, id string, moveStr string) string {236	caller := cur.Previous().Address()237238	v := duels.Get(id)239	if v == nil {240		panic("no such duel")241	}242	d := v.(*duel)243244	if d.Status != statusPending {245		panic("duel is " + d.Status.String() + ", not pending")246	}247	if caller != d.Opponent {248		panic("only the challenged opponent can accept this duel")249	}250251	m, ok := parseMove(moveStr)252	if !ok {253		panic("invalid move: use rock, paper, or scissors (r/p/s)")254	}255256	d.OpponentMove = m257	d.Status = statusAwaitingReveal258	d.AcceptedAt = runtime.ChainHeight()259	d.RevealDeadline = d.AcceptedAt + revealWindow260261	chain.Emit("DuelAccepted", "id", id, "opponentMove", moveName(m))262263	return "you played " + moveName(m) + " in duel #" + id +264		" -- waiting for " + d.Challenger.String() + " to reveal by block " +265		strconv.Itoa(int(d.RevealDeadline))266}267268// Reveal settles an accepted duel. Only the original challenger may call269// it, and only with the exact move+salt that produced their commitment.270func Reveal(cur realm, id string, moveStr string, salt string) string {271	caller := cur.Previous().Address()272273	v := duels.Get(id)274	if v == nil {275		panic("no such duel")276	}277	d := v.(*duel)278279	if d.Status != statusAwaitingReveal {280		panic("duel is " + d.Status.String() + ", not awaiting reveal")281	}282	if caller != d.Challenger {283		panic("only the challenger can reveal")284	}285286	m, ok := parseMove(moveStr)287	if !ok {288		panic("invalid move: use rock, paper, or scissors (r/p/s)")289	}290	sum := sha256.Sum256([]byte(moveName(m) + ":" + salt))291	if hex.EncodeToString(sum[:]) != d.Commit {292		panic("revealed move+salt doesn't match your original commitment")293	}294295	result := judge(m, d.OpponentMove)296	settle(d, m, result)297298	chain.Emit("DuelResolved",299		"id", id,300		"result", d.Result,301		"challengerMove", moveName(m),302		"opponentMove", moveName(d.OpponentMove),303	)304305	return "you revealed " + moveName(m) + " vs " + moveName(d.OpponentMove) + " -- " + outcomeMsg(d)306}307308func outcomeMsg(d *duel) string {309	switch d.Result {310	case "challenger":311		return "you win duel #" + d.ID + "!"312	case "opponent":313		return "you lose duel #" + d.ID + " -- " + d.Opponent.String() + " wins."314	default:315		return "draw."316	}317}318319// ClaimForfeit lets the opponent collect a default win when the challenger320// dodges revealing (e.g. because they saw the opponent's move and knew321// they'd lose) past the reveal window.322func ClaimForfeit(cur realm, id string) string {323	caller := cur.Previous().Address()324325	v := duels.Get(id)326	if v == nil {327		panic("no such duel")328	}329	d := v.(*duel)330331	if d.Status != statusAwaitingReveal {332		panic("duel is " + d.Status.String() + ", not awaiting reveal")333	}334	if caller != d.Opponent {335		panic("only the waiting opponent can claim a forfeit")336	}337	if runtime.ChainHeight() <= d.RevealDeadline {338		panic("reveal window hasn't expired yet")339	}340341	d.Status = statusForfeited342	d.Winner = d.Opponent343	d.Result = "opponent"344345	cs := getOrCreate(d.Challenger)346	cs.Duels++347	cs.Losses++348	cs.Forfeits++349	os := getOrCreate(d.Opponent)350	os.Duels++351	os.Wins++352	totalResolved++353	bumpChampion(d.Opponent, os.Wins)354355	chain.Emit("DuelForfeited", "id", id, "winner", d.Opponent.String())356357	return "duel #" + id + " forfeited -- " + d.Challenger.String() + " never revealed"358}359360// Cancel withdraws a still-pending duel. Only the challenger may cancel,361// and only before the opponent accepts.362func Cancel(cur realm, id string) string {363	caller := cur.Previous().Address()364365	v := duels.Get(id)366	if v == nil {367		panic("no such duel")368	}369	d := v.(*duel)370371	if d.Status != statusPending {372		panic("duel is " + d.Status.String() + ", not pending")373	}374	if caller != d.Challenger {375		panic("only the challenger can cancel this duel")376	}377378	d.Status = statusCancelled379	return "duel #" + id + " cancelled"380}381382// escapeInline neutralizes markdown-active characters in untrusted text383// before it's embedded inline in Render output.384func escapeInline(s string) string {385	r := strings.NewReplacer(386		"\\", "\\\\",387		"`", "\\`",388		"*", "\\*",389		"_", "\\_",390		"[", "\\[",391		"]", "\\]",392		"|", "\\|",393	)394	return r.Replace(s)395}396397func renderHome() string {398	var b strings.Builder399	b.WriteString("# Rock-Paper-Scissors Duel\n\n")400	b.WriteString("Challenge another address to rock-paper-scissors, async and " +401		"fair: you commit your move as a hash, they reply in the open, then you " +402		"reveal to settle it. Stall on revealing a losing round and your opponent " +403		"can claim a forfeit win once the window expires.\n\n")404405	b.WriteString("- Total duels resolved: " + strconv.Itoa(totalResolved) + "\n")406	if champion.IsValid() {407		b.WriteString("- Reigning champion: `" + champion.String() + "` (" +408			strconv.Itoa(championWins) + " wins)\n")409	} else {410		b.WriteString("- No champion yet -- be the first to win a duel.\n")411	}412413	b.WriteString("\n## How to play\n\n")414	b.WriteString("1. Pick a move and a random salt, e.g. `rock` + `xyz123`.\n")415	b.WriteString("2. Compute your commitment: `ComputeCommit(\"rock\", \"xyz123\")` (read-only call).\n")416	b.WriteString("3. `Open(opponentAddr, commit)` -- opens the duel, returns its ID.\n")417	b.WriteString("4. The opponent calls `Accept(id, \"paper\"|\"scissors\"|\"rock\")`.\n")418	b.WriteString("5. You call `Reveal(id, \"rock\", \"xyz123\")` -- must match your commitment exactly.\n")419	b.WriteString("6. If you never reveal, the opponent can call `ClaimForfeit(id)` after " +420		strconv.Itoa(int(revealWindow)) + " blocks.\n\n")421	b.WriteString("View a duel at this realm's path plus its ID (e.g. `.../rpsduel:3`), " +422		"or a player's record plus their address (e.g. `.../rpsduel:g1youraddress...`).\n\n")423424	b.WriteString("## Open duels\n\n")425	open := 0426	duels.Iterate("", "", func(key string, value interface{}) bool {427		d := value.(*duel)428		if d.Status == statusPending {429			open++430			b.WriteString("- #" + d.ID + ": `" + d.Challenger.String() +431				"` waiting on `" + d.Opponent.String() + "` to Accept\n")432		} else if d.Status == statusAwaitingReveal {433			open++434			b.WriteString("- #" + d.ID + ": `" + d.Challenger.String() +435				"` must Reveal by block " + strconv.Itoa(int(d.RevealDeadline)) + "\n")436		}437		return false438	})439	if open == 0 {440		b.WriteString("_none right now_\n")441	}442443	return b.String()444}445446func renderDuel(id string) string {447	v := duels.Get(id)448	if v == nil {449		return "# Duel #" + escapeInline(id) + "\n\nNo such duel.\n"450	}451	d := v.(*duel)452453	var b strings.Builder454	b.WriteString("# Duel #" + d.ID + "\n\n")455	b.WriteString("- Challenger: `" + d.Challenger.String() + "`\n")456	b.WriteString("- Opponent: `" + d.Opponent.String() + "`\n")457	b.WriteString("- Status: " + d.Status.String() + "\n")458459	if d.Status == statusAwaitingReveal {460		b.WriteString("- Opponent played: **" + moveName(d.OpponentMove) + "**\n")461		b.WriteString("- Reveal deadline: block " + strconv.Itoa(int(d.RevealDeadline)) + "\n")462	}463	if d.Status == statusResolved || d.Status == statusForfeited {464		b.WriteString("- Result: " + d.Result + "\n")465		b.WriteString("- Winner: `" + d.Winner.String() + "`\n")466	}467	return b.String()468}469470func renderPlayer(rawAddr string) string {471	addr := strings.TrimSpace(rawAddr)472	safe := escapeInline(addr)473474	v := players.Get(addr)475	if v == nil {476		return "# Player " + safe + "\n\nNo recorded duels yet.\n"477	}478	ps := v.(*playerState)479480	var b strings.Builder481	b.WriteString("# Player " + safe + "\n\n")482	b.WriteString("- Wins: " + strconv.Itoa(ps.Wins) + "\n")483	b.WriteString("- Losses: " + strconv.Itoa(ps.Losses) + "\n")484	b.WriteString("- Draws: " + strconv.Itoa(ps.Draws) + "\n")485	b.WriteString("- Duels played: " + strconv.Itoa(ps.Duels) + "\n")486	if ps.Forfeits > 0 {487		b.WriteString("- Forfeited (didn't reveal in time): " + strconv.Itoa(ps.Forfeits) + "\n")488	}489	return b.String()490}491492// Render shows the duel lobby at "", a specific duel when path is a493// numeric ID, or one player's record when path is their bech32 address.494func Render(path string) string {495	path = strings.TrimPrefix(strings.TrimSpace(path), "/")496	if path == "" {497		return renderHome()498	}499	if _, err := strconv.Atoi(path); err == nil {500		return renderDuel(path)501	}502	return renderPlayer(path)503}504

Functions

  • Accept(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}, id string, moveStr string) string

  • Cancel(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}, id string) string

  • ClaimForfeit(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}, id string) string

  • ComputeCommit(moveStr string, salt string) string

  • Open(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}, opponentAddr string, commitHex string) string

  • Render(path string) string

  • Reveal(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}, id string, moveStr string, salt 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.