← Back to Realms

Realm

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

Overview

Realm Path
gno.land/r/moul/x/daily/eggling/v1
Exported Functions
5
State Entries
12
Source Files
3
Total Package Entries
29

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

Raw Package Data

Raw JSON data