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.

minichess_test.gno

6.98 Kb · 249 lines
  1package minichess
  2
  3import "testing"
  4
  5func perft(p Position, depth int) int {
  6	if depth == 0 {
  7		return 1
  8	}
  9	if depth == 1 {
 10		return len(p.LegalMoves())
 11	}
 12	n := 0
 13	for _, m := range p.LegalMoves() {
 14		n += perft(p.Play(m), depth-1)
 15	}
 16	return n
 17}
 18
 19func mustFEN(t *testing.T, fen string) Position {
 20	t.Helper()
 21	p, err := ParseFEN(fen)
 22	if err != nil {
 23		t.Fatalf("ParseFEN(%q): %v", fen, err)
 24	}
 25	return p
 26}
 27
 28func mustMove(t *testing.T, s string) Move {
 29	t.Helper()
 30	m, err := ParseMove(s)
 31	if err != nil {
 32		t.Fatalf("ParseMove(%q): %v", s, err)
 33	}
 34	return m
 35}
 36
 37func isLegal(t *testing.T, p Position, s string) bool {
 38	t.Helper()
 39	_, ok := p.FindLegal(mustMove(t, s))
 40	return ok
 41}
 42
 43func TestStartPosition(t *testing.T) {
 44	p := NewGame()
 45	if got := p.FEN(); got != StartFEN {
 46		t.Fatalf("FEN round trip: got %q", got)
 47	}
 48	if p.PieceAt("e1") != King || p.PieceAt("d8") != -Queen {
 49		t.Fatalf("pieces misplaced")
 50	}
 51}
 52
 53// Perft values cross-checked against python-chess. These positions cover
 54// castling, en passant, promotions, pins and discovered checks.
 55func TestPerft(t *testing.T) {
 56	cases := []struct {
 57		fen  string
 58		want []int
 59	}{
 60		{StartFEN, []int{20, 400, 8902}},
 61		{"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1", []int{48, 2039}},
 62		{"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", []int{14, 191, 2812}},
 63		{"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1", []int{6, 264, 9467}},
 64		{"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8", []int{44, 1486}},
 65	}
 66	for _, c := range cases {
 67		p := mustFEN(t, c.fen)
 68		for d, w := range c.want {
 69			if got := perft(p, d+1); got != w {
 70				t.Fatalf("%s: perft(%d) = %d, want %d", c.fen, d+1, got, w)
 71			}
 72		}
 73	}
 74}
 75
 76func TestParseMove(t *testing.T) {
 77	m := mustMove(t, "E7E8N")
 78	if m.Promo != Knight || m.String() != "e7e8n" {
 79		t.Fatalf("got %v", m)
 80	}
 81	for _, bad := range []string{"", "e2", "i1i2", "a1a9", "e7e8k", "e2e4qq"} {
 82		if _, err := ParseMove(bad); err == nil {
 83			t.Fatalf("ParseMove(%q) should fail", bad)
 84		}
 85	}
 86}
 87
 88func TestStartMoves(t *testing.T) {
 89	p := NewGame()
 90	for _, s := range []string{"e2e4", "e2e3", "g1f3", "b1a3"} {
 91		if !isLegal(t, p, s) {
 92			t.Fatalf("%s should be legal", s)
 93		}
 94	}
 95	for _, s := range []string{"e2e5", "e1e2", "e1g1", "d1d3", "e7e5"} {
 96		if isLegal(t, p, s) {
 97			t.Fatalf("%s should be illegal", s)
 98		}
 99	}
100	n := p.Play(mustMove(t, "e2e4"))
101	if n.FEN() != "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" {
102		t.Fatalf("after e2e4: %s", n.FEN())
103	}
104}
105
106func TestCastling(t *testing.T) {
107	p := mustFEN(t, "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
108	n := p.Play(mustMove(t, "e1g1"))
109	if n.PieceAt("g1") != King || n.PieceAt("f1") != Rook || n.PieceAt("h1") != Empty {
110		t.Fatalf("king-side castle wrong: %s", n.FEN())
111	}
112	if n.Castling != CastleBK|CastleBQ {
113		t.Fatalf("white rights should be gone: %s", n.FEN())
114	}
115	n = p.Play(mustMove(t, "e1c1"))
116	if n.PieceAt("c1") != King || n.PieceAt("d1") != Rook || n.PieceAt("a1") != Empty {
117		t.Fatalf("queen-side castle wrong: %s", n.FEN())
118	}
119
120	// f1 attacked by the rook on f8: cannot castle through it.
121	p = mustFEN(t, "r3kr2/8/8/8/8/8/8/R3K2R w KQq - 0 1")
122	if isLegal(t, p, "e1g1") || !isLegal(t, p, "e1c1") {
123		t.Fatalf("castling through check")
124	}
125	// In check: no castling at all.
126	p = mustFEN(t, "4r1k1/8/8/8/8/8/8/R3K2R w KQ - 0 1")
127	if isLegal(t, p, "e1g1") || isLegal(t, p, "e1c1") {
128		t.Fatalf("castling out of check")
129	}
130	// Moving a rook loses that side's right.
131	p = mustFEN(t, "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
132	n = p.Play(mustMove(t, "h1h2"))
133	if n.Castling != CastleWQ|CastleBK|CastleBQ {
134		t.Fatalf("rook move rights: %s", n.FEN())
135	}
136}
137
138func TestEnPassant(t *testing.T) {
139	p := mustFEN(t, "4k3/8/8/3pP3/8/8/8/4K3 w - d6 0 1")
140	if !isLegal(t, p, "e5d6") {
141		t.Fatalf("e5d6 en passant should be legal")
142	}
143	n := p.Play(mustMove(t, "e5d6"))
144	if n.PieceAt("d5") != Empty || n.PieceAt("d6") != Pawn {
145		t.Fatalf("en passant wrong: %s", n.FEN())
146	}
147	// Without the e.p. square the capture is not available.
148	p = mustFEN(t, "4k3/8/8/3pP3/8/8/8/4K3 w - - 0 1")
149	if isLegal(t, p, "e5d6") {
150		t.Fatalf("e5d6 should be illegal without e.p. right")
151	}
152}
153
154func TestPromotion(t *testing.T) {
155	p := mustFEN(t, "8/P6k/8/8/8/8/8/4K3 w - - 0 1")
156	lm, ok := p.FindLegal(mustMove(t, "a7a8"))
157	if !ok || lm.Promo != Queen || p.Play(lm).PieceAt("a8") != Queen {
158		t.Fatalf("a7a8 should promote to a queen")
159	}
160	lm, ok = p.FindLegal(mustMove(t, "a7a8n"))
161	if !ok || p.Play(lm).PieceAt("a8") != Knight {
162		t.Fatalf("under-promotion to knight failed")
163	}
164}
165
166func TestGameEnd(t *testing.T) {
167	// Fool's mate.
168	if s := mustFEN(t, "rnb1kbnr/pppp1ppp/8/4p3/6Pq/5P2/PPPPP2P/RNBQKBNR w KQkq - 1 3").Status(); s != Checkmate {
169		t.Fatalf("expected checkmate, got %s", s)
170	}
171	if s := mustFEN(t, "7k/5Q2/6K1/8/8/8/8/8 b - - 0 1").Status(); s != Stalemate {
172		t.Fatalf("expected stalemate, got %s", s)
173	}
174	if s := mustFEN(t, "7k/8/8/8/8/8/8/2B1K3 w - - 0 1").Status(); s != InsufficientMaterial {
175		t.Fatalf("K+B vs K should be a draw, got %s", s)
176	}
177	if s := mustFEN(t, "7k/8/8/8/8/8/8/R3K3 w - - 0 1").Status(); s != Ongoing {
178		t.Fatalf("K+R vs K is not a draw, got %s", s)
179	}
180	if s := mustFEN(t, "7k/8/8/8/8/8/8/R3K3 w - - 100 80").Status(); s != FiftyMoveDraw {
181		t.Fatalf("expected 50-move draw, got %s", s)
182	}
183	if NewGame().Status() != Ongoing {
184		t.Fatalf("start should be ongoing")
185	}
186}
187
188func TestBotFindsMateInOne(t *testing.T) {
189	p := mustFEN(t, "6k1/5ppp/8/8/8/8/8/R5K1 w - - 0 1") // Ra8#
190	for level := MinLevel; level <= MaxLevel; level++ {
191		m, ok := BestMove(p, level, 0)
192		if !ok || p.Play(m).Status() != Checkmate {
193			t.Fatalf("level %d played %s, not mate", level, m)
194		}
195	}
196}
197
198func TestBotTakesHangingQueen(t *testing.T) {
199	p := mustFEN(t, "4k3/8/8/3q4/4P3/8/8/4K3 w - - 0 1")
200	for level := MinLevel; level <= MaxLevel; level++ {
201		m, _ := BestMove(p, level, 0)
202		if m.String() != "e4d5" {
203			t.Fatalf("level %d played %s, want e4d5", level, m)
204		}
205	}
206}
207
208// The node budget is what bounds a bot reply's gas cost on-chain.
209func TestBotRespectsNodeBudget(t *testing.T) {
210	positions := []string{StartFEN, "r1bqkb1r/pppp1ppp/2n2n2/4p3/2B1P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4"}
211	for _, fen := range positions {
212		p := mustFEN(t, fen)
213		for level := MinLevel; level <= MaxLevel; level++ {
214			m, nodes, ok := search(p, level, 0)
215			if !ok {
216				t.Fatalf("no move for %s", fen)
217			}
218			if _, legal := p.FindLegal(m); !legal {
219				t.Fatalf("level %d played illegal %s", level, m)
220			}
221			if nodes > Level(level).Budget+1 {
222				t.Fatalf("level %d used %d nodes, budget %d", level, nodes, Level(level).Budget)
223			}
224		}
225	}
226}
227
228func TestBotIsDeterministic(t *testing.T) {
229	p := NewGame()
230	a, _ := BestMove(p, 2, 42)
231	b, _ := BestMove(p, 2, 42)
232	if a != b {
233		t.Fatalf("same inputs gave %s and %s", a, b)
234	}
235}
236
237func TestBotPlaysLegalGame(t *testing.T) {
238	p := NewGame()
239	for ply := 0; ply < 40 && p.Status() == Ongoing; ply++ {
240		m, ok := BestMove(p, 1, ply)
241		if !ok {
242			t.Fatalf("no move in ongoing position %s", p.FEN())
243		}
244		if _, legal := p.FindLegal(m); !legal {
245			t.Fatalf("bot played illegal %s in %s", m, p.FEN())
246		}
247		p = p.Play(m)
248	}
249}