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.

eggling.gno

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