Search Apps Documentation Source Content File Folder Download Copy Actions Download

engine_test.gno

17.44 Kb ยท 600 lines
  1package chess
  2
  3import (
  4	"fmt"
  5	"strconv"
  6	"strings"
  7	"testing"
  8)
  9
 10func movePromo(from, to string, promo Piece) Move {
 11	m := move(from, to)
 12	m.Promotion = promo
 13	return m
 14}
 15
 16func move(from, to string) Move {
 17	return Move{
 18		From: SquareFromString(strings.ToLower(from)),
 19		To:   SquareFromString(strings.ToLower(to)),
 20	}
 21}
 22
 23func TestCheckmate(t *testing.T) {
 24	fp := unsafeFEN("rn1qkbnr/pbpp1ppp/1p6/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 0 1")
 25	m := move("f3", "f7")
 26	newp, ok := fp.ValidateMove(m)
 27	if !ok {
 28		t.Fatal("ValidateMove returned false")
 29	}
 30	mr := newp.IsFinished()
 31	if mr != Checkmate {
 32		t.Fatalf("expected Checkmate (%d), got %d", Checkmate, mr)
 33	}
 34}
 35
 36func TestCheckmateFromFEN(t *testing.T) {
 37	fp := unsafeFEN("rn1qkbnr/pbpp1Qpp/1p6/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 1")
 38	mr := fp.IsFinished()
 39	if mr != Checkmate {
 40		t.Fatalf("expected Checkmate (%d), got %d", Checkmate, mr)
 41	}
 42}
 43
 44func TestStalemate(t *testing.T) {
 45	fp := unsafeFEN("k1K5/8/8/8/8/8/8/1Q6 w - - 0 1")
 46	m := move("b1", "b6")
 47	newp, ok := fp.ValidateMove(m)
 48	if !ok {
 49		t.Fatal("ValidateMove rejected move")
 50	}
 51	mr := newp.IsFinished()
 52	if mr != Stalemate {
 53		t.Fatalf("expected Stalemate (%d), got %d", Stalemate, mr)
 54	}
 55}
 56
 57// position shouldn't result in check/stalemate
 58// because pawn can move http://en.lichess.org/Pc6mJDZN#138
 59func TestNotMate(t *testing.T) {
 60	fp := unsafeFEN("8/3P4/8/8/8/7k/7p/7K w - - 2 70")
 61	m := movePromo("d7", "d8", PieceQueen)
 62	newp, ok := fp.ValidateMove(m)
 63	if !ok {
 64		t.Fatal("ValidateMove returned false")
 65	}
 66	mr := newp.IsFinished()
 67	if mr != NotFinished {
 68		t.Fatalf("expected NotFinished (%d), got %d", NotFinished, mr)
 69	}
 70}
 71
 72func TestXFoldRepetition(t *testing.T) {
 73	p := NewPosition()
 74	loop := [...]Move{
 75		move("g1", "f3"),
 76		move("g8", "f6"),
 77		move("f3", "g1"),
 78		move("f6", "g8"),
 79	}
 80	var valid bool
 81	for i := 0; i < 5; i++ {
 82		for j, m := range loop {
 83			p, valid = p.ValidateMove(m)
 84			if !valid {
 85				t.Fatalf("move %s not considered valid", m.String())
 86			}
 87			fini := p.IsFinished()
 88			switch {
 89			case (i == 3 && j == 3) || i == 4:
 90				// after the fourth full iteration, it should be marked as "drawn" for 5-fold.
 91				if fini != Drawn5Fold {
 92					t.Errorf("i: %d j: %d; expect %d got %d", i, j, Drawn5Fold, fini)
 93				}
 94			case (i == 1 && j == 3) || i >= 2:
 95				// After the second full iteration, IsFinished should mark this as "can 3 fold"
 96				if fini != Can3Fold {
 97					t.Errorf("i: %d j: %d; expect %d got %d", i, j, Can3Fold, fini)
 98				}
 99			default:
100				if fini != NotFinished {
101					t.Errorf("i: %d j: %d; expect %d got %d", i, j, NotFinished, fini)
102				}
103			}
104		}
105	}
106}
107
108func assertMoves(p Position, moves ...Move) Position {
109	var valid bool
110	for _, move := range moves {
111		p, valid = p.ValidateMove(move)
112		if !valid {
113			panic("invalid move")
114		}
115	}
116	return p
117}
118
119func TestXFoldRepetition2(t *testing.T) {
120	// Like TestXFoldRepetition, but starts after the initial position.
121
122	p := assertMoves(
123		NewPosition(),
124		move("f2", "f4"),
125		move("c7", "c5"),
126	)
127
128	loop := [...]Move{
129		move("g1", "f3"),
130		move("g8", "f6"),
131		move("f3", "g1"),
132		move("f6", "g8"),
133	}
134	var valid bool
135	for i := 0; i < 5; i++ {
136		for j, m := range loop {
137			p, valid = p.ValidateMove(m)
138			if !valid {
139				t.Fatalf("move %s not considered valid", m.String())
140			}
141			fini := p.IsFinished()
142			switch {
143			case i == 4:
144				if fini != Drawn5Fold {
145					t.Errorf("i: %d j: %d; expect %d got %d", i, j, Drawn5Fold, fini)
146				}
147			case i >= 2:
148				if fini != Can3Fold {
149					t.Errorf("i: %d j: %d; expect %d got %d", i, j, Can3Fold, fini)
150				}
151			default:
152				if fini != NotFinished {
153					t.Errorf("i: %d j: %d; expect %d got %d", i, j, NotFinished, fini)
154				}
155			}
156		}
157	}
158}
159
160func TestXMoveRule(t *testing.T) {
161	p := NewPosition()
162
163	p.HalfMoveClock = 99
164	newp := assertMoves(p, move("g1", "f3"))
165	fini := newp.IsFinished()
166	if fini != Can50Move {
167		t.Errorf("want %d got %d", Can50Move, fini)
168	}
169
170	p.HalfMoveClock = 149
171	newp = assertMoves(p, move("g1", "f3"))
172	fini = newp.IsFinished()
173	if fini != Drawn75Move {
174		t.Errorf("want %d got %d", Drawn75Move, fini)
175	}
176}
177
178func TestInsufficientMaterial(t *testing.T) {
179	fens := []string{
180		"8/2k5/8/8/8/3K4/8/8 w - - 1 1",
181		"8/2k5/8/8/8/3K1N2/8/8 w - - 1 1",
182		"8/2k5/8/8/8/3K1B2/8/8 w - - 1 1",
183		"8/2k5/2b5/8/8/3K1B2/8/8 w - - 1 1",
184		"8/2k1n1n1/8/8/8/3K4/8/8 w - - 1 1",
185		"8/2k1b3/8/8/8/3K1B2/8/8 w - - 1 1",
186	}
187	for _, f := range fens {
188		pos := unsafeFEN(f)
189		o := pos.IsFinished()
190		if o != CanInsufficient {
191			t.Errorf("fen %q: want %d got %d", f, CanInsufficient, o)
192		}
193	}
194}
195
196func TestSufficientMaterial(t *testing.T) {
197	fens := []string{
198		"8/2k5/8/8/8/3K1B2/4N3/8 w - - 1 1",
199		"8/2k5/8/8/8/3KBB2/8/8 w - - 1 1",
200		"8/2k5/8/8/4P3/3K4/8/8 w - - 1 1",
201		"8/2k5/8/8/8/3KQ3/8/8 w - - 1 1",
202		"8/2k5/8/8/8/3KR3/8/8 w - - 1 1",
203	}
204	for _, f := range fens {
205		pos := unsafeFEN(f)
206		o := pos.IsFinished()
207		if o != NotFinished {
208			t.Errorf("fen %q: want %d got %d", f, NotFinished, o)
209		}
210	}
211}
212
213type moveTest struct {
214	pos     unsafeFENRes
215	m       Move
216	postPos unsafeFENRes
217}
218
219var (
220	invalidMoves = []moveTest{
221		// out of turn moves
222		{m: move("E7", "E5"), pos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")},
223		{m: move("E2", "E4"), pos: unsafeFEN("rnbqkbnr/1ppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1")},
224		// pawn moves
225		{m: move("E2", "D3"), pos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")},
226		{m: move("E2", "F3"), pos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")},
227		{m: move("E2", "E5"), pos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")},
228		{m: move("A2", "A1"), pos: unsafeFEN("8/8/8/8/8/8/p7/8 b - - 0 1")},
229		{m: move("E6", "E5"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},
230		{m: move("H7", "H5"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},
231		// knight moves
232		{m: move("E4", "F2"), pos: unsafeFEN("8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1")},
233		{m: move("E4", "F3"), pos: unsafeFEN("8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1")},
234		// bishop moves
235		{m: move("E4", "C6"), pos: unsafeFEN("8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1")},
236		{m: move("E4", "E5"), pos: unsafeFEN("8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1")},
237		{m: move("E4", "E4"), pos: unsafeFEN("8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1")},
238		{m: move("E4", "F3"), pos: unsafeFEN("8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1")},
239		// rook moves
240		{m: move("B2", "B1"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1")},
241		{m: move("B2", "C3"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1")},
242		{m: move("B2", "B8"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1")},
243		{m: move("B2", "G7"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1")},
244		// queen moves
245		{m: move("B2", "B1"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1")},
246		{m: move("B2", "C4"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1")},
247		{m: move("B2", "B8"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1")},
248		{m: move("B2", "G7"), pos: unsafeFEN("8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1")},
249		// king moves
250		{m: move("E4", "F3"), pos: unsafeFEN("5r2/8/8/8/4K3/8/8/8 w - - 0 1")},
251		{m: move("E4", "F4"), pos: unsafeFEN("5r2/8/8/8/4K3/8/8/8 w - - 0 1")},
252		{m: move("E4", "F5"), pos: unsafeFEN("5r2/8/8/8/4K3/8/8/8 w - - 0 1")},
253		// castleing
254		{m: move("E1", "B1"), pos: unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")},
255		{m: move("E8", "B8"), pos: unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1")},
256		{m: move("E1", "C1"), pos: unsafeFEN("r3k2r/8/8/8/8/8/8/R2QK2R w KQkq - 0 1")},
257		{m: move("E1", "C1"), pos: unsafeFEN("2r1k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")},
258		{m: move("E1", "C1"), pos: unsafeFEN("3rk2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")},
259		{m: move("E1", "G1"), pos: unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R w Qkq - 0 1")},
260		{m: move("E1", "C1"), pos: unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R w Kkq - 0 1")},
261		// invalid promotion for non-pawn move
262		{m: movePromo("B8", "D7", PieceQueen), pos: unsafeFEN("rn1qkb1r/pp3ppp/2p1pn2/3p4/2PP4/2NQPN2/PP3PPP/R1B1K2R b KQkq - 0 7")},
263		// en passant on doubled pawn file http://en.lichess.org/TnRtrHxf#24
264		{m: move("E3", "F6"), pos: unsafeFEN("r1b2rk1/pp2b1pp/1qn1p3/3pPp2/1P1P4/P2BPN2/6PP/RN1Q1RK1 w - f6 0 13")},
265		// can't move piece out of pin (even if checking enemy king) http://en.lichess.org/JCRBhXH7#62
266		{m: move("E1", "E7"), pos: unsafeFEN("4R3/1r1k2pp/p1p5/1pP5/8/8/1PP3PP/2K1Rr2 w - - 5 32")},
267		// invalid one up pawn capture
268		{m: move("E6", "E5"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},
269		// invalid two up pawn capture
270		{m: move("H7", "H5"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},
271		// invalid pawn move d5e4
272		{m: move("D5", "E4"), pos: unsafeFEN(`rnbqkbnr/pp2pppp/8/2pp4/3P4/4PN2/PPP2PPP/RNBQKB1R b KQkq - 0 3`)},
273	}
274
275	positionUpdates = []moveTest{
276		{
277			m:       move("E2", "E4"),
278			pos:     unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"),
279			postPos: unsafeFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"),
280		},
281		{
282			m:       move("E1", "G1"),
283			pos:     unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"),
284			postPos: unsafeFEN("r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1"),
285		},
286		{
287			m:       move("A4", "B3"),
288			pos:     unsafeFEN("2r3k1/1q1nbppp/r3p3/3pP3/pPpP4/P1Q2N2/2RN1PPP/2R4K b - b3 0 23"),
289			postPos: unsafeFEN("2r3k1/1q1nbppp/r3p3/3pP3/11pP4/PpQ2N2/2RN1PPP/2R4K w - - 0 24"),
290		},
291		{
292			m:       move("E1", "G1"),
293			pos:     unsafeFEN("r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B1K2R w KQkq - 1 9"),
294			postPos: unsafeFEN("r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B2RK1 b kq - 2 9"),
295		},
296		// half move clock - knight move to f3 from starting position
297		{
298			m:       move("G1", "F3"),
299			pos:     unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"),
300			postPos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1"),
301		},
302		// half move clock - king side castle
303		{
304			m:       move("E1", "G1"),
305			pos:     unsafeFEN("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1"),
306			postPos: unsafeFEN("r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1"),
307		},
308		// half move clock - queen side castle
309		{
310			m:       move("E1", "C1"),
311			pos:     unsafeFEN("r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/R3K2R w KQkq - 3 10"),
312			postPos: unsafeFEN("r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/2KR3R b kq - 4 10"),
313		},
314		// half move clock - pawn push
315		{
316			m:       move("E2", "E4"),
317			pos:     unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"),
318			postPos: unsafeFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"),
319		},
320		// half move clock - pawn capture
321		{
322			m:       move("E4", "D5"),
323			pos:     unsafeFEN("r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3"),
324			postPos: unsafeFEN("r1bqkbnr/ppp1pppp/2n5/3P4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 3"),
325		},
326		// half move clock - en passant
327		{
328			m:       move("E5", "F6"),
329			pos:     unsafeFEN("r1bqkbnr/ppp1p1pp/2n5/3pPp2/8/5N2/PPPP1PPP/RNBQKB1R w KQkq f6 0 4"),
330			postPos: unsafeFEN("r1bqkbnr/ppp1p1pp/2n2P2/3p4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 4"),
331		},
332		// half move clock - piece captured by knight
333		{
334			m:       move("C6", "D4"),
335			pos:     unsafeFEN("r1bqkbnr/ppp1p1pp/2n5/3pPp2/3N4/8/PPPP1PPP/RNBQKB1R b KQkq - 1 4"),
336			postPos: unsafeFEN("r1bqkbnr/ppp1p1pp/8/3pPp2/3n4/8/PPPP1PPP/RNBQKB1R w KQkq - 0 5"),
337		},
338	}
339)
340
341func TestInvalidMoves(t *testing.T) {
342	for _, mt := range invalidMoves {
343		_, ok := mt.pos.ValidateMove(mt.m)
344		if ok {
345			t.Errorf("fen %q: unexpected valid move (%s)", mt.pos.orig, mt.m.String())
346		}
347	}
348}
349
350func TestPositionUpdates(t *testing.T) {
351	for _, mt := range positionUpdates {
352		np, ok := mt.pos.ValidateMove(mt.m)
353		if !ok {
354			t.Errorf("fen %q: rejected valid move (%s)", mt.pos.orig, mt.m.String())
355			continue
356		}
357		if np.B != mt.postPos.B {
358			t.Errorf("%q: boards don't match", mt.pos.orig)
359		}
360		if np.HalfMoveClock != mt.postPos.HalfMoveClock {
361			t.Errorf("%q: hmc doesn't match; want %d got %d", mt.pos.orig, mt.postPos.HalfMoveClock, np.HalfMoveClock)
362		}
363		if np.HalfMoveClock == 0 && len(np.Hashes) != 1 {
364			t.Errorf("%q: hashes not reset", mt.pos.orig)
365		}
366		if np.Flags != mt.postPos.Flags {
367			t.Errorf("%q: flags don't match; want %d got %d", mt.pos.orig, mt.postPos.Flags, np.Flags)
368		}
369	}
370}
371
372func TestPerft(t *testing.T) {
373	moves := make([]Move, 0, 10)
374	for n, res := range perfResults {
375		t.Run(fmt.Sprintf("n%d", n), func(t *testing.T) {
376			if testing.Short() {
377				t.Skip("skipping perft in short tests")
378			}
379			res.pos.Moves = append(moves[:0], res.pos.Moves...)
380			counts := make([]int, len(res.nodesPerDepth))
381			CountMoves(res.pos.Position, len(res.nodesPerDepth), counts)
382			t.Logf("counts: %v", counts)
383			if !intsMatch(counts, res.nodesPerDepth) {
384				t.Errorf("counts don't match: got %v want %v", counts, res.nodesPerDepth)
385			}
386		})
387	}
388}
389
390func intsMatch(xx, yy []int) bool {
391	if len(xx) != len(yy) {
392		return false
393	}
394	for i := range xx {
395		if xx[i] != yy[i] {
396			return false
397		}
398	}
399	return true
400}
401
402const perftDebug = false
403
404func CountMoves(p Position, depth int, counts []int) {
405	total := 0
406	l := len(counts) - depth
407	p.GenMoves(func(newp Position, m Move) error {
408		counts[l]++
409		if depth > 1 {
410			countMoves(newp, depth-1, counts)
411		}
412		delta := counts[len(counts)-1] - total
413		if perftDebug {
414			fmt.Printf("%s%s: %d\n", m.From.String(), m.To.String(), delta)
415		}
416		total += delta
417		return nil
418	})
419}
420
421func countMoves(p Position, depth int, counts []int) {
422	l := len(counts) - depth
423	p.GenMoves(func(newp Position, m Move) error {
424		counts[l]++
425		if depth > 1 {
426			countMoves(newp, depth-1, counts)
427		}
428		return nil
429	})
430}
431
432type perfTest struct {
433	pos           unsafeFENRes
434	nodesPerDepth []int
435}
436
437/* https://www.chessprogramming.org/Perft_Results */
438var perfResults = []perfTest{
439	{pos: unsafeFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"), nodesPerDepth: []int{
440		20, 400, 8902, // 197281,
441		// 4865609, 119060324, 3195901860, 84998978956, 2439530234167, 69352859712417
442	}},
443	{pos: unsafeFEN("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"), nodesPerDepth: []int{
444		48, 2039, 97862,
445		// 4085603, 193690690
446	}},
447	{pos: unsafeFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"), nodesPerDepth: []int{
448		14, 191, 2812, 43238, // 674624,
449		// 11030083, 178633661
450	}},
451	{pos: unsafeFEN("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"), nodesPerDepth: []int{
452		6, 264, 9467, // 422333,
453		// 15833292, 706045033
454	}},
455	{pos: unsafeFEN("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1"), nodesPerDepth: []int{
456		6, 264, 9467, // 422333,
457		// 15833292, 706045033
458	}},
459	{pos: unsafeFEN("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8"), nodesPerDepth: []int{
460		44, 1486, 62379,
461		// 2103487, 89941194
462	}},
463	{pos: unsafeFEN("r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10"), nodesPerDepth: []int{
464		46, 2079, // 89890,
465		// 3894594, 164075551, 6923051137, 287188994746, 11923589843526, 490154852788714
466	}},
467}
468
469// ---
470// testing utility functions
471
472// FEN decoding: see https://www.chessprogramming.org/Forsyth-Edwards_Notation
473// copied mostly from notnil/chess and adapted to our own system.
474
475type unsafeFENRes struct {
476	Position
477	orig string
478}
479
480func unsafeFEN(fen string) unsafeFENRes {
481	p, e := decodeFEN(fen)
482	if e != nil {
483		panic(e)
484	}
485	return unsafeFENRes{p, fen}
486}
487
488// Decodes FEN into Board and previous moves.
489func decodeFEN(fen string) (p Position, err error) {
490	fen = strings.TrimSpace(fen)
491	parts := strings.Split(fen, " ")
492	if len(parts) != 6 {
493		err = fmt.Errorf("chess: fen invalid notation %s must have 6 sections", fen)
494		return
495	}
496
497	p = NewPosition()
498
499	// fen board
500	var ok bool
501	p.B, ok = fenBoard(parts[0])
502	if !ok {
503		err = fmt.Errorf("chess: invalid fen board %s", parts[0])
504		return
505	}
506
507	// do castling rights first (more convenient to set prev)
508	if parts[2] != "KQkq" {
509		p.Flags = castleRightsToPositionFlags(parts[2])
510	}
511
512	// color to play
513	color := Color(parts[1] == "b")
514	if color == Black {
515		// add fake move to make len(prev) odd
516		p.Moves = append(p.Moves, Move{})
517	}
518
519	// en passant
520	if parts[3] != "-" {
521		f, e := parseEnPassant(parts[3])
522		if e != nil {
523			err = e
524			return
525		}
526		p.Flags |= f
527	}
528
529	halfMove, _ := strconv.Atoi(parts[4])
530	p.HalfMoveClock = uint16(halfMove)
531
532	// parts[5]: full move counter, probably never implementing
533
534	return
535}
536
537// generates board from fen format: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
538func fenBoard(boardStr string) (Board, bool) {
539	rankStrs := strings.Split(boardStr, "/")
540	if len(rankStrs) != 8 {
541		return Board{}, false
542	}
543	var b Board
544	for idx, pieces := range rankStrs {
545		rank := (7 - Square(idx)) << 3
546		file := Square(0)
547		for _, ch := range pieces {
548			if ch >= '1' && ch <= '8' {
549				delta := byte(ch) - '0'
550				file += Square(delta)
551				if file > 8 {
552					return b, false
553				}
554				continue
555			}
556			piece := p[byte(ch)]
557			if piece == PieceEmpty || file >= 8 {
558				return b, false
559			}
560			b[rank|file] = piece
561			file++
562		}
563		if file != 8 {
564			return b, false
565		}
566	}
567	return b, true
568}
569
570func castleRightsToPositionFlags(cr string) (pf PositionFlags) {
571	pf = NoCastleWQ | NoCastleWK | NoCastleBQ | NoCastleBK
572	if cr == "-" {
573		return
574	}
575	for _, ch := range cr {
576		switch ch {
577		case 'K':
578			pf &^= NoCastleWK
579		case 'Q':
580			pf &^= NoCastleWQ
581		case 'k':
582			pf &^= NoCastleBK
583		case 'q':
584			pf &^= NoCastleBQ
585		}
586	}
587	return
588}
589
590func parseEnPassant(strpos string) (PositionFlags, error) {
591	eppos := SquareFromString(strpos)
592	if eppos == SquareInvalid {
593		return 0, fmt.Errorf("invalid pos: %s", eppos)
594	}
595	row, col := eppos.Split()
596	if row != 5 && row != 2 {
597		return 0, fmt.Errorf("invalid en passant pos: %s", eppos)
598	}
599	return EnPassant | PositionFlags(col), nil
600}