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.

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}