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.

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}