idle_test.gno
9.25 Kb · 286 lines
1package idle
2
3import (
4 "strings"
5 "testing"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10)
11
12// reset clears the realm between tests. Package state carries over from one
13// test to the next even though the block height does not, so every test that
14// looks at the board has to start from a known tree.
15func reset() { mines = avl.Tree{} }
16
17// t0 is an arbitrary unix second to hang the tests off. Nothing asserts an
18// absolute time, only differences from this one.
19//
20// It is only legal for the non-crossing helpers, which take the time as an
21// argument. Anything reaching now() (Render and the read helpers) reads the
22// BLOCK timestamp, which in a test is far below t0, so a mine anchored here
23// would make the chain's clock look like it ran backwards. Those tests anchor
24// at now() instead.
25const t0 = int64(1700000000)
26
27func TestStartOpensALevelOneMine(t *testing.T) {
28 reset()
29 alice := testutils.TestAddress("alice")
30
31 out := start(alice, t0)
32 uassert.True(t, strings.Contains(out, "level 1"), "expected the opening message")
33
34 m, ok := get(alice)
35 uassert.True(t, ok, "expected a mine")
36 uassert.Equal(t, int64(1), m.level)
37 uassert.Equal(t, int64(0), m.prestige)
38 uassert.Equal(t, t0, m.anchor)
39 uassert.Equal(t, int64(0), m.pending)
40}
41
42func TestStartTwicePanics(cur realm, t *testing.T) {
43 reset()
44 alice := testutils.TestAddress("alice")
45 start(alice, t0)
46 uassert.PanicsContains(t, cur, "already have a mine", func() { start(alice, t0) })
47}
48
49func TestActingWithoutAMinePanics(cur realm, t *testing.T) {
50 reset()
51 bob := testutils.TestAddress("bob")
52 uassert.PanicsContains(t, cur, "no mine yet", func() { claim(bob, t0) })
53 uassert.PanicsContains(t, cur, "no mine yet", func() { upgrade(bob, t0) })
54 uassert.PanicsContains(t, cur, "no mine yet", func() { prestige(bob, t0) })
55}
56
57func TestAccrualAndClaim(t *testing.T) {
58 reset()
59 alice := testutils.TestAddress("alice")
60 start(alice, t0)
61
62 m, _ := get(alice)
63 uassert.Equal(t, int64(0), m.view(t0+Period-1), "nothing before the first whole period")
64 uassert.Equal(t, int64(1), m.view(t0+Period), "one ore after one period")
65 uassert.Equal(t, int64(30), m.view(t0+30*Period))
66
67 out := claim(alice, t0+30*Period)
68 uassert.True(t, strings.Contains(out, "Claimed 30 ore"), "got: "+out)
69 uassert.Equal(t, int64(30), m.banked)
70 uassert.Equal(t, int64(30), m.lifetime)
71 uassert.Equal(t, int64(0), m.pending)
72 uassert.Equal(t, int64(1), m.claims)
73}
74
75func TestClaimingNothingPanics(cur realm, t *testing.T) {
76 reset()
77 alice := testutils.TestAddress("alice")
78 start(alice, t0)
79 uassert.PanicsContains(t, cur, "nothing to claim", func() { claim(alice, t0+Period-1) })
80}
81
82// TestClaimCadenceDoesNotMatter is the game-level statement of the invariant
83// the accrual package exists for: two players who dig for the same span end up
84// with the same ore whatever their claiming habits. A realm that re-anchored
85// to now would pay the busy player less, and a realm that decayed would pay
86// them more.
87//
88// "Whatever their habits" has exactly one limit, and it is the design rather
89// than the arithmetic: a cadence longer than the mine takes to fill spills,
90// and is meant to. Every cadence here stays under that, and
91// TestTheMineSpills covers the other side.
92func TestClaimCadenceDoesNotMatter(t *testing.T) {
93 const span = 600 * Period
94 fill := CapPeriods * Period
95
96 for _, cadence := range []int64{Period, 2*Period + 1, 7 * Period, 599, 6001, fill} {
97 if cadence > fill {
98 t.Fatalf("cadence %d would spill, which is not what this test is about", cadence)
99 }
100 reset()
101 alice := testutils.TestAddress("alice")
102 start(alice, t0)
103 m, _ := get(alice)
104
105 for at := t0 + cadence; at <= t0+span; at += cadence {
106 if m.view(at) > 0 {
107 claim(alice, at)
108 }
109 }
110 if m.view(t0+span) > 0 {
111 claim(alice, t0+span)
112 }
113 uassert.Equal(t, int64(600), m.lifetime, "cadence changed the payout")
114 }
115}
116
117// TestTheMineSpills pins the ceiling: production past the cap is lost, and
118// coming back late is what costs you, which is decision 1 of the design.
119func TestTheMineSpills(t *testing.T) {
120 reset()
121 alice := testutils.TestAddress("alice")
122 start(alice, t0)
123 m, _ := get(alice)
124
125 uassert.Equal(t, CapPeriods, m.view(t0+CapPeriods*Period), "full exactly at the cap")
126 uassert.Equal(t, CapPeriods, m.view(t0+10*CapPeriods*Period), "and no more, ever")
127
128 claim(alice, t0+10*CapPeriods*Period)
129 uassert.Equal(t, CapPeriods, m.lifetime, "ten cap-fulls of absence paid one")
130}
131
132func TestFullAtIsTheOnlyDeadline(t *testing.T) {
133 reset()
134 alice := testutils.TestAddress("alice")
135 start(alice, t0)
136 m, _ := get(alice)
137
138 uassert.Equal(t, t0+CapPeriods*Period, m.fullAt(t0), "an empty mine fills in CapPeriods")
139
140 half := t0 + (CapPeriods/2)*Period
141 uassert.Equal(t, half+(CapPeriods/2)*Period, m.fullAt(half), "half full, half the time left")
142
143 late := t0 + 99*CapPeriods*Period
144 uassert.Equal(t, late, m.fullAt(late), "already full reads as now")
145}
146
147func TestUpgrade(t *testing.T) {
148 reset()
149 alice := testutils.TestAddress("alice")
150 start(alice, t0)
151 m, _ := get(alice)
152
153 uassert.Equal(t, UpgradeBase, upgradeCost(1))
154 uassert.Equal(t, 4*UpgradeBase, upgradeCost(2))
155
156 claim(alice, t0+UpgradeBase*Period)
157 out := upgrade(alice, t0+UpgradeBase*Period)
158 uassert.True(t, strings.Contains(out, "Level 2"), "got: "+out)
159 uassert.Equal(t, int64(2), m.level)
160 uassert.Equal(t, int64(0), m.banked, "the upgrade spent the bank")
161
162 // The rate and the ceiling both doubled, so the fill time did not move.
163 uassert.Equal(t, int64(2), rate(m.level, m.prestige).Amount)
164 uassert.Equal(t, 2*CapPeriods, rate(m.level, m.prestige).Cap)
165}
166
167func TestUpgradeTooPoorPanics(cur realm, t *testing.T) {
168 reset()
169 alice := testutils.TestAddress("alice")
170 start(alice, t0)
171 uassert.PanicsContains(t, cur, "upgrade costs 10 ore, you have 0",
172 func() { upgrade(alice, t0) })
173}
174
175func TestPrestigeIsLockedUntilLevelTen(cur realm, t *testing.T) {
176 reset()
177 alice := testutils.TestAddress("alice")
178 start(alice, t0)
179 uassert.PanicsContains(t, cur, "prestige unlocks at level 10",
180 func() { prestige(alice, t0) })
181}
182
183func TestPrestigeResetsButKeepsLifetime(t *testing.T) {
184 reset()
185 alice := testutils.TestAddress("alice")
186 start(alice, t0)
187 m, _ := get(alice)
188
189 // Hand the mine to level 10 rather than grinding it, then bank something
190 // so the reset has ore to burn.
191 m.level = PrestigeAt
192 claim(alice, t0+50*Period)
193 banked, lifetime := m.banked, m.lifetime
194 uassert.True(t, banked > 0, "expected ore to burn")
195
196 out := prestige(alice, t0+50*Period)
197 uassert.True(t, strings.Contains(out, "Prestige 1"), "got: "+out)
198 uassert.Equal(t, int64(1), m.prestige)
199 uassert.Equal(t, int64(1), m.level)
200 uassert.Equal(t, int64(0), m.banked, "prestige burns the bank")
201 uassert.Equal(t, int64(0), m.pending)
202 uassert.Equal(t, lifetime, m.lifetime, "lifetime survives the reset")
203
204 // A level-1 prestige-1 mine digs what a level-2 mine does.
205 uassert.Equal(t, int64(2), rate(m.level, m.prestige).Amount)
206}
207
208func TestReadHelpersAreSafeOnAStranger(t *testing.T) {
209 reset()
210 carol := testutils.TestAddress("carol")
211 uassert.Equal(t, int64(0), Pending(carol))
212 uassert.Equal(t, int64(0), Banked(carol))
213 uassert.Equal(t, int64(0), FullAt(carol))
214 lvl, pre := Level(carol)
215 uassert.Equal(t, int64(0), lvl)
216 uassert.Equal(t, int64(0), pre)
217}
218
219func TestSpan(t *testing.T) {
220 cases := []struct {
221 secs int64
222 want string
223 }{
224 {0, "0s"},
225 {-5, "0s"},
226 {1, "1s"},
227 {59, "59s"},
228 {60, "1m"},
229 {61, "1m 1s"},
230 {3600, "1h"},
231 {3661, "1h 1m"},
232 {86400, "1d"},
233 {90061, "1d 1h"},
234 {7200, "2h"},
235 }
236 for _, tc := range cases {
237 uassert.Equal(t, tc.want, span(tc.secs), "span("+tc.want+")")
238 }
239}
240
241func TestRenderBoard(t *testing.T) {
242 reset()
243 uassert.True(t, strings.Contains(board(t0), "Nobody is digging yet"),
244 "expected the empty placeholder")
245
246 alice := testutils.TestAddress("alice")
247 start(alice, t0)
248 out := board(t0 + 30*Period)
249 uassert.True(t, strings.Contains(out, "The Mine"), "expected the title")
250 uassert.True(t, strings.Contains(out, "30"), "expected the live pending figure")
251 uassert.True(t, strings.Contains(out, "to spill"), "expected the spill countdown")
252}
253
254func TestRenderCard(t *testing.T) {
255 reset()
256 alice := testutils.TestAddress("alice")
257 start(alice, t0)
258
259 out := card(alice, t0+30*Period)
260 uassert.True(t, strings.Contains(out, "Level **1**"), "got: "+out)
261 uassert.True(t, strings.Contains(out, "30 / 120"), "expected stock over cap")
262 uassert.True(t, strings.Contains(out, "1h 30m"), "expected the spill countdown")
263 uassert.True(t, strings.Contains(out, "10 short"), "expected the shortfall")
264 uassert.True(t, strings.Contains(out, "9 to go"), "expected the prestige distance")
265
266 full := card(alice, t0+999*Period)
267 uassert.True(t, strings.Contains(full, "already full"), "expected the waste warning")
268
269 bob := testutils.TestAddress("bob")
270 uassert.True(t, strings.Contains(card(bob, t0), "No mine at"), "expected the stranger card")
271}
272
273// TestRenderIsLive is the property the whole design is for: the page changes
274// with nothing but the passage of time, so gnoweb serves a live dashboard and
275// no transaction was needed to move it.
276func TestRenderIsLive(t *testing.T) {
277 reset()
278 alice := testutils.TestAddress("alice")
279 start(alice, t0)
280
281 early := board(t0 + 10*Period)
282 later := board(t0 + 20*Period)
283 uassert.NotEqual(t, early, later, "the board did not move with time")
284 uassert.Equal(t, int64(0), Pending(testutils.TestAddress("nobody")),
285 "and reading it wrote nothing")
286}