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 Hatch3// it: the longer you waited (and a little luck, seeded from the egg's own4// history) decides the rarity of the creature you get. Train hatched5// creatures to level them up. Unlike a tamagotchi there's nothing to feed6// or lose to neglect — eggling is about patience and the luck of the7// hatch, not survival.8package eggling910import (11 "crypto/sha256"12 "encoding/hex"13 "sort"14 "strconv"15 "strings"1617 "chain/runtime"1819 "gno.land/p/nt/avl/v0"20)2122// minIncubation is the minimum number of blocks an egg must sit before it23// can hatch.24const minIncubation int64 = 202526// 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 = 2002930var speciesNames = [...]string{31 "Slime", "Sprout", "Ember", "Pebble", "Breeze", "Glimmer", "Shade", "Coral",32}3334var rarityNames = [...]string{"Common", "Uncommon", "Rare", "Legendary"}35var rarityEmoji = [...]string{"⚪", "🟢", "🔵", "🟡"}3637// egg is the persisted record for one planted egg, hatched or not.38type egg struct {39 ID string40 Owner address41 PlantedAt int6442 Hatched bool43 Species string44 RarityTier int45 Name string46 XP int47 Level int48}4950var (51 eggs avl.Tree // id string -> *egg52 nextID int53 totalPlanted int54 totalHatched int55 rarityCounts [4]int56)5758func get(id string) (*egg, bool) {59 v := eggs.Get(id)60 if v == nil {61 return nil, false62 }63 return v.(*egg), true64}6566// hashSeed hashes the given parts (joined with ":") to a deterministic hex67// digest. Used to derive an egg's species and rarity roll from facts68// already fixed at plant time, so nobody — not even the owner — can game69// the outcome after the fact.70func hashSeed(parts ...string) string {71 sum := sha256.Sum256([]byte(strings.Join(parts, ":")))72 return hex.EncodeToString(sum[:])73}7475// seedBytes pulls the first two bytes out of a hex digest for use as two76// 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, 081 }82 return raw[0], raw[1]83}8485// rarityTier turns a 0-255 roll plus the blocks waited into a tier index86// 0..3 (common..legendary). Waiting longer raises the effective score, so87// patience improves your odds, but a bad roll can still land common even88// after a long wait, and a lucky roll can hatch rare almost immediately.89func rarityTier(roll uint8, waitBlocks int64) int {90 bonus := waitBlocks91 if bonus > waitCap {92 bonus = waitCap93 }94 score := int(roll) + int(bonus)/295 switch {96 case score >= 300:97 return 398 case score >= 220:99 return 2100 case score >= 140:101 return 1102 default:103 return 0104 }105}106107// 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 e115}116117// 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}126127// 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.PlantedAt133 if wait < minIncubation {134 panic("egg #" + e.ID + " needs " + strconv.Itoa(int(minIncubation-wait)) + " more blocks to incubate")135 }136137 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)]141142 e.Hatched = true143 e.Species = species144 e.RarityTier = tier145 e.Name = species146 e.Level = 1147 rarityCounts[tier]++148 totalHatched++149150 return rarityEmoji[tier] + " egg #" + e.ID + " hatched into a " + rarityNames[tier] + " " + species + "!"151}152153// 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}167168// 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 += 10174 newLevel := e.XP/50 + 1175 leveled := newLevel > e.Level176 e.Level = newLevel177178 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 msg183}184185// Train gives a hatched creature the caller owns some experience, leveling186// 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}200201// 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.Name211 e.Name = name212 return old + " renamed to " + name213}214215// 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}229230// escapeInline neutralizes markdown-active characters in untrusted text231// 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}244245func shortAddr(a address) string {246 s := a.String()247 if len(s) > 12 {248 return s[:8] + "…" + s[len(s)-4:]249 }250 return s251}252253// byIDNumeric orders eggs by their numeric ID, ascending.254type byIDNumeric []*egg255256func (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 < b262}263264func allEggs() []*egg {265 var rows []*egg266 eggs.Iterate("", "", func(_ string, v any) bool {267 rows = append(rows, v.(*egg))268 return false269 })270 sort.Stable(byIDNumeric(rows))271 return rows272}273274func renderHome() string {275 var b strings.Builder276 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")281282 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 }292293 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 }299300 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}315316func 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 }321322 var b strings.Builder323 if !e.Hatched {324 wait := height - e.PlantedAt325 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 }335336 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}343344func renderOwner(rawAddr string) string {345 owner := address(strings.TrimSpace(rawAddr))346 safe := escapeInline(owner.String())347348 var b strings.Builder349 b.WriteString("# Eggs owned by " + safe + "\n\n")350351 found := false352 for _, e := range allEggs() {353 if e.Owner != owner {354 continue355 }356 found = true357 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}369370// Render shows the full egg list at "", a single egg's detail when path is371// 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}382Hatch(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}, id string) string
PlantEgg(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
Rename(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}, id string, name string) string
Render(path string) string
Train(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}, id 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.