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

connect4_test.gno

14.78 Kb · 434 lines
  1package connect4
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"strconv"
  7	"testing"
  8
  9	"gno.land/p/nt/avl/v0"
 10	"gno.land/p/nt/testutils/v0"
 11	"gno.land/p/nt/uassert/v0"
 12)
 13
 14const stake = int64(10_000_000)
 15
 16var (
 17	alice   = testutils.TestAddress("alice")
 18	bob     = testutils.TestAddress("bob")
 19	carol   = testutils.TestAddress("carol")
 20	admin   = address("g1cvr48r7l7lkmvp77cr6zg2zhu26jgfwr0y8pew")
 21	pkgAddr = chain.PackageAddress("gno.land/r/samcrew/connect4")
 22)
 23
 24func reset() {
 25	games, active, stats = avl.Tree{}, avl.Tree{}, avl.Tree{}
 26	nextID, fee, feesCollected = 0, 100_000, 0
 27}
 28
 29// pay sets the ugnot sent with the next call and credits the realm with it,
 30// mirroring what the chain does. testing.SetRealm must be called in the same
 31// frame that makes the cross call, so tests call it inline rather than here.
 32func pay(amount int64) {
 33	if amount == 0 {
 34		testing.SetOriginSend(nil)
 35		return
 36	}
 37	coins := chain.Coins{{"ugnot", amount}}
 38	testing.SetOriginSend(coins)
 39	testing.IssueCoins(pkgAddr, coins)
 40}
 41
 42func balance(addr address) int64 {
 43	return banker.NewReadonlyBanker().GetCoin(addr, "ugnot")
 44}
 45
 46// phrase returns a passphrase unique to the next game id.
 47func phrase() string { return "four random words " + strconv.Itoa(nextID+1) }
 48
 49// commit returns the commitment for phrase().
 50func commit() string { return sha256Hex(phrase()) }
 51
 52// accepted returns a game between alice (creator) and bob awaiting Reveal,
 53// and its passphrase.
 54func accepted(cur realm) (*Game, string) {
 55	p := phrase()
 56	testing.SetRealm(testing.NewUserRealm(alice))
 57	pay(stake)
 58	id := Offer(cross(cur), "", 10, sha256Hex(p))
 59	testing.SetRealm(testing.NewUserRealm(bob))
 60	pay(stake)
 61	Accept(cross(cur), id)
 62	return mustGet(id), p
 63}
 64
 65// started returns a revealed Playing game between alice (creator) and bob.
 66func started(cur realm) *Game {
 67	g, p := accepted(cur)
 68	testing.SetRealm(testing.NewUserRealm(alice))
 69	pay(0)
 70	Reveal(cross(cur), g.ID, p)
 71	return g
 72}
 73
 74// move plays column for whoever is on turn.
 75func move(cur realm, g *Game, column int) {
 76	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
 77	pay(0)
 78	Play(cross(cur), g.ID, column)
 79}
 80
 81// expireClock advances block time past the 90s move clock (SkipHeights is 5s/block).
 82func expireClock() { testing.SkipHeights(19) }
 83
 84func TestWinPaysPotMinusFee(cur realm, t *testing.T) {
 85	reset()
 86	g := started(cur)
 87	first := g.player(g.Turn)
 88	before := balance(first)
 89	for _, c := range []int{1, 2, 1, 2, 1, 2, 1} { // first mover stacks column 1
 90		move(cur, g, c)
 91	}
 92	uassert.Equal(t, "won", g.Status.String())
 93	uassert.Equal(t, first, g.Winner)
 94	uassert.Equal(t, 2*stake-100_000, balance(first)-before)
 95	uassert.Equal(t, int64(100_000), feesCollected)
 96	uassert.Equal(t, 1, statsOf(first).Wins)
 97	uassert.Equal(t, 2*stake-100_000, statsOf(first).GnotWon)
 98	uassert.Equal(t, 1, statsOf(g.player(3-g.Turn)).Games)
 99	uassert.Equal(t, 0, active.Size())
100}
101
102func TestOfferRejects(cur realm, t *testing.T) {
103	reset()
104	testing.SetRealm(testing.NewUserRealm(alice))
105	pay(minStake - 1)
106	uassert.AbortsContains(t, cur, "stake below minimum", func() { Offer(cross(cur), "", 10, commit()) })
107	testing.SetRealm(testing.NewUserRealm(alice))
108	pay(stake)
109	uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 0, commit()) })
110	testing.SetRealm(testing.NewUserRealm(alice))
111	pay(stake)
112	uassert.AbortsContains(t, cur, "validFor", func() { Offer(cross(cur), "", 61, commit()) })
113	testing.SetRealm(testing.NewUserRealm(alice))
114	pay(stake)
115	uassert.AbortsContains(t, cur, "yourself", func() { Offer(cross(cur), alice, 10, commit()) })
116	testing.SetRealm(testing.NewUserRealm(alice))
117	pay(0)
118	uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit()) })
119	testing.SetRealm(testing.NewUserRealm(alice))
120	testing.SetOriginSend(chain.Coins{{"ugnot", stake}, {"foo", 1}})
121	uassert.AbortsContains(t, cur, "send exactly one", func() { Offer(cross(cur), "", 10, commit()) })
122}
123
124func TestAcceptRejects(cur realm, t *testing.T) {
125	reset()
126	testing.SetRealm(testing.NewUserRealm(alice))
127	pay(stake)
128	id := Offer(cross(cur), bob, 1, commit())
129	testing.SetRealm(testing.NewUserRealm(alice))
130	pay(stake)
131	uassert.AbortsContains(t, cur, "own offer", func() { Accept(cross(cur), id) })
132	testing.SetRealm(testing.NewUserRealm(carol))
133	pay(stake)
134	uassert.AbortsContains(t, cur, "reserved", func() { Accept(cross(cur), id) })
135	testing.SetRealm(testing.NewUserRealm(bob))
136	pay(stake - 1)
137	uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id) })
138	testing.SetRealm(testing.NewUserRealm(bob))
139	pay(stake + 1)
140	uassert.AbortsContains(t, cur, "must send exactly", func() { Accept(cross(cur), id) })
141	testing.SkipHeights(13) // 65s > 1 minute
142	testing.SetRealm(testing.NewUserRealm(bob))
143	pay(stake)
144	uassert.AbortsContains(t, cur, "expired", func() { Accept(cross(cur), id) })
145	testing.SetRealm(testing.NewUserRealm(bob))
146	pay(stake)
147	uassert.AbortsContains(t, cur, "not found", func() { Accept(cross(cur), 99) })
148}
149
150func TestPlayRejects(cur realm, t *testing.T) {
151	reset()
152	g := started(cur)
153	testing.SetRealm(testing.NewUserRealm(g.player(3 - g.Turn)))
154	pay(0)
155	uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1) })
156	testing.SetRealm(testing.NewUserRealm(carol))
157	pay(0)
158	uassert.AbortsContains(t, cur, "not your turn", func() { Play(cross(cur), g.ID, 1) })
159	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
160	pay(0)
161	uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 0) })
162	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
163	pay(0)
164	uassert.AbortsContains(t, cur, "column must be", func() { Play(cross(cur), g.ID, 8) })
165	expireClock()
166	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
167	pay(0)
168	uassert.AbortsContains(t, cur, "clock ran out", func() { Play(cross(cur), g.ID, 1) })
169}
170
171func TestFullColumnRejected(cur realm, t *testing.T) {
172	reset()
173	g := started(cur)
174	for i := 0; i < rows; i++ {
175		move(cur, g, 4) // alternating players never make 4 in a column
176	}
177	testing.SetRealm(testing.NewUserRealm(g.player(g.Turn)))
178	pay(0)
179	uassert.AbortsContains(t, cur, "column is full", func() { Play(cross(cur), g.ID, 4) })
180}
181
182// The creator revealed, then the first mover never moved.
183func TestTimeoutBeforeFirstMoveIsVoid(cur realm, t *testing.T) {
184	reset()
185	g := started(cur)
186	testing.SetRealm(testing.NewUserRealm(carol))
187	pay(0)
188	uassert.AbortsContains(t, cur, "has not run out", func() { ClaimTimeout(cross(cur), g.ID) })
189	a, b := balance(alice), balance(bob)
190	expireClock()
191	testing.SetRealm(testing.NewUserRealm(carol))
192	pay(0) // anyone may settle
193	ClaimTimeout(cross(cur), g.ID)
194	uassert.Equal(t, "void", g.Status.String())
195	uassert.Equal(t, stake, balance(alice)-a)
196	uassert.Equal(t, stake, balance(bob)-b)
197	uassert.Equal(t, int64(0), feesCollected)
198	uassert.Equal(t, 0, stats.Size())
199}
200
201func TestTimeoutAfterMoveForfeits(cur realm, t *testing.T) {
202	reset()
203	g := started(cur)
204	mover := g.player(g.Turn)
205	loser := g.player(3 - g.Turn)
206	move(cur, g, 1)
207	expireClock()
208	before := balance(mover)
209	testing.SetRealm(testing.NewUserRealm(mover))
210	pay(0)
211	ClaimTimeout(cross(cur), g.ID)
212	uassert.Equal(t, "won", g.Status.String())
213	uassert.Equal(t, mover, g.Winner)
214	uassert.Equal(t, 2*stake-100_000, balance(mover)-before)
215	uassert.Equal(t, int64(100_000), feesCollected)
216	uassert.Equal(t, 1, statsOf(loser).Games)
217}
218
219func TestResign(cur realm, t *testing.T) {
220	reset()
221	g := started(cur)
222	testing.SetRealm(testing.NewUserRealm(carol))
223	pay(0)
224	uassert.AbortsContains(t, cur, "not a player", func() { Resign(cross(cur), g.ID) })
225	before := balance(bob)
226	testing.SetRealm(testing.NewUserRealm(alice))
227	pay(0)
228	Resign(cross(cur), g.ID)
229	uassert.Equal(t, "won", g.Status.String())
230	uassert.Equal(t, bob, g.Winner)
231	uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
232	uassert.Equal(t, int64(100_000), feesCollected)
233	uassert.Equal(t, 1, statsOf(alice).Games)
234}
235
236func TestCancel(cur realm, t *testing.T) {
237	reset()
238	testing.SetRealm(testing.NewUserRealm(alice))
239	pay(stake)
240	id := Offer(cross(cur), "", 1, commit())
241	testing.SetRealm(testing.NewUserRealm(bob))
242	pay(0)
243	uassert.AbortsContains(t, cur, "only the creator", func() { Cancel(cross(cur), id) })
244	before := balance(alice)
245	testing.SetRealm(testing.NewUserRealm(alice))
246	pay(0)
247	Cancel(cross(cur), id) // creator, before expiry
248	uassert.Equal(t, "cancelled", mustGet(id).Status.String())
249	uassert.Equal(t, stake, balance(alice)-before)
250
251	// Once expired, anyone may cancel; the refund still goes to the creator.
252	testing.SetRealm(testing.NewUserRealm(alice))
253	pay(stake)
254	id = Offer(cross(cur), "", 1, commit())
255	testing.SkipHeights(13) // 65s > 1 minute
256	before = balance(alice)
257	testing.SetRealm(testing.NewUserRealm(carol))
258	pay(0)
259	Cancel(cross(cur), id)
260	uassert.Equal(t, "cancelled", mustGet(id).Status.String())
261	uassert.Equal(t, stake, balance(alice)-before)
262
263	g := started(cur)
264	testing.SetRealm(testing.NewUserRealm(alice))
265	pay(0)
266	uassert.AbortsContains(t, cur, "not open", func() { Cancel(cross(cur), g.ID) })
267}
268
269func TestDrawRefundsBoth(cur realm, t *testing.T) {
270	reset()
271	g := started(cur)
272	// Fill every cell but the top of column 7 with a pattern that has no
273	// four in a row; player 1 owns the last cell.
274	for c := 0; c < cols; c++ {
275		for r := 0; r < rows; r++ {
276			g.Board[c][r] = byte(1 + (c/2+r)%2)
277		}
278	}
279	g.Board[6][5] = 0
280	g.Moves, g.Turn = cols*rows-1, 1
281	a, b := balance(alice), balance(bob)
282	move(cur, g, 7)
283	uassert.Equal(t, "draw", g.Status.String())
284	uassert.Equal(t, stake, balance(alice)-a)
285	uassert.Equal(t, stake, balance(bob)-b)
286	uassert.Equal(t, 1, statsOf(alice).Draws)
287}
288
289func TestFeeSnapshotAndWithdraw(cur realm, t *testing.T) {
290	reset()
291	testing.SetRealm(testing.NewUserRealm(carol))
292	pay(0)
293	uassert.AbortsContains(t, cur, "not owner", func() { SetFee(cross(cur), 0) })
294	testing.SetRealm(testing.NewUserRealm(carol))
295	pay(0)
296	uassert.AbortsContains(t, cur, "not owner", func() { WithdrawFees(cross(cur), carol) })
297	g := started(cur) // offered at the default 0.1 GNOT fee
298	testing.SetRealm(testing.NewUserRealm(admin))
299	pay(0)
300	uassert.AbortsContains(t, cur, "negative", func() { SetFee(cross(cur), -1) })
301	SetFee(cross(cur), 5_000_000)
302	testing.SetRealm(testing.NewUserRealm(alice))
303	pay(0)
304	Resign(cross(cur), g.ID)
305	uassert.Equal(t, int64(100_000), feesCollected)
306
307	before := balance(carol)
308	testing.SetRealm(testing.NewUserRealm(admin))
309	pay(0)
310	WithdrawFees(cross(cur), carol)
311	uassert.Equal(t, int64(100_000), balance(carol)-before)
312	uassert.Equal(t, int64(0), feesCollected)
313
314	// A raised fee applies to new offers, which must stake more than it.
315	testing.SetRealm(testing.NewUserRealm(alice))
316	pay(5_000_000)
317	uassert.AbortsContains(t, cur, "exceed the house fee", func() { Offer(cross(cur), "", 10, commit()) })
318}
319
320func TestFinishedGameRejects(cur realm, t *testing.T) {
321	reset()
322	g := started(cur)
323	testing.SetRealm(testing.NewUserRealm(alice))
324	pay(0)
325	Resign(cross(cur), g.ID)
326	testing.SetRealm(testing.NewUserRealm(bob))
327	pay(0)
328	uassert.AbortsContains(t, cur, "not in progress", func() { Play(cross(cur), g.ID, 1) })
329	uassert.AbortsContains(t, cur, "not in progress", func() { Resign(cross(cur), g.ID) })
330	expireClock()
331	testing.SetRealm(testing.NewUserRealm(bob))
332	pay(0)
333	uassert.AbortsContains(t, cur, "not in progress", func() { ClaimTimeout(cross(cur), g.ID) })
334	testing.SetRealm(testing.NewUserRealm(carol))
335	pay(stake)
336	uassert.AbortsContains(t, cur, "not open", func() { Accept(cross(cur), g.ID) })
337	testing.SetRealm(testing.NewUserRealm(carol))
338	pay(stake)
339	uassert.AbortsContains(t, cur, "invalid opponent", func() { Offer(cross(cur), "notanaddress", 10, commit()) })
340}
341
342func TestRevealPhase(cur realm, t *testing.T) {
343	reset()
344	g, p := accepted(cur)
345	uassert.Equal(t, byte(0), g.Turn)
346	for _, who := range []address{alice, bob} {
347		testing.SetRealm(testing.NewUserRealm(who))
348		pay(0)
349		uassert.AbortsContains(t, cur, "waiting for the creator", func() { Play(cross(cur), g.ID, 1) })
350	}
351	testing.SetRealm(testing.NewUserRealm(bob))
352	pay(0)
353	uassert.AbortsContains(t, cur, "only the creator", func() { Reveal(cross(cur), g.ID, p) })
354	testing.SetRealm(testing.NewUserRealm(alice))
355	pay(0)
356	uassert.AbortsContains(t, cur, "does not match", func() { Reveal(cross(cur), g.ID, p+"x") })
357	testing.SetRealm(testing.NewUserRealm(alice))
358	pay(0)
359	Reveal(cross(cur), g.ID, p)
360	uassert.True(t, g.Turn == 1 || g.Turn == 2)
361	testing.SetRealm(testing.NewUserRealm(alice))
362	pay(0)
363	uassert.AbortsContains(t, cur, "not awaiting a reveal", func() { Reveal(cross(cur), g.ID, p) })
364	move(cur, g, 1) // the picked first mover can play
365	uassert.Equal(t, 1, g.Moves)
366}
367
368func TestLateRevealForfeits(cur realm, t *testing.T) {
369	reset()
370	g, p := accepted(cur)
371	expireClock()
372	testing.SetRealm(testing.NewUserRealm(alice))
373	pay(0)
374	uassert.AbortsContains(t, cur, "reveal clock ran out", func() { Reveal(cross(cur), g.ID, p) })
375	before := balance(bob)
376	testing.SetRealm(testing.NewUserRealm(carol))
377	pay(0)
378	ClaimTimeout(cross(cur), g.ID)
379	uassert.Equal(t, "won", g.Status.String())
380	uassert.Equal(t, bob, g.Winner)
381	uassert.Equal(t, 2*stake-100_000, balance(bob)-before)
382	uassert.Equal(t, int64(100_000), feesCollected)
383	uassert.Equal(t, 1, statsOf(bob).Wins)
384	uassert.Equal(t, 1, statsOf(alice).Games)
385}
386
387func TestCommitmentRejects(cur realm, t *testing.T) {
388	reset()
389	good := commit()
390	for _, bad := range []string{"", good[:63], good + "0", "G" + good[1:], "A" + good[1:]} {
391		testing.SetRealm(testing.NewUserRealm(alice))
392		pay(stake)
393		uassert.AbortsContains(t, cur, "64 lowercase hex", func() { Offer(cross(cur), "", 10, bad) })
394	}
395	testing.SetRealm(testing.NewUserRealm(alice))
396	pay(stake)
397	Offer(cross(cur), "", 10, good)
398	testing.SetRealm(testing.NewUserRealm(carol))
399	pay(stake)
400	Offer(cross(cur), "", 10, good) // the same commitment may be offered again
401}
402
403func TestPaymentsRequireUserCall(cur realm, t *testing.T) {
404	reset()
405	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
406	pay(stake)
407	uassert.AbortsContains(t, cur, "only user-call", func() { Offer(cross(cur), "", 10, commit()) })
408	testing.SetRealm(testing.NewUserRealm(alice))
409	pay(stake)
410	id := Offer(cross(cur), "", 10, commit())
411	testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/somerealm"))
412	pay(stake)
413	uassert.AbortsContains(t, cur, "only user-call", func() { Accept(cross(cur), id) })
414}
415
416func TestNonPayableRejectsCoins(cur realm, t *testing.T) {
417	reset()
418	g, p := accepted(cur)
419	calls := map[string]func(){
420		"Reveal":            func() { Reveal(cross(cur), g.ID, p) },
421		"Play":              func() { Play(cross(cur), g.ID, 1) },
422		"ClaimTimeout":      func() { ClaimTimeout(cross(cur), g.ID) },
423		"Resign":            func() { Resign(cross(cur), g.ID) },
424		"Cancel":            func() { Cancel(cross(cur), g.ID) },
425		"SetFee":            func() { SetFee(cross(cur), 0) },
426		"WithdrawFees":      func() { WithdrawFees(cross(cur), admin) },
427		"TransferOwnership": func() { TransferOwnership(cross(cur), carol) },
428	}
429	for name, call := range calls {
430		testing.SetRealm(testing.NewUserRealm(admin))
431		pay(1)
432		uassert.AbortsContains(t, cur, "does not accept coins", call, name)
433	}
434}