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 trait4// 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 fishlive1213import (14 "chain"15 "chain/banker"16 "chain/runtime"17 "chain/runtime/unsafe"18 "crypto/sha256"19 "strconv"20 "time"21)2223const (24 denom = "ugnot"25 mintFee int64 = 150_000_00026 stakeMin int64 = 10_000_00027 quietMs int64 = 5_00028 capFish = 12829)3031var (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)3940type fish struct {41 owner address42 name string43 rarity string44 trait string45 bite int46 hpMax int47 hp int48 speed int49 pool int6450 bouts int51 commitH int6452 touched int6453 inSea bool54 revealed bool55 mustHeal bool56}5758var (59 inited bool60 self address61 lpSink address62 team address63 burn address64 minted int65 // 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 []fish68)6970// 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 = true81 self = cur.Address()82 lpSink = lp83 team = crew84 burn = ash85 chain.Emit("Init", "self", self.String())86}8788// 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 = caller100 roster[id].commitH = height101 payMint(mintFee, cur)102 chain.Emit("Commit", "id", strconv.Itoa(id), "owner", caller.String())103 return id104}105106// 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 + 2111 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 = rarity116 row.trait = trait117 row.bite = bite118 row.hpMax = hp119 row.hp = hp120 row.speed = speed121 row.name = pickName(tierOf(rarity))122 row.revealed = true123 chain.Emit("Reveal", "id", strconv.Itoa(id), "rarity", rarity, "trait", trait, "name", row.name)124}125126// 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 = stake134 row.inSea = true135 row.touched = time.Now().UnixMilli()136 chain.Emit("Register", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))137}138139// 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.pool147 row.pool = 0148 row.inSea = false149 send(caller, stake, cur)150 chain.Emit("Leave", "id", strconv.Itoa(id), "stake", strconv.FormatInt(stake, 10))151}152153// 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.pool166 if vic.pool < covered {167 covered = vic.pool168 }169 excess := vic.pool - covered170 owner := vic.owner171 vic.pool = 0172 vic.inSea = false173 vic.mustHeal = true174 vic.hp = vic.hpMax * 30 / 100175 if vic.hp < 1 {176 vic.hp = 1177 }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}188189// 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 = false197 row.hp = row.hpMax198 chain.Emit("Heal", "id", strconv.Itoa(id))199}200201// 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])) % 10000209 tier := 0210 if n == 0 {211 tier = 5212 } else if n < 4 {213 tier = 4214 } else if n < 14 {215 tier = 3216 } else if n < 54 {217 tier = 2218 } else if n < 254 {219 tier = 1220 }221 kind := int(sum[2]) % 3222 bite = biteBase[tier]223 hp = hpBase[tier]224 speed = spdBase[tier]225 if kind == 0 {226 speed = (speed*112 + 50) / 100227 } else if kind == 1 {228 bite = (bite*112 + 50) / 100229 } else {230 hp = (hp*112 + 50) / 100231 }232 return rarities[tier], traits[kind], bite, hp, speed233}234235func 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() }243244func OwnerOf(id string) string {245 return mustFish(atoi(id)).owner.String()246}247248func 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}255256func 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 continue265 }266 if out != "" {267 out += ","268 }269 out += strconv.Itoa(i)270 }271 return out272}273274func 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 }290291func Pending(owner string) int {292 who := address(owner)293 if !who.IsValid() {294 return 0295 }296 return pending(who)297}298299func 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}305306func 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}316317func payMint(fee int64, rlm realm) {318 lp := fee * 6000 / 10000319 crew := fee * 2000 / 10000320 ash := fee * 1000 / 10000321 send(lpSink, lp, rlm)322 send(team, crew+fee-lp-crew-ash, rlm)323 send(burn, ash, rlm)324}325326func splitKill(covered int64) (killer int64, lp int64, crew int64, buyback int64) {327 killer = covered * 80 / 100328 lp = covered * 10 / 100329 teamPart := covered * 3 / 100330 buyback = covered * 3 / 100331 cheer := covered * 4 / 100332 buyback += covered - killer - lp - teamPart - buyback - cheer333 crew = teamPart + cheer334 return335}336337func send(to address, amt int64, rlm realm) {338 if !rlm.IsCurrent() {339 panic("fishlive: realm")340 }341 if amt == 0 {342 return343 }344 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)345 bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})346}347348func 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 minted358}359360func mustFish(id int) *fish {361 if !inited || id < 1 || id > minted || id >= len(roster) {362 panic("fishlive: fish")363 }364 return &roster[id]365}366367func pending(who address) int {368 for i := 1; i <= minted; i++ {369 if roster[i].owner == who && !roster[i].revealed {370 return i371 }372 }373 return 0374}375376func seated(who address) bool {377 for i := 1; i <= minted; i++ {378 if roster[i].owner == who && roster[i].inSea {379 return true380 }381 }382 return false383}384385func pickName(tier int) string {386 base := species[tier]387 if !nameTaken(base) {388 return base389 }390 for n := 2; n <= capFish; n++ {391 candidate := base + strconv.Itoa(n)392 if !nameTaken(candidate) {393 return candidate394 }395 }396 panic("fishlive: name")397}398399func nameTaken(name string) bool {400 for i := 1; i <= minted; i++ {401 if roster[i].revealed && roster[i].name == name {402 return true403 }404 }405 return false406}407408func tierOf(rarity string) int {409 for i := 0; i < len(rarities); i++ {410 if rarities[i] == rarity {411 return i412 }413 }414 panic("fishlive: roll")415}416417func takeExact(n int64) {418 sent := unsafe.OriginSend()419 if n == 0 {420 if len(sent) != 0 {421 panic("fishlive: payment")422 }423 return424 }425 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {426 panic("fishlive: payment")427 }428}429430func atoi(s string) int {431 n, err := strconv.Atoi(s)432 if err != nil {433 panic("fishlive: fish")434 }435 return n436}437BiteOf(id int) int
BoutsOf(id int) int
CommitMint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int
CommitOf(id int) int64
Heal(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Height() int64
HpOf(id int) int
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, lp string, crew string, ash string)
InMatch(id int) bool
Leave(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Minted() int
MintPrice() int64
MustHeal(id int) bool
Name() string
NameOf(id int) string
OwnerOf(id string) string
Pending(owner string) int
PoolOf(id int) int64
RarityOf(id int) string
Realm() string
Register(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, stake int64)
Render(path string) string
Revealed(id int) bool
RevealMint(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int)
Roll(caller string, commitH int64, entropyH int64, id int) (rarity string, trait string, bite int, hp int, speed int)
Settle(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int, target int)
SizeOf(id int) string
SpeedOf(id int) int
StakeMin() int64
Symbol() string
TokensOf(owner string) string
TokenURI(id string) string
TraitOf(id int) string
Version() string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.