spec.gno
3.86 Kb · 153 lines
1package arena
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "strings"
7
8 "gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0"
9)
10
11// An arena is created off chain, by the game server, at a player's request.
12// It is described by a spec, a string of fields separated by "|":
13//
14// v1|mode|stake|minSeats|maxSeats|lives|seconds|killTarget|creator|created|nonce
15//
16// v1|deathmatch|10000000|2|8|3|900|5|g1abc...|1760000000|k3x9
17//
18// and known by its id, the first 24 hex characters of the SHA-256 of the
19// spec. The first player to pay for an arena hands the realm its spec; the
20// realm checks it and stores the arena under the id it computes. Nobody can
21// change the stake or the rules of an id: a different spec is a different
22// id.
23//
24// created is the server's clock when the arena was proposed. An arena takes
25// entries for an hour from then, and its lobby can be closed by anyone after
26// that, see ExpireLobby.
27
28// Bounds of a spec, the same as the game server's.
29const (
30 specVersion = "v1"
31 idLen = 24
32
33 minStake = 1000000 // ugnot, 1 GNOT
34 maxStake = 1000000000 // ugnot, 1000 GNOT
35 minSeconds = 300 // 5 minutes
36 maxSeconds = 1800 // 30 minutes
37 entryWindow = 3600 // seconds an arena takes entries after it was created
38 clockSkew = 300 // seconds a server clock may run ahead of the chain
39 maxNonceLen = 16
40 maxSpecLength = 160
41
42 // feeBps is the house's share of every settled game, for the treasury.
43 feeBps = 500
44)
45
46// spec is a parsed arena spec.
47type spec struct {
48 mode string
49 stake int64
50 minSeats int64
51 maxSeats int64
52 lives int64
53 seconds int64
54 killTarget int64
55 creator address
56 created int64
57 nonce string
58}
59
60// idOf returns the id of a spec string.
61func idOf(s string) string {
62 sum := sha256.Sum256([]byte(s))
63 return hex.EncodeToString(sum[:])[:idLen]
64}
65
66// parseSpec reads and checks a spec string. It panics on anything the realm
67// would not play.
68func parseSpec(s string) spec {
69 if len(s) > maxSpecLength {
70 panic(errInvalidSpec)
71 }
72 f := strings.Split(s, "|")
73 if len(f) != 11 || f[0] != specVersion {
74 panic(errInvalidSpec)
75 }
76 num := func(i int) int64 {
77 n, ok := digits(f[i])
78 if !ok {
79 panic(errInvalidSpec)
80 }
81 return n
82 }
83 sp := spec{
84 mode: f[1],
85 stake: num(2),
86 minSeats: num(3),
87 maxSeats: num(4),
88 lives: num(5),
89 seconds: num(6),
90 killTarget: num(7),
91 creator: address(f[8]),
92 created: num(9),
93 nonce: f[10],
94 }
95 if payout.Lookup(sp.mode) == nil {
96 panic(errInvalidMode)
97 }
98 if sp.stake < minStake || sp.stake > maxStake {
99 panic(errInvalidStake)
100 }
101 if sp.minSeats < payout.MinPlayers || sp.minSeats > sp.maxSeats || sp.maxSeats > payout.MaxPlayers {
102 panic(errInvalidSeats)
103 }
104 if sp.lives < payout.MinLives || sp.lives > payout.MaxLives {
105 panic(errInvalidLives)
106 }
107 if sp.seconds < minSeconds || sp.seconds > maxSeconds {
108 panic(errInvalidSeconds)
109 }
110 if sp.killTarget > payout.MaxKillTarget {
111 panic(errInvalidKillTarget)
112 }
113 // A battle royale is won by staying in the air: a kill target would end
114 // it before that.
115 if sp.mode == payout.ModeBattleRoyale && sp.killTarget != 0 {
116 panic(errRoyaleTarget)
117 }
118 if !validAddress(sp.creator) {
119 panic(errInvalidAddress)
120 }
121 if !validNonce(sp.nonce) {
122 panic(errInvalidSpec)
123 }
124 return sp
125}
126
127// validNonce reports whether s is 1 to 16 characters of [a-z0-9].
128func validNonce(s string) bool {
129 if len(s) == 0 || len(s) > maxNonceLen {
130 return false
131 }
132 for i := 0; i < len(s); i++ {
133 c := s[i]
134 if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') {
135 return false
136 }
137 }
138 return true
139}
140
141// validID reports whether s can be an arena id: 24 lowercase hex characters.
142func validID(s string) bool {
143 if len(s) != idLen {
144 return false
145 }
146 for i := 0; i < len(s); i++ {
147 c := s[i]
148 if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') {
149 return false
150 }
151 }
152 return true
153}