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.

tamagotchi_test.gno

6.31 Kb · 233 lines
  1package tamagotchi
  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	pets = avl.Tree{}
 13}
 14
 15func TestClamp(t *testing.T) {
 16	if clamp(150, 0, 100) != 100 {
 17		t.Fatal("clamp should cap at hi")
 18	}
 19	if clamp(-5, 0, 100) != 0 {
 20		t.Fatal("clamp should floor at lo")
 21	}
 22	if clamp(50, 0, 100) != 50 {
 23		t.Fatal("clamp should pass through in-range values")
 24	}
 25}
 26
 27func TestHatchCreatesLivingPet(t *testing.T) {
 28	resetState()
 29	alice := testutils.TestAddress("alice")
 30
 31	hatch(alice, "Fluffy", 100)
 32
 33	p, ok := get(alice)
 34	if !ok || !p.alive {
 35		t.Fatal("expected a living pet after hatch")
 36	}
 37	if p.name != "Fluffy" {
 38		t.Fatalf("expected name Fluffy, got %s", p.name)
 39	}
 40}
 41
 42func TestHatchTwiceWhileAlivePanics(t *testing.T) {
 43	resetState()
 44	alice := testutils.TestAddress("alice")
 45	hatch(alice, "Fluffy", 100)
 46
 47	defer func() {
 48		if r := recover(); r == nil {
 49			t.Fatal("expected panic hatching a second living pet")
 50		}
 51	}()
 52	hatch(alice, "Again", 101)
 53}
 54
 55func TestHatchAfterDeathKeepsLifetimeStats(t *testing.T) {
 56	resetState()
 57	alice := testutils.TestAddress("alice")
 58	hatch(alice, "Fluffy", 0)
 59	feed(alice, 0)
 60
 61	p, _ := get(alice)
 62	p.alive = false
 63	p.deaths = 1
 64
 65	hatch(alice, "Fluffy2", 10)
 66
 67	p, _ = get(alice)
 68	if p.feeds != 1 || p.deaths != 1 {
 69		t.Fatalf("expected lifetime stats to carry over, got feeds=%d deaths=%d", p.feeds, p.deaths)
 70	}
 71	if !p.alive {
 72		t.Fatal("expected the new pet to be alive")
 73	}
 74}
 75
 76func TestFeedReducesHunger(t *testing.T) {
 77	resetState()
 78	alice := testutils.TestAddress("alice")
 79	hatch(alice, "Fluffy", 100)
 80
 81	p, _ := get(alice)
 82	p.hunger = 60
 83
 84	feed(alice, 100)
 85	if p.hunger != 30 {
 86		t.Fatalf("expected hunger 30 after feed, got %d", p.hunger)
 87	}
 88	if p.feeds != 1 {
 89		t.Fatalf("expected feeds counter to increment, got %d", p.feeds)
 90	}
 91}
 92
 93func TestPlayRefusesWhenTooTired(t *testing.T) {
 94	resetState()
 95	alice := testutils.TestAddress("alice")
 96	hatch(alice, "Fluffy", 100)
 97
 98	p, _ := get(alice)
 99	p.energy = 5
100
101	out := play(alice, 100)
102	if !strings.Contains(out, "too tired") {
103		t.Fatalf("expected too-tired message, got %q", out)
104	}
105	if p.plays != 0 {
106		t.Fatal("play should not have counted while too tired")
107	}
108}
109
110func TestNeglectKillsPet(t *testing.T) {
111	resetState()
112	alice := testutils.TestAddress("alice")
113	hatch(alice, "Fluffy", 0)
114
115	defer func() {
116		if r := recover(); r == nil {
117			t.Fatal("expected panic acting on a neglected/dead pet")
118		}
119	}()
120	// Feed re-ticks against a much later height, starving the pet to death.
121	feed(alice, 1000)
122}
123
124func TestFeedWithoutPetPanics(t *testing.T) {
125	resetState()
126	bob := testutils.TestAddress("bob")
127
128	defer func() {
129		if r := recover(); r == nil {
130			t.Fatal("expected panic feeding without a pet")
131		}
132	}()
133	feed(bob, 100)
134}
135
136func TestRenderShowsEmptyThenPet(t *testing.T) {
137	resetState()
138	if !strings.Contains(Render(""), "No pets hatched yet") {
139		t.Fatal("expected empty-state placeholder")
140	}
141
142	alice := testutils.TestAddress("alice")
143	hatch(alice, "Fluffy", 100)
144
145	out := Render("")
146	if !strings.Contains(out, "Fluffy") {
147		t.Fatal("expected pet name in leaderboard render")
148	}
149
150	detail := Render(alice.String())
151	if !strings.Contains(detail, "Hunger") {
152		t.Fatal("expected detail card to show hunger stat")
153	}
154}
155
156// TestHatchViaCrossingCall smoke-tests the exported, realm-crossing entry
157// point end to end (as an on-chain caller would invoke it).
158func TestHatchViaCrossingCall(cur realm, t *testing.T) {
159	resetState()
160	alice := testutils.TestAddress("alice")
161	testing.SetRealm(testing.NewUserRealm(alice))
162
163	msg := Hatch(cross(cur), "Fluffy")
164	if !strings.Contains(msg, "hatched") {
165		t.Fatalf("expected hatch confirmation, got %q", msg)
166	}
167	if _, ok := get(alice); !ok {
168		t.Fatal("expected pet to be persisted for caller")
169	}
170}
171
172// decayOver ticks one pet from full stats across `blocks` blocks, calling tick
173// every `every` blocks, and returns what it is left with. It drives tick
174// directly rather than through Feed/Play/Sleep, because those also restore
175// stats and the question here is only what decay does.
176func decayOver(blocks, every int) (hunger, happiness, energy int) {
177	p := &pet{alive: true, hunger: 0, happiness: 100, energy: 100, lastUpdate: 0}
178	for h := every; h <= blocks; h += every {
179		p.tick(int64(h))
180	}
181	return p.hunger, p.happiness, p.energy
182}
183
184// TestDecayDoesNotDependOnCallFrequency is the bug this file exists for.
185//
186// happiness decays once per 2 blocks and energy once per 3, by integer
187// division. Before the carried debt, that division truncated on EVERY call, so
188// ticking once per block cost 1/2 = 0 happiness and 1/3 = 0 energy, forever: a
189// pet fed in a `while true` loop was immortal while everyone else's died. The
190// leaderboard rewarded a bot, which is the opposite of what the realm claims.
191//
192// Over the same 60 blocks the outcome must now be the same whether the owner
193// acts every block or once at the end.
194func TestDecayDoesNotDependOnCallFrequency(t *testing.T) {
195	const blocks = 60
196	wantHunger, wantHappy, wantEnergy := decayOver(blocks, blocks)
197
198	for _, every := range []int{1, 2, 3, 5, 30} {
199		h, hap, e := decayOver(blocks, every)
200		if h != wantHunger || hap != wantHappy || e != wantEnergy {
201			t.Errorf("acting every %d block(s) over %d blocks: hunger=%d happiness=%d energy=%d, "+
202				"want %d/%d/%d as when acting once",
203				every, blocks, h, hap, e, wantHunger, wantHappy, wantEnergy)
204		}
205	}
206}
207
208// The absolute numbers, so a future change to the rates is a deliberate edit
209// and not a silent one: 60 blocks costs 60 hunger, 30 happiness, 20 energy.
210func TestDecayRates(t *testing.T) {
211	h, hap, e := decayOver(60, 1)
212	if h != 60 || hap != 70 || e != 80 {
213		t.Errorf("60 blocks of decay = hunger %d, happiness %d, energy %d; want 60, 70, 80", h, hap, e)
214	}
215}
216
217// project draws the page; tick decides the outcome. If they disagree, the pet a
218// reader sees is not the pet the next call will act on.
219func TestProjectAgreesWithTick(t *testing.T) {
220	for _, every := range []int{1, 2, 3, 7} {
221		p := &pet{alive: true, hunger: 0, happiness: 100, energy: 100, lastUpdate: 0}
222		for h := every; h <= 42; h += every {
223			p.tick(int64(h))
224		}
225		// One more step, projected rather than applied.
226		ph, phap, pe, _ := project(p, int64(42+every))
227		p.tick(int64(42 + every))
228		if ph != p.hunger || phap != p.happiness || pe != p.energy {
229			t.Errorf("every %d: project says %d/%d/%d, tick says %d/%d/%d",
230				every, ph, phap, pe, p.hunger, p.happiness, p.energy)
231		}
232	}
233}