← Back to Realms

Realm

gno.land/r/moul/x/daily/eggling/v0

Overview

Realm Path
gno.land/r/moul/x/daily/eggling/v0
Exported Functions
5
State Entries
12
Source Files
2
Total Package Entries
31

Exported Functions

5 exported functions

State

12 state entries

Source Code

FILES
eggling.gno
go
1// Package eggling is an on-chain egg incubator and creature collector.
2// Plant an egg, let it incubate for a minimum number of blocks, then Hatch
3// it: the longer you waited (and a little luck, seeded from the egg's own
4// history) decides the rarity of the creature you get. Train hatched
5// creatures to level them up. Unlike a tamagotchi there's nothing to feed
6// or lose to neglect — eggling is about patience and the luck of the
7// hatch, not survival.
8package eggling
9
10import (
11	"crypto/sha256"
12	"encoding/hex"
13	"sort"
14	"strconv"
15	"strings"
16
17	"chain/runtime"
18
19	"gno.land/p/nt/avl/v0"
20)
21
22// minIncubation is the minimum number of blocks an egg must sit before it
23// can hatch.
24const minIncubation int64 = 20
25
26// waitCap is the incubation-time bonus cap (in blocks) used by rarityTier,
27// so waiting forever doesn't guarantee a legendary — it just improves odds.
28const waitCap int64 = 200
29
30var speciesNames = [...]string{
31	"Slime", "Sprout", "Ember", "Pebble", "Breeze", "Glimmer", "Shade", "Coral",
32}
33
34var rarityNames = [...]string{"Common", "Uncommon", "Rare", "Legendary"}
35var rarityEmoji = [...]string{"⚪", "🟢", "🔵", "🟡"}
36
37// egg is the persisted record for one planted egg, hatched or not.
38type egg struct {
39	ID         string
40	Owner      address
41	PlantedAt  int64
42	Hatched    bool
43	Species    string
44	RarityTier int
45	Name       string
46	XP         int
47	Level      int
48}
49
50var (
51	eggs         avl.Tree // id string -> *egg
52	nextID       int
53	totalPlanted int
54	totalHatched int
55	rarityCounts [4]int
56)
57
58func get(id string) (*egg, bool) {
59	v := eggs.Get(id)
60	if v == nil {
61		return nil, false
62	}
63	return v.(*egg), true
64}
65
66// hashSeed hashes the given parts (joined with ":") to a deterministic hex
67// digest. Used to derive an egg's species and rarity roll from facts
68// already fixed at plant time, so nobody — not even the owner — can game
69// the outcome after the fact.
70func hashSeed(parts ...string) string {
71	sum := sha256.Sum256([]byte(strings.Join(parts, ":")))
72	return hex.EncodeToString(sum[:])
73}
74
75// seedBytes pulls the first two bytes out of a hex digest for use as two
76// independent 0-255 rolls (species pick, rarity roll).
77func seedBytes(seedHex string) (byte, byte) {
78	raw, err := hex.DecodeString(seedHex)
79	if err != nil || len(raw) < 2 {
80		return 0, 0
81	}
82	return raw[0], raw[1]
83}
84
85// rarityTier turns a 0-255 roll plus the blocks waited into a tier index
86// 0..3 (common..legendary). Waiting longer raises the effective score, so
87// patience improves your odds, but a bad roll can still land common even
88// after a long wait, and a lucky roll can hatch rare almost immediately.
89func rarityTier(roll uint8, waitBlocks int64) int {
90	bonus := waitBlocks
91	if bonus > waitCap {
92		bonus = waitCap
93	}
94	score := int(roll) + int(bonus)/2
95	switch {
96	case score >= 300:
97		return 3
98	case score >= 220:
99		return 2
100	case score >= 140:
101		return 1
102	default:
103		return 0
104	}
105}
106
107// plant is the non-crossing core of PlantEgg.
108func plant(owner address, height int64) *egg {
109	nextID++
110	id := strconv.Itoa(nextID)
111	e := &egg{ID: id, Owner: owner, PlantedAt: height}
112	eggs.Set(id, e)
113	totalPlanted++
114	return e
115}
116
117// PlantEgg starts incubating a new egg for the caller. Returns its ID.
118func PlantEgg(cur realm) string {
119	if !cur.IsCurrent() {
120		panic("spoofed realm")
121	}
122	e := plant(cur.Previous().Address(), runtime.ChainHeight())
123	return "🥚 planted egg #" + e.ID + " — incubate at least " +
124		strconv.Itoa(int(minIncubation)) + " blocks, then Hatch(\"" + e.ID + "\")"
125}
126
127// hatch is the non-crossing core of Hatch.
128func hatch(e *egg, height int64) string {
129	if e.Hatched {
130		panic("egg #" + e.ID + " already hatched into a " + e.Species)
131	}
132	wait := height - e.PlantedAt
133	if wait < minIncubation {
134		panic("egg #" + e.ID + " needs " + strconv.Itoa(int(minIncubation-wait)) + " more blocks to incubate")
135	}
136
137	seed := hashSeed(e.ID, e.Owner.String(), strconv.FormatInt(e.PlantedAt, 10))
138	rarityRoll, speciesRoll := seedBytes(seed)
139	tier := rarityTier(rarityRoll, wait)
140	species := speciesNames[int(speciesRoll)%len(speciesNames)]
141
142	e.Hatched = true
143	e.Species = species
144	e.RarityTier = tier
145	e.Name = species
146	e.Level = 1
147	rarityCounts[tier]++
148	totalHatched++
149
150	return rarityEmoji[tier] + " egg #" + e.ID + " hatched into a " + rarityNames[tier] + " " + species + "!"
151}
152
153// Hatch hatches an incubated egg the caller owns.
154func Hatch(cur realm, id string) string {
155	if !cur.IsCurrent() {
156		panic("spoofed realm")
157	}
158	e, ok := get(id)
159	if !ok {
160		panic("no such egg #" + id)
161	}
162	if e.Owner != cur.Previous().Address() {
163		panic("only the owner can hatch egg #" + id)
164	}
165	return hatch(e, runtime.ChainHeight())
166}
167
168// train is the non-crossing core of Train.
169func train(e *egg) string {
170	if !e.Hatched {
171		panic("egg #" + e.ID + " hasn't hatched yet")
172	}
173	e.XP += 10
174	newLevel := e.XP/50 + 1
175	leveled := newLevel > e.Level
176	e.Level = newLevel
177
178	msg := e.Name + " gained 10 XP (" + strconv.Itoa(e.XP) + " total)"
179	if leveled {
180		msg += " and reached level " + strconv.Itoa(e.Level) + "!"
181	}
182	return msg
183}
184
185// Train gives a hatched creature the caller owns some experience, leveling
186// it up every 50 XP.
187func Train(cur realm, id string) string {
188	if !cur.IsCurrent() {
189		panic("spoofed realm")
190	}
191	e, ok := get(id)
192	if !ok {
193		panic("no such egg #" + id)
194	}
195	if e.Owner != cur.Previous().Address() {
196		panic("only the owner can train egg #" + id)
197	}
198	return train(e)
199}
200
201// rename is the non-crossing core of Rename.
202func rename(e *egg, name string) string {
203	if !e.Hatched {
204		panic("egg #" + e.ID + " hasn't hatched yet — nothing to name")
205	}
206	name = strings.TrimSpace(name)
207	if name == "" {
208		panic("name cannot be empty")
209	}
210	old := e.Name
211	e.Name = name
212	return old + " renamed to " + name
213}
214
215// Rename gives a hatched creature the caller owns a custom name.
216func Rename(cur realm, id string, name string) string {
217	if !cur.IsCurrent() {
218		panic("spoofed realm")
219	}
220	e, ok := get(id)
221	if !ok {
222		panic("no such egg #" + id)
223	}
224	if e.Owner != cur.Previous().Address() {
225		panic("only the owner can rename egg #" + id)
226	}
227	return rename(e, name)
228}
229
230// escapeInline neutralizes markdown-active characters in untrusted text
231// before it's embedded inline in Render output.
232func escapeInline(s string) string {
233	r := strings.NewReplacer(
234		"\\", "\\\\",
235		"`", "\\`",
236		"*", "\\*",
237		"_", "\\_",
238		"[", "\\[",
239		"]", "\\]",
240		"|", "\\|",
241	)
242	return r.Replace(s)
243}
244
245func shortAddr(a address) string {
246	s := a.String()
247	if len(s) > 12 {
248		return s[:8] + "…" + s[len(s)-4:]
249	}
250	return s
251}
252
253// byIDNumeric orders eggs by their numeric ID, ascending.
254type byIDNumeric []*egg
255
256func (r byIDNumeric) Len() int      { return len(r) }
257func (r byIDNumeric) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
258func (r byIDNumeric) Less(i, j int) bool {
259	a, _ := strconv.Atoi(r[i].ID)
260	b, _ := strconv.Atoi(r[j].ID)
261	return a < b
262}
263
264func allEggs() []*egg {
265	var rows []*egg
266	eggs.Iterate("", "", func(_ string, v any) bool {
267		rows = append(rows, v.(*egg))
268		return false
269	})
270	sort.Stable(byIDNumeric(rows))
271	return rows
272}
273
274func renderHome() string {
275	var b strings.Builder
276	b.WriteString("# 🥚 Eggling\n\n")
277	b.WriteString("An on-chain egg incubator. `PlantEgg()` to start one, wait at least " +
278		strconv.Itoa(int(minIncubation)) + " blocks, then `Hatch(id)` — the longer you " +
279		"waited (plus a little sealed-in luck) decides the rarity of the creature you get. " +
280		"`Train(id)` levels a hatched creature up; `Rename(id, name)` gives it a name.\n\n")
281
282	b.WriteString("- Eggs planted: " + strconv.Itoa(totalPlanted) + "\n")
283	b.WriteString("- Creatures hatched: " + strconv.Itoa(totalHatched) + "\n")
284	if totalHatched > 0 {
285		for tier := 3; tier >= 0; tier-- {
286			if rarityCounts[tier] > 0 {
287				b.WriteString("  - " + rarityEmoji[tier] + " " + rarityNames[tier] + ": " +
288					strconv.Itoa(rarityCounts[tier]) + "\n")
289			}
290		}
291	}
292
293	rows := allEggs()
294	b.WriteString("\n## Eggs\n\n")
295	if len(rows) == 0 {
296		b.WriteString("_None planted yet. Be the first!_\n")
297		return b.String()
298	}
299
300	b.WriteString("| ID | Owner | Status | Level |\n")
301	b.WriteString("| :--- | :--- | :--- | ---: |\n")
302	for _, e := range rows {
303		status := "🥚 incubating"
304		level := "-"
305		if e.Hatched {
306			status = rarityEmoji[e.RarityTier] + " " + escapeInline(e.Name) + " (" + rarityNames[e.RarityTier] + " " + e.Species + ")"
307			level = strconv.Itoa(e.Level)
308		}
309		b.WriteString("| #" + e.ID + " | `" + shortAddr(e.Owner) + "` | " + status + " | " + level + " |\n")
310	}
311	b.WriteString("\n_View a single egg at this realm's path plus its ID (e.g. `.../eggling:3`), " +
312		"or one owner's eggs plus their address._\n")
313	return b.String()
314}
315
316func renderEgg(id string, height int64) string {
317	e, ok := get(id)
318	if !ok {
319		return "# Egg #" + escapeInline(id) + "\n\nNo such egg.\n"
320	}
321
322	var b strings.Builder
323	if !e.Hatched {
324		wait := height - e.PlantedAt
325		b.WriteString("# 🥚 Egg #" + e.ID + "\n\n")
326		b.WriteString("- Owner: `" + e.Owner.String() + "`\n")
327		b.WriteString("- Planted at block: " + strconv.FormatInt(e.PlantedAt, 10) + "\n")
328		if wait < minIncubation {
329			b.WriteString("- Ready to hatch in: " + strconv.FormatInt(minIncubation-wait, 10) + " more blocks\n")
330		} else {
331			b.WriteString("- Ready to hatch now — call `Hatch(\"" + e.ID + "\")`\n")
332		}
333		return b.String()
334	}
335
336	b.WriteString("# " + rarityEmoji[e.RarityTier] + " " + e.Name + "\n\n")
337	b.WriteString("- Owner: `" + e.Owner.String() + "`\n")
338	b.WriteString("- Species: " + e.Species + "\n")
339	b.WriteString("- Rarity: " + rarityNames[e.RarityTier] + "\n")
340	b.WriteString("- Level: " + strconv.Itoa(e.Level) + " (" + strconv.Itoa(e.XP) + " XP)\n")
341	return b.String()
342}
343
344func renderOwner(rawAddr string) string {
345	owner := address(strings.TrimSpace(rawAddr))
346	safe := escapeInline(owner.String())
347
348	var b strings.Builder
349	b.WriteString("# Eggs owned by " + safe + "\n\n")
350
351	found := false
352	for _, e := range allEggs() {
353		if e.Owner != owner {
354			continue
355		}
356		found = true
357		if e.Hatched {
358			b.WriteString("- #" + e.ID + ": " + rarityEmoji[e.RarityTier] + " " + escapeInline(e.Name) +
359				" — " + rarityNames[e.RarityTier] + " " + e.Species + ", level " + strconv.Itoa(e.Level) + "\n")
360		} else {
361			b.WriteString("- #" + e.ID + ": 🥚 incubating\n")
362		}
363	}
364	if !found {
365		b.WriteString("_No eggs found for this address._\n")
366	}
367	return b.String()
368}
369
370// Render shows the full egg list at "", a single egg's detail when path is
371// a numeric ID, or one owner's eggs when path is a bech32 address.
372func Render(path string) string {
373	path = strings.TrimPrefix(strings.TrimSpace(path), "/")
374	if path == "" {
375		return renderHome()
376	}
377	if _, err := strconv.Atoi(path); err == nil {
378		return renderEgg(path, runtime.ChainHeight())
379	}
380	return renderOwner(path)
381}
382

Raw Package Data

Raw JSON data