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 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: 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 fishsea1314import (15 "chain"16 "chain/banker"17 "chain/runtime"18 "chain/runtime/unsafe"19 "crypto/sha256"20 "strconv"21 "time"22)2324const (25 denom = "ugnot"26 mintFee int64 = 150_000_00027 stakeMin int64 = 10_000_00028 quietMs int64 = 5_00029 capFish = 12830)3132var (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)4041type fish struct {42 owner address43 name string44 rarity string45 trait string46 bite int47 hpMax int48 hp int49 speed int50 pool int6451 bouts int52 commitH int6453 touched int6454 inSea bool55 revealed bool56 mustHeal bool57}5859var (60 inited bool61 self address62 mintTo address63 lpSink address64 team address65 minted int66 roster []fish67)6869// 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 = true81 self = cur.Address()82 mintTo = caller83 lpSink = lp84 team = crew85 chain.Emit("Init", "mint", caller.String(), "lp", lp.String(), "team", crew.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("fishsea: mint")93 }94 height := runtime.ChainHeight()95 if height < 1 {96 panic("fishsea: 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("fishsea: 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("fishsea: 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("fishsea: 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. 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.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 := 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}187188// 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 = false196 row.hp = row.hpMax197 chain.Emit("Heal", "id", strconv.Itoa(id))198}199200// 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])) % 10000208 tier := 0209 if n == 0 {210 tier = 5211 } else if n < 4 {212 tier = 4213 } else if n < 14 {214 tier = 3215 } else if n < 54 {216 tier = 2217 } else if n < 254 {218 tier = 1219 }220 kind := int(sum[2]) % 3221 bite = biteBase[tier]222 hp = hpBase[tier]223 speed = spdBase[tier]224 if kind == 0 {225 speed = (speed*112 + 50) / 100226 } else if kind == 1 {227 bite = (bite*112 + 50) / 100228 } else {229 hp = (hp*112 + 50) / 100230 }231 return rarities[tier], traits[kind], bite, hp, speed232}233234func 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() }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("fishsea: 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 "# 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}305306func 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}316317func payMint(fee int64, rlm realm) {318 send(mintTo, fee, rlm)319}320321func splitKill(covered int64) (killer int64, lp int64, crew int64) {322 killer = covered * 80 / 100323 lp = covered * 10 / 100324 crew = covered - killer - lp325 return326}327328func send(to address, amt int64, rlm realm) {329 if !rlm.IsCurrent() {330 panic("fishsea: realm")331 }332 if amt == 0 {333 return334 }335 bk := banker.NewBanker(banker.BankerTypeRealmSend, rlm)336 bk.SendCoins(rlm.Address(), to, chain.Coins{{Denom: denom, Amount: amt}})337}338339func 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 minted349}350351func mustFish(id int) *fish {352 if !inited || id < 1 || id > minted || id >= len(roster) {353 panic("fishsea: fish")354 }355 return &roster[id]356}357358func pending(who address) int {359 for i := 1; i <= minted; i++ {360 if roster[i].owner == who && !roster[i].revealed {361 return i362 }363 }364 return 0365}366367func seated(who address) bool {368 for i := 1; i <= minted; i++ {369 if roster[i].owner == who && roster[i].inSea {370 return true371 }372 }373 return false374}375376func pickName(tier int) string {377 base := species[tier]378 if !nameTaken(base) {379 return base380 }381 for n := 2; n <= capFish; n++ {382 candidate := base + strconv.Itoa(n)383 if !nameTaken(candidate) {384 return candidate385 }386 }387 panic("fishsea: name")388}389390func nameTaken(name string) bool {391 for i := 1; i <= minted; i++ {392 if roster[i].revealed && roster[i].name == name {393 return true394 }395 }396 return false397}398399func tierOf(rarity string) int {400 for i := 0; i < len(rarities); i++ {401 if rarities[i] == rarity {402 return i403 }404 }405 panic("fishsea: roll")406}407408func takeExact(n int64) {409 sent := unsafe.OriginSend()410 if n == 0 {411 if len(sent) != 0 {412 panic("fishsea: payment")413 }414 return415 }416 if len(sent) != 1 || sent[0].Denom != denom || sent[0].Amount != n {417 panic("fishsea: payment")418 }419}420421func atoi(s string) int {422 n, err := strconv.Atoi(s)423 if err != nil {424 panic("fishsea: fish")425 }426 return n427}428BiteOf(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)
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
MintTo() string
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.