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.

tamagotchi.gno

9.08 Kb · 317 lines
  1// Package tamagotchi is an on-chain virtual pet. Hatch one, then keep it
  2// alive by Feeding, Playing and letting it Sleep — hunger, happiness and
  3// 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 lifetime
  5// stats, so the leaderboard rewards long-term care, not just luck.
  6package tamagotchi
  7
  8import (
  9	"sort"
 10	"strconv"
 11
 12	"chain/runtime"
 13
 14	"gno.land/p/nym-moultest123/kit/ui/v0"
 15	"gno.land/p/nt/avl/v0"
 16)
 17
 18// pet is the persisted record for one owner's tamagotchi.
 19type pet struct {
 20	owner      address
 21	name       string
 22	born       int64
 23	lastUpdate int64
 24	hunger     int // 0 (full) .. 100 (starving)
 25	happiness  int // 0 (miserable) .. 100 (joyful)
 26	energy     int // 0 (exhausted) .. 100 (energetic)
 27	// happyDebt and energyDebt carry the blocks that have elapsed but not yet
 28	// cost a point, because happiness decays once per 2 blocks and energy once
 29	// per 3. Without them the division truncates on EVERY call, so acting once
 30	// per block cost 1/2 = 0 happiness and 1/3 = 0 energy forever: a pet fed in
 31	// a loop never aged. The remainder makes the decay depend on elapsed time
 32	// and not on how often someone calls.
 33	happyDebt  int // 0..1 blocks owed toward the next happiness point
 34	energyDebt int // 0..2 blocks owed toward the next energy point
 35	alive      bool
 36	deaths     int
 37	feeds      int
 38	plays      int
 39}
 40
 41var pets avl.Tree // owner address string -> *pet
 42
 43func clamp(v, lo, hi int) int {
 44	if v < lo {
 45		return lo
 46	}
 47	if v > hi {
 48		return hi
 49	}
 50	return v
 51}
 52
 53func get(owner address) (*pet, bool) {
 54	v := pets.Get(owner.String())
 55	if v == nil {
 56		return nil, false
 57	}
 58	return v.(*pet), true
 59}
 60
 61// tick applies stat decay for blocks elapsed since the pet's last update and
 62// kills it if any stat has bottomed/topped out. Mutates and persists.
 63func (p *pet) tick(height int64) {
 64	if !p.alive {
 65		return
 66	}
 67	elapsed := int(height - p.lastUpdate)
 68	if elapsed <= 0 {
 69		return
 70	}
 71	p.hunger = clamp(p.hunger+elapsed, 0, 100)
 72
 73	// Spend the elapsed blocks against the debt, take whole points out, and
 74	// keep the remainder for next time.
 75	p.happyDebt += elapsed
 76	p.happiness = clamp(p.happiness-p.happyDebt/2, 0, 100)
 77	p.happyDebt %= 2
 78
 79	p.energyDebt += elapsed
 80	p.energy = clamp(p.energy-p.energyDebt/3, 0, 100)
 81	p.energyDebt %= 3
 82
 83	p.lastUpdate = height
 84	if p.hunger >= 100 || p.happiness <= 0 || p.energy <= 0 {
 85		p.alive = false
 86		p.deaths++
 87	}
 88}
 89
 90// project computes display stats as of height without mutating the pet, so
 91// Render can show a live-looking view without writing state on a query.
 92func project(p *pet, height int64) (hunger, happiness, energy int, alive bool) {
 93	if !p.alive {
 94		return p.hunger, p.happiness, p.energy, false
 95	}
 96	elapsed := int(height - p.lastUpdate)
 97	if elapsed < 0 {
 98		elapsed = 0
 99	}
100	// Same arithmetic as tick, including the carried debt, so the page never
101	// disagrees with what the next call will do.
102	hunger = clamp(p.hunger+elapsed, 0, 100)
103	happiness = clamp(p.happiness-(p.happyDebt+elapsed)/2, 0, 100)
104	energy = clamp(p.energy-(p.energyDebt+elapsed)/3, 0, 100)
105	alive = hunger < 100 && happiness > 0 && energy > 0
106	return
107}
108
109func ageStageName(born, height int64) string {
110	switch age := height - born; {
111	case age < 10:
112		return "🥚 Egg"
113	case age < 50:
114		return "🐣 Baby"
115	case age < 200:
116		return "🐤 Teen"
117	default:
118		return "🐓 Adult"
119	}
120}
121
122// requireLivingPet ticks and fetches the caller's pet, panicking with a
123// friendly message if there is none or it has died.
124func requireLivingPet(owner address, height int64) *pet {
125	p, ok := get(owner)
126	if !ok {
127		panic("you don't have a pet yet — call Hatch(name) first")
128	}
129	p.tick(height)
130	if !p.alive {
131		panic(p.name + " has died of neglect. Call Hatch(name) to start over.")
132	}
133	return p
134}
135
136// hatch is the non-crossing core of Hatch, kept separate so unit tests can
137// exercise its panics directly without going through a realm-crossing call.
138func hatch(owner address, name string, height int64) string {
139	if name == "" {
140		panic("name cannot be empty")
141	}
142
143	deaths, feeds, plays := 0, 0, 0
144	if existing, ok := get(owner); ok {
145		if existing.alive {
146			existing.tick(height)
147		}
148		if existing.alive {
149			panic("you already have a living pet: " + existing.name)
150		}
151		deaths, feeds, plays = existing.deaths, existing.feeds, existing.plays
152	}
153
154	pets.Set(owner.String(), &pet{
155		owner:      owner,
156		name:       name,
157		born:       height,
158		lastUpdate: height,
159		hunger:     20,
160		happiness:  80,
161		energy:     80,
162		alive:      true,
163		deaths:     deaths,
164		feeds:      feeds,
165		plays:      plays,
166	})
167	return "🥚 " + name + " has hatched!"
168}
169
170// Hatch creates a new pet for the caller. Lifetime feeds/plays/deaths carry
171// over from any previous pet so the leaderboard tracks long-term care.
172func Hatch(cur realm, name string) string {
173	if !cur.IsCurrent() {
174		panic("spoofed realm")
175	}
176	return hatch(cur.Previous().Address(), name, runtime.ChainHeight())
177}
178
179// feed is the non-crossing core of Feed.
180func feed(owner address, height int64) string {
181	p := requireLivingPet(owner, height)
182	p.hunger = clamp(p.hunger-30, 0, 100)
183	p.energy = clamp(p.energy+5, 0, 100)
184	p.feeds++
185	return p.name + " munches happily. Hunger: " + strconv.Itoa(p.hunger) + "/100"
186}
187
188// Feed reduces hunger and gives a small energy boost.
189func Feed(cur realm) string {
190	if !cur.IsCurrent() {
191		panic("spoofed realm")
192	}
193	return feed(cur.Previous().Address(), runtime.ChainHeight())
194}
195
196// play is the non-crossing core of Play.
197func play(owner address, height int64) string {
198	p := requireLivingPet(owner, height)
199	if p.energy < 10 {
200		return p.name + " is too tired to play — try Sleep() first."
201	}
202	p.happiness = clamp(p.happiness+20, 0, 100)
203	p.energy = clamp(p.energy-10, 0, 100)
204	p.hunger = clamp(p.hunger+5, 0, 100)
205	p.plays++
206	return p.name + " had a blast! Happiness: " + strconv.Itoa(p.happiness) + "/100"
207}
208
209// Play boosts happiness at the cost of energy and a bit of hunger. Refuses
210// to run a tired pet into the ground — rest first.
211func Play(cur realm) string {
212	if !cur.IsCurrent() {
213		panic("spoofed realm")
214	}
215	return play(cur.Previous().Address(), runtime.ChainHeight())
216}
217
218// sleep is the non-crossing core of Sleep.
219func sleep(owner address, height int64) string {
220	p := requireLivingPet(owner, height)
221	p.energy = clamp(p.energy+40, 0, 100)
222	p.hunger = clamp(p.hunger+5, 0, 100)
223	return p.name + " takes a nap. Energy: " + strconv.Itoa(p.energy) + "/100"
224}
225
226// Sleep restores energy at the cost of a little hunger.
227func Sleep(cur realm) string {
228	if !cur.IsCurrent() {
229		panic("spoofed realm")
230	}
231	return sleep(cur.Previous().Address(), runtime.ChainHeight())
232}
233
234// byAliveThenOwner ranks living pets before dead ones, then orders each
235// group by owner address so the leaderboard is fully deterministic.
236type byAliveThenOwner []*pet
237
238func (r byAliveThenOwner) Len() int      { return len(r) }
239func (r byAliveThenOwner) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
240func (r byAliveThenOwner) Less(i, j int) bool {
241	if r[i].alive != r[j].alive {
242		return r[i].alive
243	}
244	return r[i].owner.String() < r[j].owner.String()
245}
246
247func bar(v int) string {
248	filled := v / 10
249	out := ""
250	for i := 0; i < 10; i++ {
251		if i < filled {
252			out += "█"
253		} else {
254			out += "░"
255		}
256	}
257	return out
258}
259
260func renderCard(p *pet, height int64) string {
261	hunger, happiness, energy, alive := project(p, height)
262	out := "## " + p.name + " — owned by " + ui.Addr(p.owner) + "\n\n"
263	if !alive {
264		out += "💀 **Deceased.** Owner can `Hatch(name)` a new pet.\n\n"
265		return out
266	}
267	out += ageStageName(p.born, height) + " · age **" + strconv.Itoa(int(height-p.born)) + "** blocks\n\n"
268	out += "- Hunger:    " + bar(100-hunger) + " (" + strconv.Itoa(hunger) + "/100 — lower is better)\n"
269	out += "- Happiness: " + bar(happiness) + " (" + strconv.Itoa(happiness) + "/100)\n"
270	out += "- Energy:    " + bar(energy) + " (" + strconv.Itoa(energy) + "/100)\n\n"
271	return out
272}
273
274// Render shows either every pet ever hatched (path == "") or a single pet's
275// detail card when path is a bech32 owner address.
276func Render(path string) string {
277	height := runtime.ChainHeight()
278
279	out := "# 🐣 Tamagotchi\n\n"
280	out += "A tiny on-chain pet. `Hatch(\"name\")` to start, then keep it alive with " +
281		"`Feed()`, `Play()` and `Sleep()` — stats decay every block, so neglect kills it.\n\n"
282
283	if path != "" {
284		owner := address(path)
285		p, ok := get(owner)
286		if !ok {
287			return out + "_No pet found for `" + path + "`._\n"
288		}
289		return out + renderCard(p, height)
290	}
291
292	var rows []*pet
293	pets.Iterate("", "", func(_ string, v any) bool {
294		rows = append(rows, v.(*pet))
295		return false
296	})
297	if len(rows) == 0 {
298		out += "_No pets hatched yet. Be the first!_\n"
299		return out
300	}
301	sort.Stable(byAliveThenOwner(rows))
302
303	out += "| Pet | Owner | Status | Feeds | Plays | Deaths |\n"
304	out += "| :--- | :--- | :--- | ---: | ---: | ---: |\n"
305	for _, p := range rows {
306		_, _, _, alive := project(p, height)
307		status := "🐤 alive"
308		if !alive {
309			status = "💀 dead"
310		}
311		out += "| " + p.name + " | " + ui.Addr(p.owner) + " | " + status +
312			" | " + strconv.Itoa(p.feeds) + " | " + strconv.Itoa(p.plays) +
313			" | " + strconv.Itoa(p.deaths) + " |\n"
314	}
315	out += "\n_View a single pet at `?<owner-address>`._\n"
316	return out
317}