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_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}