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.

fishsea.gno

10.81 Kb · 427 lines
  1// Package fishsea is the person-versus-person sea.
  2// CommitMint pays 150 GNOT and the whole fee goes to the wallet that called Init.
  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: 80% winner, 10% liquidity, 10% team.
  8// There is no machine fish and no FISH draw.
  9// A seated wallet can settle any other seated fish. The realm does not replay the bite.
 10// fishlive stays as deployed. This package does not move it.
 11// fishsea-1.
 12package fishsea
 13
 14import (
 15	"chain"
 16	"chain/banker"
 17	"chain/runtime"
 18	"chain/runtime/unsafe"
 19	"crypto/sha256"
 20	"strconv"
 21	"time"
 22)
 23
 24const (
 25	denom          = "ugnot"
 26	mintFee  int64 = 150_000_000
 27	stakeMin int64 = 10_000_000
 28	quietMs  int64 = 5_000
 29	capFish        = 128
 30)
 31
 32var (
 33	hpBase   = [6]int{100, 122, 148, 178, 214, 256}
 34	biteBase = [6]int{24, 30, 37, 45, 54, 66}
 35	spdBase  = [6]int{108, 104, 100, 96, 92, 88}
 36	species  = [6]string{"Bubble", "Tideblade", "Abysswing", "Embermaw", "Aurelion", "Astravex"}
 37	rarities = [6]string{"common", "uncommon", "rare", "epic", "legendary", "mythic"}
 38	traits   = [3]string{"swift", "fang", "hide"}
 39)
 40
 41type fish struct {
 42	owner    address
 43	name     string
 44	rarity   string
 45	trait    string
 46	bite     int
 47	hpMax    int
 48	hp       int
 49	speed    int
 50	pool     int64
 51	bouts    int
 52	commitH  int64
 53	touched  int64
 54	inSea    bool
 55	revealed bool
 56	mustHeal bool
 57}
 58
 59var (
 60	inited  bool
 61	self    address
 62	mintTo  address
 63	lpSink  address
 64	team    address
 65	minted  int
 66	roster  []fish
 67)
 68
 69// Init records the caller as the mint wallet, plus liquidity and team.
 70// Those three must be distinct. Call once, from an EOA, with no coins.
 71func Init(cur realm, lp, crew address) {
 72	if !cur.Previous().IsUserCall() {
 73		panic("fishsea: eoa")
 74	}
 75	takeExact(0)
 76	caller := cur.Previous().Address()
 77	if inited || !caller.IsValid() || !lp.IsValid() || !crew.IsValid() || caller == lp || caller == crew || lp == crew {
 78		panic("fishsea: init")
 79	}
 80	inited = true
 81	self = cur.Address()
 82	mintTo = caller
 83	lpSink = lp
 84	team = crew
 85	chain.Emit("Init", "mint", caller.String(), "lp", lp.String(), "team", crew.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("fishsea: mint")
 93	}
 94	height := runtime.ChainHeight()
 95	if height < 1 {
 96		panic("fishsea: 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("fishsea: 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("fishsea: 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("fishsea: 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. 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("fishsea: 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("fishsea: 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 := splitKill(covered)
181	send(atk.owner, killer, cur)
182	send(lpSink, lp, cur)
183	send(team, crew, cur)
184	send(owner, excess, cur)
185	chain.Emit("Kill", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "covered", strconv.FormatInt(covered, 10))
186}
187
188// Heal clears mustHeal outside the sea so Register can run again.
189func Heal(cur realm, id int) {
190	caller := gate(cur, 0)
191	row := mustFish(id)
192	if row.owner != caller || row.inSea || !row.mustHeal || !row.revealed {
193		panic("fishsea: heal")
194	}
195	row.mustHeal = false
196	row.hp = row.hpMax
197	chain.Emit("Heal", "id", strconv.Itoa(id))
198}
199
200// Roll is the reveal hash. entropyH must be commitH+2. Trait adds 12%, half up.
201func Roll(caller address, commitH int64, entropyH int64, id int) (rarity string, trait string, bite int, hp int, speed int) {
202	if id < 1 || commitH < 1 || entropyH != commitH+2 {
203		panic("fishsea: roll")
204	}
205	raw := caller.String() + "|" + strconv.FormatInt(commitH, 10) + "|" + strconv.FormatInt(entropyH, 10) + "|" + strconv.Itoa(id)
206	sum := sha256.Sum256([]byte(raw))
207	n := (int(sum[0])<<8 | int(sum[1])) % 10000
208	tier := 0
209	if n == 0 {
210		tier = 5
211	} else if n < 4 {
212		tier = 4
213	} else if n < 14 {
214		tier = 3
215	} else if n < 54 {
216		tier = 2
217	} else if n < 254 {
218		tier = 1
219	}
220	kind := int(sum[2]) % 3
221	bite = biteBase[tier]
222	hp = hpBase[tier]
223	speed = spdBase[tier]
224	if kind == 0 {
225		speed = (speed*112 + 50) / 100
226	} else if kind == 1 {
227		bite = (bite*112 + 50) / 100
228	} else {
229		hp = (hp*112 + 50) / 100
230	}
231	return rarities[tier], traits[kind], bite, hp, speed
232}
233
234func Realm() address    { return self }
235func MintTo() address   { return mintTo }
236func Version() string   { return "fishsea-1" }
237func Name() string      { return "GnoFish Sea" }
238func Symbol() string    { return "SEA" }
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("fishsea: 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 "# GnoFish Sea\n\nUnknown path.\n"
302	}
303	return "# GnoFish Sea\n\nPerson versus person. The mint fee goes to the deploy wallet. A win pays 80% to the winner, 10% to liquidity, and 10% to the team.\n"
304}
305
306func gate(cur realm, pay int64) address {
307	if !cur.Previous().IsUserCall() {
308		panic("fishsea: eoa")
309	}
310	if !inited || self == address("") {
311		panic("fishsea: init")
312	}
313	takeExact(pay)
314	return cur.Previous().Address()
315}
316
317func payMint(fee int64, rlm realm) {
318	send(mintTo, fee, rlm)
319}
320
321func splitKill(covered int64) (killer int64, lp int64, crew int64) {
322	killer = covered * 80 / 100
323	lp = covered * 10 / 100
324	crew = covered - killer - lp
325	return
326}
327
328func send(to address, amt int64, rlm realm) {
329	if !rlm.IsCurrent() {
330		panic("fishsea: realm")
331	}
332	if amt == 0 {
333		return
334	}
335	bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
336	bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
337}
338
339func grow() int {
340	if len(roster) == 0 {
341		roster = append(roster, fish{})
342	}
343	if minted >= capFish || len(roster) != minted+1 {
344		panic("fishsea: mint")
345	}
346	roster = append(roster, fish{})
347	minted++
348	return minted
349}
350
351func mustFish(id int) *fish {
352	if !inited || id < 1 || id > minted || id >= len(roster) {
353		panic("fishsea: fish")
354	}
355	return &roster[id]
356}
357
358func pending(who address) int {
359	for i := 1; i <= minted; i++ {
360		if roster[i].owner == who && !roster[i].revealed {
361			return i
362		}
363	}
364	return 0
365}
366
367func seated(who address) bool {
368	for i := 1; i <= minted; i++ {
369		if roster[i].owner == who && roster[i].inSea {
370			return true
371		}
372	}
373	return false
374}
375
376func pickName(tier int) string {
377	base := species[tier]
378	if !nameTaken(base) {
379		return base
380	}
381	for n := 2; n <= capFish; n++ {
382		candidate := base + strconv.Itoa(n)
383		if !nameTaken(candidate) {
384			return candidate
385		}
386	}
387	panic("fishsea: name")
388}
389
390func nameTaken(name string) bool {
391	for i := 1; i <= minted; i++ {
392		if roster[i].revealed && roster[i].name == name {
393			return true
394		}
395	}
396	return false
397}
398
399func tierOf(rarity string) int {
400	for i := 0; i < len(rarities); i++ {
401		if rarities[i] == rarity {
402			return i
403		}
404	}
405	panic("fishsea: roll")
406}
407
408func takeExact(n int64) {
409	sent := unsafe.OriginSend()
410	if n == 0 {
411		if len(sent) != 0 {
412			panic("fishsea: payment")
413		}
414		return
415	}
416	if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
417		panic("fishsea: payment")
418	}
419}
420
421func atoi(s string) int {
422	n, err := strconv.Atoi(s)
423	if err != nil {
424		panic("fishsea: fish")
425	}
426	return n
427}