game_daily_test.gno
11.98 Kb · 429 lines
1package game31
2
3import (
4 "strconv"
5 "testing"
6 "time"
7)
8
9// ── DailyCheckin helpers ──────────────────────────────────────────────────────
10
11// resetDailyCheckin removes any stored check-in state for the address.
12func resetDailyCheckin(addr string) {
13 dailyCheckins.Remove(addr)
14}
15
16// setDailyCheckinRaw directly writes a raw value into dailyCheckins.
17// Use this to seed "yesterday", "old format", etc. without going through
18// the DailyCheckin transaction path.
19func setDailyCheckinRaw(addr, raw string) {
20 dailyCheckins.Set(addr, raw)
21}
22
23// getDailyCheckinRaw reads the raw stored value (for assertions).
24func getDailyCheckinRaw(addr string) string {
25 if !dailyCheckins.Has(addr) {
26 return ""
27 }
28 return dailyCheckins.Get(addr).(string)
29}
30
31// ── parseDailyCheckin tests ───────────────────────────────────────────────────
32
33func TestParseDailyCheckinNewFormat(t *testing.T) {
34 lastDay, streak := parseDailyCheckin("20000:5")
35 if lastDay != 20000 {
36 t.Errorf("lastDay: got %d, want 20000", lastDay)
37 }
38 if streak != 5 {
39 t.Errorf("streak: got %d, want 5", streak)
40 }
41}
42
43func TestParseDailyCheckinLegacyFormat(t *testing.T) {
44 // Old format: raw unix seconds. day 19000 = 19000 * 86400 = 1641600000
45 seconds := int64(19000 * 86400)
46 raw := strconv.FormatInt(seconds, 10)
47 lastDay, streak := parseDailyCheckin(raw)
48 if lastDay != 19000 {
49 t.Errorf("lastDay: got %d, want 19000", lastDay)
50 }
51 if streak != 1 {
52 t.Errorf("legacy format must give streak=1, got %d", streak)
53 }
54}
55
56func TestParseDailyCheckinEmpty(t *testing.T) {
57 // Edge: value "0" (no prior check-in default)
58 lastDay, streak := parseDailyCheckin("0")
59 if lastDay != 0 {
60 t.Errorf("lastDay: got %d, want 0", lastDay)
61 }
62 if streak != 1 {
63 t.Errorf("streak: got %d, want 1", streak)
64 }
65}
66
67// ── DailyCheckin streak logic tests ──────────────────────────────────────────
68
69func TestDailyCheckinFirstCheckin(t *testing.T) {
70 addr := "g1dailytest001"
71 resetDailyCheckin(addr)
72
73 today := time.Now().Unix() / 86400
74
75 // Simulate first check-in: no prior state → DailyCheckin writes today:1
76 // We call the logic inline (mirrors BuyToolDirect approach) since DailyCheckin
77 // requires a cur realm that isn't available in unit tests.
78 // Precondition: nothing stored
79 if dailyCheckins.Has(addr) {
80 t.Fatal("precondition: must have no prior check-in")
81 }
82
83 // Perform the streak update logic directly (same as DailyCheckin body)
84 var lastDay, streak int64
85 // No entry → both stay 0
86 _ = lastDay
87 _ = streak
88 // today != 0 and 0 != today-1 → streak = 1
89 newStreak := int64(1)
90 val := strconv.FormatInt(today, 10) + ":" + strconv.FormatInt(newStreak, 10)
91 dailyCheckins.Set(addr, val)
92
93 // Verify
94 stored := getDailyCheckinRaw(addr)
95 gotDay, gotStreak := parseDailyCheckin(stored)
96 if gotDay != today {
97 t.Errorf("lastDay: got %d, want %d", gotDay, today)
98 }
99 if gotStreak != 1 {
100 t.Errorf("streak after first check-in: got %d, want 1", gotStreak)
101 }
102}
103
104func TestDailyCheckinConsecutiveDays(t *testing.T) {
105 addr := "g1dailytest002"
106 resetDailyCheckin(addr)
107
108 today := time.Now().Unix() / 86400
109 yesterday := today - 1
110
111 // Seed: player checked in yesterday with streak=3
112 setDailyCheckinRaw(addr, strconv.FormatInt(yesterday, 10)+":3")
113
114 // Now simulate today's check-in
115 raw := getDailyCheckinRaw(addr)
116 lastDay, streak := parseDailyCheckin(raw)
117
118 if lastDay == today {
119 t.Fatal("test setup error: lastDay should be yesterday")
120 }
121 var newStreak int64
122 if lastDay == today-1 {
123 newStreak = streak + 1 // should be 4
124 } else {
125 newStreak = 1
126 }
127
128 val := strconv.FormatInt(today, 10) + ":" + strconv.FormatInt(newStreak, 10)
129 dailyCheckins.Set(addr, val)
130
131 gotDay, gotStreak := parseDailyCheckin(getDailyCheckinRaw(addr))
132 if gotDay != today {
133 t.Errorf("lastDay: got %d, want %d", gotDay, today)
134 }
135 if gotStreak != 4 {
136 t.Errorf("streak after consecutive day: got %d, want 4", gotStreak)
137 }
138}
139
140func TestDailyCheckinSkippedDay(t *testing.T) {
141 addr := "g1dailytest003"
142 resetDailyCheckin(addr)
143
144 today := time.Now().Unix() / 86400
145 twoDaysAgo := today - 2
146
147 // Seed: player last checked in 2 days ago with a long streak
148 setDailyCheckinRaw(addr, strconv.FormatInt(twoDaysAgo, 10)+":42")
149
150 raw := getDailyCheckinRaw(addr)
151 lastDay, streak := parseDailyCheckin(raw)
152 _ = streak
153
154 var newStreak int64
155 if lastDay == today {
156 t.Fatal("same-day check detected unexpectedly")
157 } else if lastDay == today-1 {
158 newStreak = streak + 1
159 } else {
160 newStreak = 1 // streak resets
161 }
162
163 val := strconv.FormatInt(today, 10) + ":" + strconv.FormatInt(newStreak, 10)
164 dailyCheckins.Set(addr, val)
165
166 _, gotStreak := parseDailyCheckin(getDailyCheckinRaw(addr))
167 if gotStreak != 1 {
168 t.Errorf("streak after missed day: got %d, want 1", gotStreak)
169 }
170}
171
172func TestDailyCheckinSameDayPanics(t *testing.T) {
173 addr := "g1dailytest004"
174 resetDailyCheckin(addr)
175
176 today := time.Now().Unix() / 86400
177
178 // Seed: already checked in today
179 setDailyCheckinRaw(addr, strconv.FormatInt(today, 10)+":7")
180
181 // The DailyCheckin function should panic with "already checked in today"
182 // when lastDay == today. We replicate the same panic condition here.
183 mustPanic(t, "already checked in today", func() {
184 raw := getDailyCheckinRaw(addr)
185 lastDay, _ := parseDailyCheckin(raw)
186 if lastDay == today {
187 panic("already checked in today")
188 }
189 })
190}
191
192func TestDailyCheckinOldUnixSecondsFormat(t *testing.T) {
193 addr := "g1dailytest005"
194 resetDailyCheckin(addr)
195
196 today := time.Now().Unix() / 86400
197 yesterdaySeconds := (today - 1) * 86400
198
199 // Seed: old format — raw unix seconds for yesterday
200 setDailyCheckinRaw(addr, strconv.FormatInt(yesterdaySeconds, 10))
201
202 // parseDailyCheckin must interpret this as lastDay = yesterdaySeconds/86400 = today-1, streak=1
203 raw := getDailyCheckinRaw(addr)
204 lastDay, streak := parseDailyCheckin(raw)
205
206 if lastDay != today-1 {
207 t.Errorf("old format lastDay: got %d, want %d", lastDay, today-1)
208 }
209 if streak != 1 {
210 t.Errorf("old format streak: got %d, want 1", streak)
211 }
212
213 // Checking in today from old-format yesterday → streak becomes 2
214 var newStreak int64
215 if lastDay == today-1 {
216 newStreak = streak + 1
217 } else {
218 newStreak = 1
219 }
220 if newStreak != 2 {
221 t.Errorf("streak after old-format yesterday: got %d, want 2", newStreak)
222 }
223}
224
225// ── DailyCheckinStatus tests ──────────────────────────────────────────────────
226
227func TestDailyCheckinStatusNeverCheckedIn(t *testing.T) {
228 addr := "g1statustest001"
229 resetDailyCheckin(addr)
230
231 status := DailyCheckinStatus(addr)
232 if !strContains(status, `"ok":true`) {
233 t.Errorf("expected ok:true, got %s", status)
234 }
235 if !strContains(status, `"done":false`) {
236 t.Errorf("expected done:false for no check-in, got %s", status)
237 }
238 if !strContains(status, `"streak":0`) {
239 t.Errorf("expected streak:0 for no check-in, got %s", status)
240 }
241}
242
243func TestDailyCheckinStatusCheckedInToday(t *testing.T) {
244 addr := "g1statustest002"
245 resetDailyCheckin(addr)
246
247 today := time.Now().Unix() / 86400
248 // Simulate checked in today with streak=5
249 setDailyCheckinRaw(addr, strconv.FormatInt(today, 10)+":5")
250
251 status := DailyCheckinStatus(addr)
252 if !strContains(status, `"done":true`) {
253 t.Errorf("expected done:true when checked in today, got %s", status)
254 }
255 if !strContains(status, `"streak":5`) {
256 t.Errorf("expected streak:5, got %s", status)
257 }
258}
259
260func TestDailyCheckinStatusCheckedInYesterday(t *testing.T) {
261 addr := "g1statustest003"
262 resetDailyCheckin(addr)
263
264 today := time.Now().Unix() / 86400
265 yesterday := today - 1
266 // Checked in yesterday — streak is still live (not broken yet)
267 setDailyCheckinRaw(addr, strconv.FormatInt(yesterday, 10)+":12")
268
269 status := DailyCheckinStatus(addr)
270 if !strContains(status, `"done":false`) {
271 t.Errorf("expected done:false (not checked in today), got %s", status)
272 }
273 // Streak is still live (lastDay == today-1)
274 if !strContains(status, `"streak":12`) {
275 t.Errorf("expected streak:12 (live streak), got %s", status)
276 }
277}
278
279func TestDailyCheckinStatusStreakBrokenAfterMissedDay(t *testing.T) {
280 addr := "g1statustest004"
281 resetDailyCheckin(addr)
282
283 today := time.Now().Unix() / 86400
284 twoDaysAgo := today - 2
285 // Last check-in was 2 days ago — streak is broken
286 setDailyCheckinRaw(addr, strconv.FormatInt(twoDaysAgo, 10)+":30")
287
288 status := DailyCheckinStatus(addr)
289 if !strContains(status, `"done":false`) {
290 t.Errorf("expected done:false, got %s", status)
291 }
292 // Streak must report 0 when broken
293 if !strContains(status, `"streak":0`) {
294 t.Errorf("expected streak:0 after missed day, got %s", status)
295 }
296}
297
298func TestDailyCheckinStatusNextMidnight(t *testing.T) {
299 addr := "g1statustest005"
300 resetDailyCheckin(addr)
301
302 today := time.Now().Unix() / 86400
303 setDailyCheckinRaw(addr, strconv.FormatInt(today, 10)+":1")
304
305 status := DailyCheckinStatus(addr)
306 // next field must be (today+1)*86400
307 wantNext := (today + 1) * 86400
308 wantStr := `"next":` + strconv.FormatInt(wantNext, 10)
309 if !strContains(status, wantStr) {
310 t.Errorf("expected %s in status, got %s", wantStr, status)
311 }
312}
313
314func TestDailyCheckinStatusOldFormat(t *testing.T) {
315 addr := "g1statustest006"
316 resetDailyCheckin(addr)
317
318 today := time.Now().Unix() / 86400
319 // Seed yesterday in old unix-seconds format
320 yesterdaySeconds := (today - 1) * 86400
321 setDailyCheckinRaw(addr, strconv.FormatInt(yesterdaySeconds, 10))
322
323 status := DailyCheckinStatus(addr)
324 // Old format is yesterday → streak lives at 1, not yet done today
325 if !strContains(status, `"done":false`) {
326 t.Errorf("expected done:false (old format, yesterday), got %s", status)
327 }
328 if !strContains(status, `"streak":1`) {
329 t.Errorf("expected streak:1 (old format gives streak=1), got %s", status)
330 }
331}
332
333// ── helpers (from the game28 test suite) ──
334func mustPanic(t *testing.T, wantSubstr string, f func()) {
335 t.Helper()
336 var msg string
337 panicked := false
338 func() {
339 defer func() {
340 r := recover()
341 if r != nil {
342 panicked = true
343 switch v := r.(type) {
344 case string:
345 msg = v
346 case error:
347 msg = v.Error()
348 default:
349 msg = "non-string panic"
350 }
351 }
352 }()
353 f()
354 }()
355 if !panicked {
356 t.Errorf("expected panic containing %q, but no panic occurred", wantSubstr)
357 return
358 }
359 if wantSubstr != "" && !strContains(msg, wantSubstr) {
360 t.Errorf("panic message %q does not contain %q", msg, wantSubstr)
361 }
362}
363
364func strContains(s, sub string) bool {
365 if len(sub) == 0 {
366 return true
367 }
368 if len(sub) > len(s) {
369 return false
370 }
371 for i := 0; i <= len(s)-len(sub); i++ {
372 if s[i:i+len(sub)] == sub {
373 return true
374 }
375 }
376 return false
377}
378
379
380func TestRepairCostCeil30Pct(t *testing.T) {
381 type tc struct {
382 toolId string
383 coinId string
384 base uint64
385 expected uint64 // ceil(base × 30%) = (base×30 + 99) / 100
386 }
387 cases := []tc{
388 // wood: seedling:19 → ceil(5.7) = 6
389 {"wood", "seedling", 19, 6},
390 // wood: party:8 → ceil(2.4) = 3
391 {"wood", "party", 8, 3},
392 // iron: gear:42 → ceil(12.6) = 13
393 {"iron", "gear", 42, 13},
394 // iron: magnet:4 → ceil(1.2) = 2
395 {"iron", "magnet", 4, 2},
396 // quasar: lotus:3510 → ceil(1053.0) = 1053
397 {"quasar", "lotus", 3510, 1053},
398 // omega: lotus:26970 → ceil(8091.0) = 8091
399 {"omega", "lotus", 26970, 8091},
400 // stone: cactus:15 → ceil(4.5) = 5
401 {"stone", "cactus", 15, 5},
402 }
403 for _, c := range cases {
404 costs, hasCosts := toolCosts[c.toolId]
405 if !hasCosts {
406 t.Errorf("toolCosts missing for %s", c.toolId)
407 continue
408 }
409 found := false
410 for _, pair := range costs {
411 if pair[0] != c.coinId {
412 continue
413 }
414 found = true
415 base, _ := strconv.ParseUint(pair[1], 10, 64)
416 if base != c.base {
417 t.Errorf("%s[%s]: expected base=%d, got %d", c.toolId, c.coinId, c.base, base)
418 }
419 got := (base*30 + 99) / 100
420 if got != c.expected {
421 t.Errorf("%s[%s]: ceil(30%%)=%d expected, got %d", c.toolId, c.coinId, c.expected, got)
422 }
423 break
424 }
425 if !found {
426 t.Errorf("%s does not contain ingredient %s", c.toolId, c.coinId)
427 }
428 }
429}