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.

fishbay.gno

7.91 Kb · 342 lines
  1// Package fishbay is a small onyx trial sea.
  2// It keeps Mint, Register, Leave, and Bite.
  3// Damage is floor(bite * 100 / (hpMax + 100)), at least 1.
  4// A kill pays GNOT and FISH from the smaller stake.
  5// FISH is drawn from fishmark. This realm does not mint FISH.
  6// Lay, the market, cheer, and party are not in this bay.
  7// shoal/fish stays the live NFT collection. This package is not that collection.
  8// The trial mint is 1 GNOT and the stake floor is 1 GNOT.
  9package fishbay
 10
 11import (
 12	"chain"
 13	"chain/banker"
 14	"chain/runtime/unsafe"
 15	"strconv"
 16
 17	fishmark "gno.land/r/g10ragdr3cjqkael8rslpw4y6rlelhev4h0fswcx/fishmark"
 18)
 19
 20const (
 21	denom    = "ugnot"
 22	unit     int64 = 1_000_000
 23	mintFee  int64 = 1_000_000
 24	stakeMin int64 = 1_000_000
 25	biteStat       = 69
 26	hpStat         = 88
 27	seatMax        = 16
 28)
 29
 30type swimmer struct {
 31	owner    address
 32	name     string
 33	pool     int64
 34	hp       int
 35	hpMax    int
 36	bite     int
 37	inSea    bool
 38	mustHeal bool
 39}
 40
 41var (
 42	inited  bool
 43	self    address
 44	reserve address
 45	upgrade address
 46	lpSink  address
 47	team    address
 48	burn    address
 49	minted  int
 50	roster  [seatMax + 1]swimmer
 51)
 52
 53// Init records the FISH reserve and the four GNOT sinks. The reserve must not also be a sink.
 54// Upgrade and burn may be the same address. Call this once, from an EOA, with no coins.
 55func Init(cur realm, who, up, lp, crew, ash address) {
 56	if !cur.IsCurrent() {
 57		panic("fishbay: realm")
 58	}
 59	if !cur.Previous().IsUserCall() {
 60		panic("fishbay: eoa")
 61	}
 62	takeExact(0)
 63	if inited {
 64		panic("fishbay: init")
 65	}
 66	if !who.IsValid() || !up.IsValid() || !lp.IsValid() || !crew.IsValid() || !ash.IsValid() {
 67		panic("fishbay: addr")
 68	}
 69	if who == up || who == lp || who == crew || who == ash || lp == crew {
 70		panic("fishbay: addr")
 71	}
 72	inited = true
 73	self = cur.Address()
 74	reserve = who
 75	upgrade = up
 76	lpSink = lp
 77	team = crew
 78	burn = ash
 79	chain.Emit("Init", "self", self.String(), "reserve", who.String())
 80}
 81
 82// Mint sells one trial fish for 1 GNOT. The fee leaves this realm in the same call.
 83func Mint(cur realm, name string) int {
 84	if !cur.IsCurrent() {
 85		panic("fishbay: realm")
 86	}
 87	caller := cur.Previous()
 88	if !caller.IsUserCall() {
 89		panic("fishbay: eoa")
 90	}
 91	requireInit()
 92	takeExact(mintFee)
 93	if minted >= seatMax || !validName(name) {
 94		panic("fishbay: mint")
 95	}
 96	minted++
 97	roster[minted] = swimmer{
 98		owner: caller.Address(),
 99		name:  name,
100		hp:    hpStat,
101		hpMax: hpStat,
102		bite:  biteStat,
103	}
104	payMint(mintFee, cur)
105	chain.Emit("Mint", "id", strconv.Itoa(minted), "owner", caller.Address().String())
106	return minted
107}
108
109// Register seats a fish. The attached coins are the stake and must be at least 1 GNOT.
110func Register(cur realm, id int, stake int64) {
111	if !cur.IsCurrent() {
112		panic("fishbay: realm")
113	}
114	caller := cur.Previous()
115	if !caller.IsUserCall() {
116		panic("fishbay: eoa")
117	}
118	requireInit()
119	if stake < stakeMin {
120		panic("fishbay: stake")
121	}
122	takeExact(stake)
123	fish := mustFish(id)
124	if fish.owner != caller.Address() || fish.inSea || fish.mustHeal || fish.hp*100 < fish.hpMax*30 {
125		panic("fishbay: register")
126	}
127	if ownerSeated(caller.Address()) {
128		panic("fishbay: seat")
129	}
130	fish.pool = stake
131	fish.inSea = true
132	chain.Emit("Register", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
133}
134
135// Leave returns the seated stake to the owner. A fish that is already out of the bay cannot leave.
136func Leave(cur realm, id int) {
137	if !cur.IsCurrent() {
138		panic("fishbay: realm")
139	}
140	caller := cur.Previous()
141	if !caller.IsUserCall() {
142		panic("fishbay: eoa")
143	}
144	requireInit()
145	takeExact(0)
146	fish := mustFish(id)
147	if fish.owner != caller.Address() || !fish.inSea {
148		panic("fishbay: leave")
149	}
150	stake := fish.pool
151	fish.pool = 0
152	fish.inSea = false
153	send(fish.owner, stake, cur)
154	chain.Emit("Leave", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))
155}
156
157// Bite deals flat damage. A kill pays the smaller stake and draws 1 FISH per 1 GNOT of that stake.
158func Bite(cur realm, id, target int) int {
159	if !cur.IsCurrent() {
160		panic("fishbay: realm")
161	}
162	caller := cur.Previous()
163	if !caller.IsUserCall() {
164		panic("fishbay: eoa")
165	}
166	requireInit()
167	takeExact(0)
168	if id == target {
169		panic("fishbay: bite")
170	}
171	atk := mustFish(id)
172	vic := mustFish(target)
173	if atk.owner != caller.Address() || !atk.inSea || !vic.inSea || atk.owner == vic.owner || atk.pool < stakeMin || vic.pool < stakeMin {
174		panic("fishbay: bite")
175	}
176	dmg := Strike(atk.bite, vic.hpMax)
177	if vic.hp > dmg {
178		vic.hp -= dmg
179		chain.Emit("Bite", "id", strconv.Itoa(id), "target", strconv.Itoa(target), "dmg", strconv.Itoa(dmg))
180		return dmg
181	}
182	covered := atk.pool
183	if vic.pool < covered {
184		covered = vic.pool
185	}
186	excess := vic.pool - covered
187	owner := vic.owner
188	vic.hp = downHP(vic.hpMax)
189	vic.pool = 0
190	vic.inSea = false
191	vic.mustHeal = true
192	killer, lp, crew, buyback := splitKill(covered)
193	send(atk.owner, killer, cur)
194	send(lpSink, lp, cur)
195	send(team, crew, cur)
196	send(burn, buyback, cur)
197	send(owner, excess, cur)
198	got := payFish(cur, atk.owner, covered)
199	chain.Emit(
200		"Kill",
201		"id", strconv.Itoa(id),
202		"target", strconv.Itoa(target),
203		"covered", strconv.FormatInt(covered, 10),
204		"fish", strconv.FormatInt(got, 10),
205	)
206	return dmg
207}
208
209// Strike is the damage formula. Size and rarity are not inputs.
210func Strike(bite, hpMax int) int {
211	if bite < 1 {
212		bite = 1
213	}
214	if hpMax < 0 {
215		hpMax = 0
216	}
217	dmg := bite * 100 / (hpMax + 100)
218	if dmg < 1 {
219		return 1
220	}
221	return dmg
222}
223
224func Realm() address { return self }
225func Reserve() address { return reserve }
226func Version() string { return "fishbay-1" }
227func MintPrice() int64 { return mintFee }
228func StakeMin() int64 { return stakeMin }
229func Minted() int { return minted }
230
231func OwnerOf(id int) address { return mustFish(id).owner }
232func PoolOf(id int) int64    { return mustFish(id).pool }
233func HpOf(id int) int        { return mustFish(id).hp }
234func InSea(id int) bool      { return mustFish(id).inSea }
235func MustHeal(id int) bool   { return mustFish(id).mustHeal }
236
237func Render(path string) string {
238	if path != "" {
239		return "# Fishbay\n\nUnknown path.\n"
240	}
241	return "# Fishbay\n\nTrial bay. Mint, Register, Leave, and Bite. This is not the shoal/fish collection.\n"
242}
243
244func payFish(cur realm, to address, covered int64) int64 {
245	whole := covered / unit
246	if whole <= 0 {
247		return 0
248	}
249	return fishmark.Draw(cross(cur), reserve, to, whole*unit)
250}
251
252func splitKill(covered int64) (killer, lp, crew, buyback int64) {
253	killer = covered * 80 / 100
254	lp = covered * 10 / 100
255	teamPart := covered * 3 / 100
256	buyback = covered * 3 / 100
257	cheer := covered * 4 / 100
258	buyback += covered - killer - lp - teamPart - buyback - cheer
259	crew = teamPart + cheer
260	return
261}
262
263func payMint(fee int64, rlm realm) {
264	up := fee * 6000 / 10000
265	lp := fee * 2000 / 10000
266	crew := fee * 1000 / 10000
267	ash := fee - up - lp - crew
268	send(upgrade, up, rlm)
269	send(lpSink, lp, rlm)
270	send(team, crew, rlm)
271	send(burn, ash, rlm)
272}
273
274func send(to address, amt int64, rlm realm) {
275	if !rlm.IsCurrent() {
276		panic("fishbay: realm")
277	}
278	if amt == 0 {
279		return
280	}
281	if !to.IsValid() {
282		panic("fishbay: sink")
283	}
284	bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)
285	bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})
286}
287
288func mustFish(id int) *swimmer {
289	requireInit()
290	if id < 1 || id > minted {
291		panic("fishbay: fish")
292	}
293	return &roster[id]
294}
295
296func ownerSeated(who address) bool {
297	for i := 1; i <= minted; i++ {
298		if roster[i].owner == who && roster[i].inSea {
299			return true
300		}
301	}
302	return false
303}
304
305func downHP(hpMax int) int {
306	hp := hpMax * 30 / 100
307	if hp < 1 {
308		return 1
309	}
310	return hp
311}
312
313func validName(name string) bool {
314	if len(name) < 1 || len(name) > 16 {
315		return false
316	}
317	for _, c := range name {
318		if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
319			return false
320		}
321	}
322	return true
323}
324
325func requireInit() {
326	if !inited || self == address("") {
327		panic("fishbay: init")
328	}
329}
330
331func takeExact(n int64) {
332	sent := unsafe.OriginSend()
333	if n == 0 {
334		if len(sent) != 0 {
335			panic("fishbay: payment")
336		}
337		return
338	}
339	if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {
340		panic("fishbay: payment")
341	}
342}