notify.gno
5.91 Kb · 155 lines
1package gnoslinger
2
3import (
4 "strconv"
5 "strings"
6 "time"
7
8 "gno.land/p/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/engine/v0"
9 gnotif "gno.land/r/nym-gfanton001/gnotif/v1"
10)
11
12// Browser notifications, through gnotif (https://gnotif.xyz). At the
13// moments a player may have the game closed, the realm emits one event per
14// player concerned, naming it in the "to" attribute (PlanDue, RevealDue,
15// ShowdownOver), and declares in the gnotif registry one trigger per
16// event, which gives the notification its title, body and link. A gnotif
17// server (gnotif.xyz on Onyx; make notif runs one on gnodev) reads the
18// chain's events and pushes the notification to every browser opted in
19// with that address; the client opts in from the lobby
20// (game/notifications.ts). A deadline passing emits nothing, so a timeout
21// is never notified.
22
23// notifTriggers are the realm's triggers, by name. Title and body are
24// templates, {key} standing for the event's attribute: every event names
25// the player's character (char) and the others' (vs), and the game; the
26// link opens the game in the client (game/route.ts).
27var notifTriggers = []struct {
28 name string
29 t gnotif.Trigger
30}{
31 {"move", gnotif.Trigger{Event: "PlanDue", Param: "to", Title: "Gnoslinger · Your move", Body: "{char} vs {vs}, game {id}, turn {turn} of {turns}: {left} to send your plan.", Link: "/#game/{id}"}},
32 {"reveal", gnotif.Trigger{Event: "RevealDue", Param: "to", Title: "Gnoslinger · Reveal your plan", Body: "{char} vs {vs}, game {id}, turn {turn} of {turns}: every plan is in, yours waits to be revealed.", Link: "/#game/{id}"}},
33 {"over", gnotif.Trigger{Event: "ShowdownOver", Param: "to", Title: "Gnoslinger · Showdown over", Body: "{char} vs {vs}, game {id}: {outcome}.", Link: "/#game/{id}"}},
34}
35
36// notif is the realm's handle on its triggers in the registry, kept so that
37// DeclareTriggers can set them again: a changed title, body or link keeps
38// the trigger's id, and so the browsers opted in.
39var notif *gnotif.Handle
40
41// DeclareTriggers sets the realm's triggers in the gnotif registry. Admin
42// only, and the first call has what the registry's Open requires: the
43// namespace owner's own key (not a session), in a call sent straight to
44// the realm; so the deployer sends it once the realm is deployed (on
45// gnodev, the make notif script sends it as test1). The triggers keep
46// their ids from one call to the next. Every call makes the registry emit
47// each trigger again (TriggerDeclared), which is what a gnotif server
48// reads them from: the registry emits nothing for a trigger set as it
49// already is, so each is first set with a nudged body, then as it should
50// be. That is for gnodev, whose reload puts the earlier call in the
51// genesis, where no indexer serves its events: the script calls again
52// after each reload and the triggers reach gnotifd anew.
53func DeclareTriggers(cur realm) {
54 assertNoSend()
55 assertAdmin(cur)
56 me := cur.Sub(gnotif.Subpath)
57 if notif == nil {
58 notif = gnotif.Open(cross(me))
59 }
60 for _, nt := range notifTriggers {
61 nudged := nt.t
62 nudged.Body += " "
63 notif.Set(cross(me), nt.name, nudged)
64 notif.Set(cross(me), nt.name, nt.t)
65 }
66}
67
68// notifyPlanDue tells every player with a plan to send that the turn
69// started: its character in the fight, not committed, with time points to
70// play (one passed automatically has nothing to send).
71func (g *Game) notifyPlanDue() {
72 if g.Phase != PhasePlanning {
73 return
74 }
75 left := formatTimeout(g.Timeout)
76 for p, pl := range g.Players {
77 if g.State.Chars[p].Status == engine.Alive && !g.committed(p) && g.State.Budget(p) > 0 {
78 emit("PlanDue", "id", g.ID, "turn", strconv.Itoa(g.Turn), "turns", strconv.Itoa(g.MaxTurns), "to", pl.Addr.String(), "char", charName(pl.Char), "vs", g.others(p), "left", left)
79 }
80 }
81}
82
83// notifyRevealDue tells every player whose committed plan is not revealed
84// yet that the reveals started.
85func (g *Game) notifyRevealDue() {
86 for p, pl := range g.Players {
87 if g.State.Chars[p].Status == engine.Alive && !g.Revealed[p] {
88 emit("RevealDue", "id", g.ID, "turn", strconv.Itoa(g.Turn), "turns", strconv.Itoa(g.MaxTurns), "to", pl.Addr.String(), "char", charName(pl.Char), "vs", g.others(p))
89 }
90 }
91}
92
93// notifyOver tells every player how the finished showdown ended for it.
94func (g *Game) notifyOver() {
95 for p, pl := range g.Players {
96 outcome := "a draw"
97 switch {
98 case g.Winner == p:
99 outcome = "you won"
100 case g.Winner >= 0:
101 outcome = "you lost"
102 }
103 emit("ShowdownOver", "id", g.ID, "to", pl.Addr.String(), "char", charName(pl.Char), "vs", g.others(p), "outcome", outcome)
104 }
105}
106
107// notifyCancelled tells the players who had joined a showdown that its
108// creator cancelled it (the creator knows).
109func (g *Game) notifyCancelled(creator address) {
110 for p, pl := range g.Players {
111 if pl.Addr != "" && pl.Addr != creator {
112 emit("ShowdownOver", "id", g.ID, "to", pl.Addr.String(), "char", charName(pl.Char), "vs", g.others(p), "outcome", "cancelled by its creator, every stake given back")
113 }
114 }
115}
116
117// others names the characters of the other seated players, in seat
118// order, comma-separated: "The Kid, Reb".
119func (g *Game) others(seat int) string {
120 var names []string
121 for p, pl := range g.Players {
122 if p != seat && pl.Addr != "" {
123 names = append(names, charName(pl.Char))
124 }
125 }
126 return strings.Join(names, ", ")
127}
128
129// charName spells a character key as the client does: "the_kid" is "The
130// Kid".
131func charName(key string) string {
132 words := strings.Split(key, "_")
133 for i, w := range words {
134 if w != "" {
135 words[i] = strings.ToUpper(w[:1]) + w[1:]
136 }
137 }
138 return strings.Join(words, " ")
139}
140
141// formatTimeout spells the time a turn gives: whole days, else whole hours,
142// else minutes.
143func formatTimeout(d time.Duration) string {
144 n, unit := int64(d/time.Minute), "minute"
145 switch {
146 case d%(24*time.Hour) == 0:
147 n, unit = int64(d/(24*time.Hour)), "day"
148 case d%time.Hour == 0:
149 n, unit = int64(d/time.Hour), "hour"
150 }
151 if n != 1 {
152 unit += "s"
153 }
154 return strconv.FormatInt(n, 10) + " " + unit
155}