eggling_test.gno
6.54 Kb · 269 lines
1package eggling
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/testutils/v0"
9)
10
11func resetState() {
12 eggs = avl.Tree{}
13 nextID = 0
14 totalPlanted = 0
15 totalHatched = 0
16 rarityCounts = [4]int{}
17}
18
19func TestHashSeedDeterministicAndSensitive(t *testing.T) {
20 a := hashSeed("1", "g1alice", "100")
21 b := hashSeed("1", "g1alice", "100")
22 if a != b {
23 t.Fatal("hashSeed should be deterministic for the same inputs")
24 }
25 if a == hashSeed("2", "g1alice", "100") {
26 t.Fatal("different egg IDs should hash differently")
27 }
28 if len(a) != 64 {
29 t.Fatalf("hashSeed length = %d, want 64 (hex sha256)", len(a))
30 }
31}
32
33func TestSeedBytesFromKnownDigest(t *testing.T) {
34 // sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
35 r1, r2 := seedBytes(hashSeed(""))
36 if r1 != 0xe3 || r2 != 0xb0 {
37 t.Fatalf("seedBytes = (%x, %x), want (e3, b0)", r1, r2)
38 }
39}
40
41func TestRarityTierBoundaries(t *testing.T) {
42 cases := []struct {
43 roll uint8
44 wait int64
45 want int
46 }{
47 {0, 0, 0}, // score 0 -> common
48 {139, 0, 0}, // score 139 -> common
49 {140, 0, 1}, // score 140 -> uncommon
50 {219, 0, 1}, // score 219 -> uncommon
51 {220, 0, 2}, // score 220 -> rare
52 {255, 0, 2}, // score 255 -> rare (no wait bonus)
53 {255, 200, 3}, // score 355 -> legendary (max wait bonus)
54 {0, 200, 0}, // score 100 -> still common, roll matters too
55 }
56 for _, c := range cases {
57 if got := rarityTier(c.roll, c.wait); got != c.want {
58 t.Fatalf("rarityTier(%d, %d) = %d, want %d", c.roll, c.wait, got, c.want)
59 }
60 }
61}
62
63func TestRarityTierCapsWaitBonus(t *testing.T) {
64 // waitCap is 200; waiting 1000 blocks should score identically to
65 // waiting exactly 200.
66 if rarityTier(50, 1000) != rarityTier(50, 200) {
67 t.Fatal("rarityTier should cap the wait bonus at waitCap")
68 }
69}
70
71func TestPlantAssignsSequentialIDs(t *testing.T) {
72 resetState()
73 alice := testutils.TestAddress("alice")
74 e1 := plant(alice, 100)
75 e2 := plant(alice, 101)
76 if e1.ID != "1" || e2.ID != "2" {
77 t.Fatalf("expected sequential IDs 1, 2; got %s, %s", e1.ID, e2.ID)
78 }
79 if totalPlanted != 2 {
80 t.Fatalf("totalPlanted = %d, want 2", totalPlanted)
81 }
82}
83
84func TestHatchTooEarlyPanics(t *testing.T) {
85 resetState()
86 alice := testutils.TestAddress("alice")
87 e := plant(alice, 100)
88
89 defer func() {
90 if r := recover(); r == nil {
91 t.Fatal("expected panic hatching before minIncubation blocks pass")
92 }
93 }()
94 hatch(e, 100+minIncubation-1)
95}
96
97func TestHatchTwicePanics(t *testing.T) {
98 resetState()
99 alice := testutils.TestAddress("alice")
100 e := plant(alice, 100)
101 hatch(e, 100+minIncubation)
102
103 defer func() {
104 if r := recover(); r == nil {
105 t.Fatal("expected panic hatching an already-hatched egg")
106 }
107 }()
108 hatch(e, 100+minIncubation)
109}
110
111func TestHatchSetsSpeciesAndCountsStats(t *testing.T) {
112 resetState()
113 alice := testutils.TestAddress("alice")
114 e := plant(alice, 100)
115 hatch(e, 100+minIncubation)
116
117 if !e.Hatched {
118 t.Fatal("expected egg to be hatched")
119 }
120 if e.Species == "" {
121 t.Fatal("expected a species to be assigned")
122 }
123 if e.Level != 1 {
124 t.Fatalf("expected level 1 on hatch, got %d", e.Level)
125 }
126 if totalHatched != 1 {
127 t.Fatalf("totalHatched = %d, want 1", totalHatched)
128 }
129 if rarityCounts[e.RarityTier] != 1 {
130 t.Fatalf("expected rarityCounts[%d] = 1, got %d", e.RarityTier, rarityCounts[e.RarityTier])
131 }
132}
133
134func TestTrainBeforeHatchPanics(t *testing.T) {
135 resetState()
136 alice := testutils.TestAddress("alice")
137 e := plant(alice, 100)
138
139 defer func() {
140 if r := recover(); r == nil {
141 t.Fatal("expected panic training an unhatched egg")
142 }
143 }()
144 train(e)
145}
146
147func TestTrainAccumulatesXPAndLevelsUp(t *testing.T) {
148 resetState()
149 alice := testutils.TestAddress("alice")
150 e := plant(alice, 100)
151 hatch(e, 100+minIncubation)
152
153 for i := 0; i < 5; i++ {
154 train(e)
155 }
156 if e.XP != 50 {
157 t.Fatalf("XP = %d, want 50", e.XP)
158 }
159 if e.Level != 2 {
160 t.Fatalf("Level = %d, want 2", e.Level)
161 }
162}
163
164func TestRenameRequiresHatchedAndNonEmpty(t *testing.T) {
165 resetState()
166 alice := testutils.TestAddress("alice")
167 e := plant(alice, 100)
168
169 func() {
170 defer func() {
171 if r := recover(); r == nil {
172 t.Fatal("expected panic renaming an unhatched egg")
173 }
174 }()
175 rename(e, "Sparky")
176 }()
177
178 hatch(e, 100+minIncubation)
179 rename(e, "Sparky")
180 if e.Name != "Sparky" {
181 t.Fatalf("Name = %q, want Sparky", e.Name)
182 }
183
184 defer func() {
185 if r := recover(); r == nil {
186 t.Fatal("expected panic renaming with an empty name")
187 }
188 }()
189 rename(e, " ")
190}
191
192func TestRenderShowsEmptyThenEggThenOwner(t *testing.T) {
193 resetState()
194 if !strings.Contains(Render(""), "None planted yet") {
195 t.Fatal("expected empty-state placeholder")
196 }
197
198 alice := testutils.TestAddress("alice")
199 e := plant(alice, 100)
200 hatch(e, 100+minIncubation)
201
202 home := Render("")
203 if !strings.Contains(home, "#"+e.ID) {
204 t.Fatal("expected egg row in home listing")
205 }
206
207 detail := Render(e.ID)
208 if !strings.Contains(detail, "Rarity") {
209 t.Fatal("expected detail card to show rarity")
210 }
211
212 owned := Render(alice.String())
213 if !strings.Contains(owned, e.Name) {
214 t.Fatal("expected owner view to list the creature by name")
215 }
216}
217
218// TestFullLifecycleViaCrossingCalls smoke-tests the exported, realm-crossing
219// entry points end to end (as an on-chain caller would invoke them).
220func TestFullLifecycleViaCrossingCalls(cur realm, t *testing.T) {
221 resetState()
222 alice := testutils.TestAddress("alice")
223 testing.SetRealm(testing.NewUserRealm(alice))
224
225 msg := PlantEgg(cross(cur))
226 if !strings.Contains(msg, "planted egg #1") {
227 t.Fatalf("expected plant confirmation, got %q", msg)
228 }
229
230 testing.SkipHeights(minIncubation)
231
232 hatchMsg := Hatch(cross(cur), "1")
233 if !strings.Contains(hatchMsg, "hatched into a") {
234 t.Fatalf("expected hatch confirmation, got %q", hatchMsg)
235 }
236
237 trainMsg := Train(cross(cur), "1")
238 if !strings.Contains(trainMsg, "gained 10 XP") {
239 t.Fatalf("expected train confirmation, got %q", trainMsg)
240 }
241
242 renameMsg := Rename(cross(cur), "1", "Buddy")
243 if !strings.Contains(renameMsg, "renamed to Buddy") {
244 t.Fatalf("expected rename confirmation, got %q", renameMsg)
245 }
246
247 e, ok := get("1")
248 if !ok || e.Name != "Buddy" || e.XP != 10 {
249 t.Fatalf("unexpected final state: %+v", e)
250 }
251}
252
253// TestOnlyOwnerCanHatch checks that another address can't hatch someone
254// else's egg.
255func TestOnlyOwnerCanHatch(cur realm, t *testing.T) {
256 resetState()
257 alice := testutils.TestAddress("alice")
258 bob := testutils.TestAddress("bob")
259
260 testing.SetRealm(testing.NewUserRealm(alice))
261 PlantEgg(cross(cur))
262 testing.SkipHeights(minIncubation)
263
264 testing.SetRealm(testing.NewUserRealm(bob))
265 rec := revive(func() { Hatch(cross(cur), "1") })
266 if rec == nil {
267 t.Fatal("expected panic hatching someone else's egg")
268 }
269}