Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}