1// Package tamagotchi is an on-chain virtual pet. Hatch one, then keep it2// alive by Feeding, Playing and letting it Sleep — hunger, happiness and3// energy all decay with block height, and neglect too long kills the pet.4// Hatching again after a death starts a fresh pet but keeps your lifetime5// stats, so the leaderboard rewards long-term care, not just luck.6package tamagotchi78import (9 "sort"10 "strconv"1112 "chain/runtime"1314 "gno.land/p/nt/avl/v0"15)1617// pet is the persisted record for one owner's tamagotchi.18type pet struct {19 owner address20 name string21 born int6422 lastUpdate int6423 hunger int // 0 (full) .. 100 (starving)24 happiness int // 0 (miserable) .. 100 (joyful)25 energy int // 0 (exhausted) .. 100 (energetic)26 alive bool27 deaths int28 feeds int29 plays int30}3132var pets avl.Tree // owner address string -> *pet3334func clamp(v, lo, hi int) int {35 if v < lo {36 return lo37 }38 if v > hi {39 return hi40 }41 return v42}4344func get(owner address) (*pet, bool) {45 v := pets.Get(owner.String())46 if v == nil {47 return nil, false48 }49 return v.(*pet), true50}5152// tick applies stat decay for blocks elapsed since the pet's last update and53// kills it if any stat has bottomed/topped out. Mutates and persists.54func (p *pet) tick(height int64) {55 if !p.alive {56 return57 }58 elapsed := int(height - p.lastUpdate)59 if elapsed <= 0 {60 return61 }62 p.hunger = clamp(p.hunger+elapsed, 0, 100)63 p.happiness = clamp(p.happiness-elapsed/2, 0, 100)64 p.energy = clamp(p.energy-elapsed/3, 0, 100)65 p.lastUpdate = height66 if p.hunger >= 100 || p.happiness <= 0 || p.energy <= 0 {67 p.alive = false68 p.deaths++69 }70}7172// project computes display stats as of height without mutating the pet, so73// Render can show a live-looking view without writing state on a query.74func project(p *pet, height int64) (hunger, happiness, energy int, alive bool) {75 if !p.alive {76 return p.hunger, p.happiness, p.energy, false77 }78 elapsed := int(height - p.lastUpdate)79 if elapsed < 0 {80 elapsed = 081 }82 hunger = clamp(p.hunger+elapsed, 0, 100)83 happiness = clamp(p.happiness-elapsed/2, 0, 100)84 energy = clamp(p.energy-elapsed/3, 0, 100)85 alive = hunger < 100 && happiness > 0 && energy > 086 return87}8889func ageStageName(born, height int64) string {90 switch age := height - born; {91 case age < 10:92 return "🥚 Egg"93 case age < 50:94 return "🐣 Baby"95 case age < 200:96 return "🐤 Teen"97 default:98 return "🐓 Adult"99 }100}101102// requireLivingPet ticks and fetches the caller's pet, panicking with a103// friendly message if there is none or it has died.104func requireLivingPet(owner address, height int64) *pet {105 p, ok := get(owner)106 if !ok {107 panic("you don't have a pet yet — call Hatch(name) first")108 }109 p.tick(height)110 if !p.alive {111 panic(p.name + " has died of neglect. Call Hatch(name) to start over.")112 }113 return p114}115116// hatch is the non-crossing core of Hatch, kept separate so unit tests can117// exercise its panics directly without going through a realm-crossing call.118func hatch(owner address, name string, height int64) string {119 if name == "" {120 panic("name cannot be empty")121 }122123 deaths, feeds, plays := 0, 0, 0124 if existing, ok := get(owner); ok {125 if existing.alive {126 existing.tick(height)127 }128 if existing.alive {129 panic("you already have a living pet: " + existing.name)130 }131 deaths, feeds, plays = existing.deaths, existing.feeds, existing.plays132 }133134 pets.Set(owner.String(), &pet{135 owner: owner,136 name: name,137 born: height,138 lastUpdate: height,139 hunger: 20,140 happiness: 80,141 energy: 80,142 alive: true,143 deaths: deaths,144 feeds: feeds,145 plays: plays,146 })147 return "🥚 " + name + " has hatched!"148}149150// Hatch creates a new pet for the caller. Lifetime feeds/plays/deaths carry151// over from any previous pet so the leaderboard tracks long-term care.152func Hatch(cur realm, name string) string {153 if !cur.IsCurrent() {154 panic("spoofed realm")155 }156 return hatch(cur.Previous().Address(), name, runtime.ChainHeight())157}158159// feed is the non-crossing core of Feed.160func feed(owner address, height int64) string {161 p := requireLivingPet(owner, height)162 p.hunger = clamp(p.hunger-30, 0, 100)163 p.energy = clamp(p.energy+5, 0, 100)164 p.feeds++165 return p.name + " munches happily. Hunger: " + strconv.Itoa(p.hunger) + "/100"166}167168// Feed reduces hunger and gives a small energy boost.169func Feed(cur realm) string {170 if !cur.IsCurrent() {171 panic("spoofed realm")172 }173 return feed(cur.Previous().Address(), runtime.ChainHeight())174}175176// play is the non-crossing core of Play.177func play(owner address, height int64) string {178 p := requireLivingPet(owner, height)179 if p.energy < 10 {180 return p.name + " is too tired to play — try Sleep() first."181 }182 p.happiness = clamp(p.happiness+20, 0, 100)183 p.energy = clamp(p.energy-10, 0, 100)184 p.hunger = clamp(p.hunger+5, 0, 100)185 p.plays++186 return p.name + " had a blast! Happiness: " + strconv.Itoa(p.happiness) + "/100"187}188189// Play boosts happiness at the cost of energy and a bit of hunger. Refuses190// to run a tired pet into the ground — rest first.191func Play(cur realm) string {192 if !cur.IsCurrent() {193 panic("spoofed realm")194 }195 return play(cur.Previous().Address(), runtime.ChainHeight())196}197198// sleep is the non-crossing core of Sleep.199func sleep(owner address, height int64) string {200 p := requireLivingPet(owner, height)201 p.energy = clamp(p.energy+40, 0, 100)202 p.hunger = clamp(p.hunger+5, 0, 100)203 return p.name + " takes a nap. Energy: " + strconv.Itoa(p.energy) + "/100"204}205206// Sleep restores energy at the cost of a little hunger.207func Sleep(cur realm) string {208 if !cur.IsCurrent() {209 panic("spoofed realm")210 }211 return sleep(cur.Previous().Address(), runtime.ChainHeight())212}213214// byAliveThenOwner ranks living pets before dead ones, then orders each215// group by owner address so the leaderboard is fully deterministic.216type byAliveThenOwner []*pet217218func (r byAliveThenOwner) Len() int { return len(r) }219func (r byAliveThenOwner) Swap(i, j int) { r[i], r[j] = r[j], r[i] }220func (r byAliveThenOwner) Less(i, j int) bool {221 if r[i].alive != r[j].alive {222 return r[i].alive223 }224 return r[i].owner.String() < r[j].owner.String()225}226227func bar(v int) string {228 filled := v / 10229 out := ""230 for i := 0; i < 10; i++ {231 if i < filled {232 out += "█"233 } else {234 out += "░"235 }236 }237 return out238}239240func shortAddr(a address) string {241 s := a.String()242 if len(s) > 12 {243 return s[:8] + "…" + s[len(s)-4:]244 }245 return s246}247248func renderCard(p *pet, height int64) string {249 hunger, happiness, energy, alive := project(p, height)250 out := "## " + p.name + " — owned by `" + shortAddr(p.owner) + "`\n\n"251 if !alive {252 out += "💀 **Deceased.** Owner can `Hatch(name)` a new pet.\n\n"253 return out254 }255 out += ageStageName(p.born, height) + " · age **" + strconv.Itoa(int(height-p.born)) + "** blocks\n\n"256 out += "- Hunger: " + bar(100-hunger) + " (" + strconv.Itoa(hunger) + "/100 — lower is better)\n"257 out += "- Happiness: " + bar(happiness) + " (" + strconv.Itoa(happiness) + "/100)\n"258 out += "- Energy: " + bar(energy) + " (" + strconv.Itoa(energy) + "/100)\n\n"259 return out260}261262// Render shows either every pet ever hatched (path == "") or a single pet's263// detail card when path is a bech32 owner address.264func Render(path string) string {265 height := runtime.ChainHeight()266267 out := "# 🐣 Tamagotchi\n\n"268 out += "A tiny on-chain pet. `Hatch(\"name\")` to start, then keep it alive with " +269 "`Feed()`, `Play()` and `Sleep()` — stats decay every block, so neglect kills it.\n\n"270271 if path != "" {272 owner := address(path)273 p, ok := get(owner)274 if !ok {275 return out + "_No pet found for `" + path + "`._\n"276 }277 return out + renderCard(p, height)278 }279280 var rows []*pet281 pets.Iterate("", "", func(_ string, v any) bool {282 rows = append(rows, v.(*pet))283 return false284 })285 if len(rows) == 0 {286 out += "_No pets hatched yet. Be the first!_\n"287 return out288 }289 sort.Stable(byAliveThenOwner(rows))290291 out += "| Pet | Owner | Status | Feeds | Plays | Deaths |\n"292 out += "| :--- | :--- | :--- | ---: | ---: | ---: |\n"293 for _, p := range rows {294 _, _, _, alive := project(p, height)295 status := "🐤 alive"296 if !alive {297 status = "💀 dead"298 }299 out += "| " + p.name + " | `" + shortAddr(p.owner) + "` | " + status +300 " | " + strconv.Itoa(p.feeds) + " | " + strconv.Itoa(p.plays) +301 " | " + strconv.Itoa(p.deaths) + " |\n"302 }303 out += "\n_View a single pet at `?<owner-address>`._\n"304 return out305}306Feed(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}) string
Hatch(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}, name string) string
Play(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}) string
Render(path string) string
Sleep(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}) 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.