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}