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.

arena.gno

4.55 Kb · 160 lines
  1// Package arena holds Gnomepit's online matches. A challenger posts a sealed deck and
  2// round-1 plan; an acceptor answers face up; the challenger reveals each round with the
  3// seal of the next, and the shared duel engine checks and resolves every move. A session
  4// key is bound to the match of the first call it signs: it then acts in no other match and
  5// posts or accepts nothing more. Calls the wallet signs are never bound.
  6package arena
  7
  8import (
  9	"encoding/base64"
 10	"strconv"
 11	"strings"
 12	"time"
 13
 14	"gno.land/p/g188vafzutzvlfw6wt7u3s62xtuvm0epmu8ufmfz/gnomepit/engine"
 15)
 16
 17const (
 18	moveWindow = 24 * 60 * 60     // seconds a player has for each move
 19	openWindow = 7 * 24 * 60 * 60 // seconds an open challenge stays on the board
 20	maxBox     = 104              // bytes
 21	maxPage    = 50
 22	maxCleared = 8 // expired challenges one Challenge deletes at most
 23	idDigits   = 10
 24)
 25
 26const (
 27	phaseOpen    = "open"
 28	phaseReveal  = "reveal"
 29	phasePlay    = "play"
 30	phaseEnded   = "ended"
 31	phaseExpired = "expired" // reported for an open match past its expiry, never stored
 32)
 33
 34var lastID int
 35
 36// OpenChallenge is an open challenge as the board lists it.
 37type OpenChallenge struct {
 38	ID, Challenger, Clan, Opponent string
 39	Posted, Expires                int64
 40}
 41
 42// Challenge posts a challenge for clan with the seals of the challenger's deck and
 43// round-1 plan, the box holding both, and an opponent who alone may accept, or none.
 44// It returns the new match's id. It first deletes up to 8 expired challenges, whoever
 45// posted them.
 46func Challenge(cur realm, clan, deckSeal, planSeal, box string, opponent address) string {
 47	rules := engine.ProofOfConcept()
 48	c := clanNumber(rules, clan)
 49	if c == 0 {
 50		panic("unknown clan")
 51	}
 52	if !validSeal(deckSeal) || !validSeal(planSeal) {
 53		panic("invalid seal")
 54	}
 55	raw := boxBytes(box)
 56	if opponent != "" && !canonical(opponent) {
 57		panic("invalid opponent")
 58	}
 59	now := time.Now().Unix()
 60	for _, id := range openClear(now, maxCleared) {
 61		if m, ok := loadMatch(rules, id); ok {
 62			forget(id, m.challenger)
 63		}
 64	}
 65
 66	lastID++
 67	m := record{
 68		phase:      phaseOpen,
 69		deadline:   now + openWindow,
 70		challenger: cur.Previous().Address(),
 71		opponent:   opponent,
 72		deckSeal:   deckSeal,
 73		seal:       planSeal,
 74		box:        raw,
 75	}
 76	m.clans[0] = c
 77	join(cur.Previous().IsUser(), m.challenger, lastID)
 78	saveMatch(rules, lastID, m)
 79	openAdd(lastID, m.deadline)
 80	tellBoard(lastID, clan+" posted a challenge.")
 81	return strconv.Itoa(lastID)
 82}
 83
 84// Open lists open, unexpired challenges, newest first: limit of them after skipping
 85// offset. limit is 1 to 50.
 86func Open(offset, limit int) []OpenChallenge {
 87	checkPage(offset, limit)
 88	rules := engine.ProofOfConcept()
 89	list := []OpenChallenge{}
 90	for _, id := range openNewest(time.Now().Unix(), offset, limit) {
 91		m, ok := loadMatch(rules, id)
 92		if !ok {
 93			continue
 94		}
 95		list = append(list, OpenChallenge{
 96			ID:         strconv.Itoa(id),
 97			Challenger: m.challenger.String(),
 98			Clan:       clanName(rules, m.clans[0]),
 99			Opponent:   m.opponent.String(),
100			Posted:     m.deadline - openWindow,
101			Expires:    m.deadline,
102		})
103	}
104	return list
105}
106
107// forget deletes open challenge id and its place among its challenger's matches. Its
108// entry on the open list is the caller's to remove.
109func forget(id int, challenger address) {
110	dropMatch(id)
111	p := loadPlayer(challenger)
112	p.remove(id)
113	savePlayer(challenger, p)
114}
115
116func (m record) expired(now int64) bool {
117	return m.phase == phaseOpen && now > m.deadline
118}
119
120func checkPage(offset, limit int) {
121	if offset < 0 {
122		panic("offset must not be negative")
123	}
124	if limit < 1 || limit > maxPage {
125		panic("limit must be 1 to 50")
126	}
127}
128
129// boxBytes decodes a box argument: the standard base64 of 1 to maxBox bytes, in the one
130// form that encodes them, so Match returns every box exactly as it was sent.
131func boxBytes(text string) string {
132	if len(text) == 0 || len(text) > base64.StdEncoding.EncodedLen(maxBox) {
133		panic("invalid box")
134	}
135	raw, err := base64.StdEncoding.DecodeString(text)
136	if err != nil || len(raw) > maxBox || base64.StdEncoding.EncodeToString(raw) != text {
137		panic("invalid box")
138	}
139	return string(raw)
140}
141
142// canonical accepts a valid address in the lowercase form callers carry. bech32 also
143// allows uppercase, which would never equal a caller's address.
144func canonical(a address) bool {
145	return a.IsValid() && string(a) == strings.ToLower(string(a))
146}
147
148// validSeal accepts a sha256 sum in lowercase hex.
149func validSeal(s string) bool {
150	if len(s) != 64 {
151		return false
152	}
153	for i := 0; i < len(s); i++ {
154		c := s[i]
155		if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
156			return false
157		}
158	}
159	return true
160}