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}