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}