fishlive.gno
11.20 Kb · 436 lines
1// Package fishlive is the person-versus-person sea.
2// CommitMint pays 150 GNOT and stores only the caller and the commit height.
3// RevealMint, once the chain is at least two blocks later, rolls rarity and trait
4// from sha256 of the caller, the commit height, that fixed height plus two, and the id.
5// Waiting longer does not change the roll. The player does not pass trait, size, or rarity.
6// Swim stays off chain. Register locks at least 10 GNOT. Leave returns it after 5 seconds.
7// Settle pays one covered stake. There is no machine fish and no FISH draw.
8// A seated wallet can settle any other seated fish. The realm does not replay the bite.
9// The five older commons stay on shoal/fish. This package does not move them.
10// fishlive-1.
11package fishlive
12
13import (
14 "chain"
15 "chain/banker"
16 "chain/runtime"
17 "chain/runtime/unsafe"
18 "crypto/sha256"
19 "strconv"
20 "time"
21)
22
23const (
24 denom = "ugnot"
25 mintFee int64 = 150_000_000
26 stakeMin int64 = 10_000_000
27 quietMs int64 = 5_000
28 capFish = 128
29)
30
31var (
32 hpBase = [6]int{100, 122, 148, 178, 214, 256}
33 biteBase = [6]int{24, 30, 37, 45, 54, 66}
34 spdBase = [6]int{108, 104, 100, 96, 92, 88}
35 species = [6]string{"Bubble", "Tideblade", "Abysswing", "Embermaw", "Aurelion", "Astravex"}
36 rarities = [6]string{"common", "uncommon", "rare", "epic", "legendary", "mythic"}
37 traits = [3]string{"swift", "fang", "hide"}
38)
39
40type fish struct {
41 owner address
42 name string
43 rarity string
44 trait string
45 bite int
46 hpMax int
47 hp int
48 speed int
49 pool int64
50 bouts int
51 commitH int64
52 touched int64
53 inSea bool
54 revealed bool
55 mustHeal bool
56}
57
58var (
59 inited bool
60 self address
61 lpSink address
62 team address
63 burn address
64 minted int
65 // A fixed [128]fish of this struct is refused by the Onyx approver.
66 // Index 0 stays empty so id and index stay equal.
67 roster []fish
68)
69
70// Init records three GNOT sinks. They must be distinct. Call once, from an EOA, with no coins.
71// Mint is 60/20/10 and the last tenth joins team. Kill cheer also joins team.
72func Init(cur realm, lp, crew, ash address) {
73 if !cur.Previous().IsUserCall() {
74 panic("fishlive: eoa")
75 }
76 takeExact(0)
77 if inited || !lp.IsValid() || !crew.IsValid() || !ash.IsValid() || lp == crew || lp == ash || crew == ash {
78 panic("fishlive: init")
79 }
80 inited = true
81 self = cur.Address()
82 lpSink = lp
83 team = crew
84 burn = ash
85 chain.Emit("Init", "self", self.String())
86}
87
88// CommitMint sells one unrevealed fish for 150 GNOT. The fee leaves this realm in the same call.
89func CommitMint(cur realm) int {
90 caller := gate(cur, mintFee)
91 if minted >= capFish || pending(caller) != 0 {
92 panic("fishlive: mint")
93 }
94 height := runtime.ChainHeight()
95 if height < 1 {
96 panic("fishlive: mint")
97 }
98 id := grow()
99 roster[id].owner = caller
100 roster[id].commitH = height
101 payMint(mintFee, cur)
102 chain.Emit("Commit", "id", strconv.Itoa(id), "owner", caller.String())
103 return id
104}
105
106// RevealMint assigns rarity, trait, and the species name. Entropy is the commit height plus two.
107func RevealMint(cur realm, id int) {
108 caller := gate(cur, 0)
109 row := mustFish(id)
110 entropyH := row.commitH + 2
111 if row.owner != caller || row.revealed || runtime.ChainHeight() < entropyH {
112 panic("fishlive: reveal")
113 }
114 rarity, trait, bite, hp, speed := Roll(caller, row.commitH, entropyH, id)
115 row.rarity = rarity
116 row.trait = trait
117 row.bite = bite
118 row.hpMax = hp
119 row.hp = hp
120 row.speed = speed
121 row.name = pickName(tierOf(rarity))
122 row.revealed = true
123 chain.Emit("Reveal", "id", strconv.Itoa(id), "rarity", rarity, "trait", trait, "name", row.name)
124}
125
126// Register seats one revealed fish. The attached coins are the stake.
127func Register(cur realm, id int, stake int64) {
128 caller := gate(cur, stake)
129 row := mustFish(id)
130 if stake < stakeMin || row.owner != caller || !row.revealed || row.inSea || row.mustHeal || seated(caller) {
131 panic("fishlive: register")
132 }
133 row.pool = stake
134 row.inSea = true
135 row.touched = time.Now().UnixMilli()
136 chain.Emit("Register", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
137}
138
139// Leave returns the seated stake once 5 seconds have passed since Register.
140func Leave(cur realm, id int) {
141 caller := gate(cur, 0)
142 row := mustFish(id)
143 if row.owner != caller || !row.inSea || time.Now().UnixMilli() < row.touched+quietMs {
144 panic("fishlive: leave")
145 }
146 stake := row.pool
147 row.pool = 0
148 row.inSea = false
149 send(caller, stake, cur)
150 chain.Emit("Leave", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
151}
152
153// Settle pays the smaller stake to the killer's books. The NFT stays. HP falls to 30% and must heal.
154// The caller has to own the killer. This realm does not check who bit whom.
155func Settle(cur realm, id int, target int) {
156 caller := gate(cur, 0)
157 if id == target {
158 panic("fishlive: settle")
159 }
160 atk := mustFish(id)
161 vic := mustFish(target)
162 if atk.owner != caller || !atk.inSea || !vic.inSea || atk.owner == vic.owner || atk.pool < stakeMin || vic.pool < stakeMin {
163 panic("fishlive: settle")
164 }
165 covered := atk.pool
166 if vic.pool < covered {
167 covered = vic.pool
168 }
169 excess := vic.pool - covered
170 owner := vic.owner
171 vic.pool = 0
172 vic.inSea = false
173 vic.mustHeal = true
174 vic.hp = vic.hpMax * 30 / 100
175 if vic.hp < 1 {
176 vic.hp = 1
177 }
178 atk.bouts++
179 vic.bouts++
180 killer, lp, crew, buyback := splitKill(covered)
181 send(atk.owner, killer, cur)
182 send(lpSink, lp, cur)
183 send(team, crew, cur)
184 send(burn, buyback, cur)
185 send(owner, excess, cur)
186 chain.Emit("Kill", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "covered", strconv.FormatInt(covered, 10))
187}
188
189// Heal clears mustHeal outside the sea so Register can run again.
190func Heal(cur realm, id int) {
191 caller := gate(cur, 0)
192 row := mustFish(id)
193 if row.owner != caller || row.inSea || !row.mustHeal || !row.revealed {
194 panic("fishlive: heal")
195 }
196 row.mustHeal = false
197 row.hp = row.hpMax
198 chain.Emit("Heal", "id", strconv.Itoa(id))
199}
200
201// Roll is the reveal hash. entropyH must be commitH+2. Trait adds 12%, half up.
202func Roll(caller address, commitH int64, entropyH int64, id int) (rarity string, trait string, bite int, hp int, speed int) {
203 if id < 1 || commitH < 1 || entropyH != commitH+2 {
204 panic("fishlive: roll")
205 }
206 raw := caller.String() + "|" + strconv.FormatInt(commitH, 10) + "|" + strconv.FormatInt(entropyH, 10) + "|" + strconv.Itoa(id)
207 sum := sha256.Sum256([]byte(raw))
208 n := (int(sum[0])<<8 | int(sum[1])) % 10000
209 tier := 0
210 if n == 0 {
211 tier = 5
212 } else if n < 4 {
213 tier = 4
214 } else if n < 14 {
215 tier = 3
216 } else if n < 54 {
217 tier = 2
218 } else if n < 254 {
219 tier = 1
220 }
221 kind := int(sum[2]) % 3
222 bite = biteBase[tier]
223 hp = hpBase[tier]
224 speed = spdBase[tier]
225 if kind == 0 {
226 speed = (speed*112 + 50) / 100
227 } else if kind == 1 {
228 bite = (bite*112 + 50) / 100
229 } else {
230 hp = (hp*112 + 50) / 100
231 }
232 return rarities[tier], traits[kind], bite, hp, speed
233}
234
235func Realm() address { return self }
236func Version() string { return "fishlive-1" }
237func Name() string { return "Shoal Live" }
238func Symbol() string { return "LIVE" }
239func MintPrice() int64 { return mintFee }
240func StakeMin() int64 { return stakeMin }
241func Minted() int { return minted }
242func Height() int64 { return runtime.ChainHeight() }
243
244func OwnerOf(id string) string {
245 return mustFish(atoi(id)).owner.String()
246}
247
248func TokenURI(id string) string {
249 row := mustFish(atoi(id))
250 if !row.revealed {
251 panic("fishlive: uri")
252 }
253 return `{"name":"` + row.name + `","image":""}`
254}
255
256func TokensOf(owner string) string {
257 who := address(owner)
258 if !who.IsValid() {
259 return ""
260 }
261 out := ""
262 for i := 1; i <= minted; i++ {
263 if roster[i].owner != who {
264 continue
265 }
266 if out != "" {
267 out += ","
268 }
269 out += strconv.Itoa(i)
270 }
271 return out
272}
273
274func TraitOf(id int) string { return mustFish(id).trait }
275func RarityOf(id int) string { return mustFish(id).rarity }
276func SizeOf(id int) string {
277 mustFish(id)
278 return "medium"
279}
280func BoutsOf(id int) int { return mustFish(id).bouts }
281func InMatch(id int) bool { return mustFish(id).inSea }
282func PoolOf(id int) int64 { return mustFish(id).pool }
283func Revealed(id int) bool { return mustFish(id).revealed }
284func MustHeal(id int) bool { return mustFish(id).mustHeal }
285func CommitOf(id int) int64 { return mustFish(id).commitH }
286func HpOf(id int) int { return mustFish(id).hp }
287func BiteOf(id int) int { return mustFish(id).bite }
288func SpeedOf(id int) int { return mustFish(id).speed }
289func NameOf(id int) string { return mustFish(id).name }
290
291func Pending(owner string) int {
292 who := address(owner)
293 if !who.IsValid() {
294 return 0
295 }
296 return pending(who)
297}
298
299func Render(path string) string {
300 if path != "" {
301 return "# Shoal Live\n\nUnknown path.\n"
302 }
303 return "# Shoal Live\n\nPerson versus person. CommitMint, RevealMint, Register, Leave, Heal, and Settle. No machine fish. The older shoal/fish commons stay on that package.\n"
304}
305
306func gate(cur realm, pay int64) address {
307 if !cur.Previous().IsUserCall() {
308 panic("fishlive: eoa")
309 }
310 if !inited || self == address("") {
311 panic("fishlive: init")
312 }
313 takeExact(pay)
314 return cur.Previous().Address()
315}
316
317func payMint(fee int64, rlm realm) {
318 lp := fee * 6000 / 10000
319 crew := fee * 2000 / 10000
320 ash := fee * 1000 / 10000
321 send(lpSink, lp, rlm)
322 send(team, crew+fee-lp-crew-ash, rlm)
323 send(burn, ash, rlm)
324}
325
326func splitKill(covered int64) (killer int64, lp int64, crew int64, buyback int64) {
327 killer = covered * 80 / 100
328 lp = covered * 10 / 100
329 teamPart := covered * 3 / 100
330 buyback = covered * 3 / 100
331 cheer := covered * 4 / 100
332 buyback += covered - killer - lp - teamPart - buyback - cheer
333 crew = teamPart + cheer
334 return
335}
336
337func send(to address, amt int64, rlm realm) {
338 if !rlm.IsCurrent() {
339 panic("fishlive: realm")
340 }
341 if amt == 0 {
342 return
343 }
344 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
345 bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
346}
347
348func grow() int {
349 if len(roster) == 0 {
350 roster = append(roster, fish{})
351 }
352 if minted >= capFish || len(roster) != minted+1 {
353 panic("fishlive: mint")
354 }
355 roster = append(roster, fish{})
356 minted++
357 return minted
358}
359
360func mustFish(id int) *fish {
361 if !inited || id < 1 || id > minted || id >= len(roster) {
362 panic("fishlive: fish")
363 }
364 return &roster[id]
365}
366
367func pending(who address) int {
368 for i := 1; i <= minted; i++ {
369 if roster[i].owner == who && !roster[i].revealed {
370 return i
371 }
372 }
373 return 0
374}
375
376func seated(who address) bool {
377 for i := 1; i <= minted; i++ {
378 if roster[i].owner == who && roster[i].inSea {
379 return true
380 }
381 }
382 return false
383}
384
385func pickName(tier int) string {
386 base := species[tier]
387 if !nameTaken(base) {
388 return base
389 }
390 for n := 2; n <= capFish; n++ {
391 candidate := base + strconv.Itoa(n)
392 if !nameTaken(candidate) {
393 return candidate
394 }
395 }
396 panic("fishlive: name")
397}
398
399func nameTaken(name string) bool {
400 for i := 1; i <= minted; i++ {
401 if roster[i].revealed && roster[i].name == name {
402 return true
403 }
404 }
405 return false
406}
407
408func tierOf(rarity string) int {
409 for i := 0; i < len(rarities); i++ {
410 if rarities[i] == rarity {
411 return i
412 }
413 }
414 panic("fishlive: roll")
415}
416
417func takeExact(n int64) {
418 sent := unsafe.OriginSend()
419 if n == 0 {
420 if len(sent) != 0 {
421 panic("fishlive: payment")
422 }
423 return
424 }
425 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
426 panic("fishlive: payment")
427 }
428}
429
430func atoi(s string) int {
431 n, err := strconv.Atoi(s)
432 if err != nil {
433 panic("fishlive: fish")
434 }
435 return n
436}