Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pingpong.gno

3.05 Kb · 108 lines
  1// Package pingpong is a two-player game where the only move is to pass the
  2// turn. It exists to demonstrate gnotif: every turn emits TurnPlayed, and
  3// the realm declares a verified gnotif trigger that tells the next player.
  4package pingpong
  5
  6import (
  7	"chain"
  8	"strconv"
  9	"strings"
 10
 11	"gno.land/p/nt/avl/v0"
 12	"gno.land/p/nt/seqid/v0"
 13
 14	"gno.land/r/g13rqvks880v0hmlfepcj0kkudq0js38d3pc5k6a/gnotif/v0"
 15)
 16
 17// Game is a match between First and Second. First plays turn 1. It is
 18// pending until Second accepts, and only then are players named in events
 19// that notify them.
 20type Game struct {
 21	ID                  string
 22	First, Second, Next address
 23	Turn                int
 24	Started             bool
 25}
 26
 27var (
 28	games    avl.Tree // id → *Game
 29	lastGame seqid.ID
 30	declared bool
 31)
 32
 33func load(id string) *Game {
 34	if !games.Has(id) {
 35		panic("unknown game")
 36	}
 37	return games.Get(id).(*Game)
 38}
 39
 40// NewGame invites opponent to a game and returns its id. The caller plays
 41// first once opponent accepts.
 42func NewGame(cur realm, opponent address) string {
 43	caller := cur.Previous().Address()
 44	// IsValid accepts all-uppercase bech32, but a caller's address is always
 45	// lowercase, so an uppercase opponent could never accept.
 46	if !opponent.IsValid() || opponent.String() != strings.ToLower(opponent.String()) {
 47		panic("invalid opponent")
 48	}
 49	if opponent == caller {
 50		panic("cannot play against yourself")
 51	}
 52	g := &Game{ID: lastGame.Next().String(), First: caller, Second: opponent, Next: caller}
 53	games.Set(g.ID, g)
 54	chain.Emit("GameInvited", "game", g.ID, "first", g.First.String(), "second", g.Second.String())
 55	return g.ID
 56}
 57
 58// Accept starts a pending game. Only the invited player may call it.
 59func Accept(cur realm, game string) {
 60	g := load(game)
 61	if g.Second != cur.Previous().Address() {
 62		panic("not invited")
 63	}
 64	if g.Started {
 65		panic("game already started")
 66	}
 67	g.Started = true
 68	chain.Emit("GameStarted", "game", g.ID, "first", g.First.String(), "second", g.Second.String())
 69}
 70
 71// Play takes the caller's turn in game and passes it to the other player.
 72func Play(cur realm, game string) {
 73	g := load(game)
 74	if !g.Started {
 75		panic("game not started")
 76	}
 77	if g.Next != cur.Previous().Address() {
 78		panic("not your turn")
 79	}
 80	if g.Next == g.First {
 81		g.Next = g.Second
 82	} else {
 83		g.Next = g.First
 84	}
 85	g.Turn++
 86	chain.Emit("TurnPlayed", "game", g.ID, "next", g.Next.String(), "turn", strconv.Itoa(g.Turn))
 87}
 88
 89// Leave cancels a pending game or ends a started one, and deletes it.
 90func Leave(cur realm, game string) {
 91	g := load(game)
 92	caller := cur.Previous().Address()
 93	if caller != g.First && caller != g.Second {
 94		panic("not a player")
 95	}
 96	games.Remove(game)
 97	chain.Emit("GameEnded", "game", g.ID, "by", caller.String())
 98}
 99
100// DeclareTriggers declares the "your turn" trigger in the gnotif registry.
101// The registry sees this realm as the caller, so the trigger is verified.
102func DeclareTriggers(cur realm) {
103	if declared {
104		panic("triggers already declared")
105	}
106	declared = true
107	gnotif.Declare(cross(cur), cur.PkgPath(), "TurnPlayed", "", "next", "Your turn", "Game {game}, turn {turn}", "/?game={game}")
108}