···13131414pub type Move {
1515 Castle(from: Int, to: Int)
1616- Move(from: Int, to: Int)
1717- Capture(from: Int, to: Int)
1616+ Move(from: Int, to: Int, piece: board.Piece)
1717+ Capture(from: Int, to: Int, piece: board.Piece, captured_piece: board.Piece)
1818 EnPassant(from: Int, to: Int)
1919- Promotion(from: Int, to: Int, piece: board.Piece)
1919+ Promotion(
2020+ from: Int,
2121+ to: Int,
2222+ piece: board.Piece,
2323+ captured_piece: Option(board.Piece),
2424+ )
2525+}
2626+2727+pub fn moving_piece(move: Move) -> board.Piece {
2828+ case move {
2929+ Capture(piece:, ..) | Move(piece:, ..) -> piece
3030+ Castle(..) -> board.King
3131+ EnPassant(..) | Promotion(..) -> board.Pawn
3232+ }
2033}
21342235pub fn legal(game: Game) -> List(Move) {
···8396) -> List(Move) {
8497 case piece {
8598 board.Bishop ->
8686- sliding_moves(game, position, moves, direction.bishop_directions)
9999+ sliding_moves(game, piece, position, moves, direction.bishop_directions)
87100 board.Rook ->
8888- sliding_moves(game, position, moves, direction.rook_directions)
101101+ sliding_moves(game, piece, position, moves, direction.rook_directions)
89102 board.Queen ->
9090- sliding_moves(game, position, moves, direction.queen_directions)
103103+ sliding_moves(game, piece, position, moves, direction.queen_directions)
91104 board.King -> king_moves(game, position, moves, direction.queen_directions)
92105 board.Knight ->
93106 knight_moves(game, position, moves, direction.knight_directions)
···120133 {
121134 False -> moves
122135 True if is_promotion ->
123123- add_promotions(position, forward_one, moves, board.pawn_promotions)
124124- True -> [Move(from: position, to: forward_one), ..moves]
136136+ add_promotions(
137137+ position,
138138+ forward_one,
139139+ None,
140140+ moves,
141141+ board.pawn_promotions,
142142+ )
143143+ True -> [Move(board.Pawn, from: position, to: forward_one), ..moves]
125144 }
126145127146 let can_double_move = case game.to_move, position / 8 {
···136155 board.Empty ->
137156 case can_move(position, forward_two, game.attack_information) {
138157 False -> moves
139139- True -> [Move(from: position, to: forward_two), ..moves]
158158+ True -> [Move(board.Pawn, from: position, to: forward_two), ..moves]
140159 }
141160 board.Occupied(_, _) | board.OffBoard -> moves
142161 }
···146165147166 let new_position = direction.in_direction(position, left)
148167 let moves = case board.get(game.board, new_position) {
149149- board.Occupied(colour:, ..) if colour != game.to_move ->
168168+ board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
150169 case can_move(position, new_position, game.attack_information) {
151170 False -> moves
152171 True if is_promotion ->
153153- add_promotions(position, new_position, moves, board.pawn_promotions)
154154- True -> [Capture(from: position, to: new_position), ..moves]
172172+ add_promotions(
173173+ position,
174174+ new_position,
175175+ Some(captured_piece),
176176+ moves,
177177+ board.pawn_promotions,
178178+ )
179179+ True -> [
180180+ Capture(board.Pawn, from: position, to: new_position, captured_piece:),
181181+ ..moves
182182+ ]
155183 }
156184 board.Empty if game.en_passant_square == Some(new_position) ->
157185 case en_passant_is_valid(game, position, new_position) {
···163191164192 let new_position = direction.in_direction(position, right)
165193 case board.get(game.board, new_position) {
166166- board.Occupied(colour:, ..) if colour != game.to_move ->
194194+ board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
167195 case can_move(position, new_position, game.attack_information) {
168196 False -> moves
169197 True if is_promotion ->
170170- add_promotions(position, new_position, moves, board.pawn_promotions)
171171- True -> [Capture(from: position, to: new_position), ..moves]
198198+ add_promotions(
199199+ position,
200200+ new_position,
201201+ Some(captured_piece),
202202+ moves,
203203+ board.pawn_promotions,
204204+ )
205205+ True -> [
206206+ Capture(board.Pawn, from: position, to: new_position, captured_piece:),
207207+ ..moves
208208+ ]
172209 }
173210 board.Empty if game.en_passant_square == Some(new_position) ->
174211 case en_passant_is_valid(game, position, new_position) {
···288325fn add_promotions(
289326 from: Int,
290327 to: Int,
328328+ captured_piece: Option(board.Piece),
291329 moves: List(Move),
292330 pieces: List(board.Piece),
293331) -> List(Move) {
294332 case pieces {
295333 [] -> moves
296334 [piece, ..pieces] ->
297297- add_promotions(from, to, [Promotion(from:, to:, piece:), ..moves], pieces)
335335+ add_promotions(
336336+ from,
337337+ to,
338338+ captured_piece,
339339+ [Promotion(from:, to:, piece:, captured_piece:), ..moves],
340340+ pieces,
341341+ )
298342 }
299343}
300344···312356 board.Empty ->
313357 case can_move(position, new_position, game.attack_information) {
314358 False -> moves
315315- True -> [Move(from: position, to: new_position), ..moves]
359359+ True -> [
360360+ Move(board.Knight, from: position, to: new_position),
361361+ ..moves
362362+ ]
316363 }
317317- board.Occupied(colour:, ..) if colour != game.to_move ->
364364+ board.Occupied(colour:, piece: captured_piece)
365365+ if colour != game.to_move
366366+ ->
318367 case can_move(position, new_position, game.attack_information) {
319368 False -> moves
320320- True -> [Capture(from: position, to: new_position), ..moves]
369369+ True -> [
370370+ Capture(
371371+ board.Knight,
372372+ from: position,
373373+ to: new_position,
374374+ captured_piece:,
375375+ ),
376376+ ..moves
377377+ ]
321378 }
322379 board.Occupied(_, _) | board.OffBoard -> moves
323380 }
···388445 board.Empty ->
389446 case king_can_move(new_position, game.attack_information) {
390447 False -> moves
391391- True -> [Move(from: position, to: new_position), ..moves]
448448+ True -> [
449449+ Move(board.King, from: position, to: new_position),
450450+ ..moves
451451+ ]
392452 }
393393- board.Occupied(colour:, ..) if colour != game.to_move ->
453453+ board.Occupied(colour:, piece: captured_piece)
454454+ if colour != game.to_move
455455+ ->
394456 case king_can_move(new_position, game.attack_information) {
395457 False -> moves
396396- True -> [Capture(from: position, to: new_position), ..moves]
458458+ True -> [
459459+ Capture(
460460+ board.King,
461461+ from: position,
462462+ to: new_position,
463463+ captured_piece:,
464464+ ),
465465+ ..moves
466466+ ]
397467 }
398468 board.Occupied(_, _) | board.OffBoard -> moves
399469 }
···405475406476fn sliding_moves(
407477 game: Game,
478478+ piece: board.Piece,
408479 position: Int,
409480 moves: List(Move),
410481 directions: List(Direction),
···414485 [direction, ..directions] ->
415486 sliding_moves(
416487 game,
488488+ piece,
417489 position,
418418- sliding_moves_in_direction(game, position, position, direction, moves),
490490+ sliding_moves_in_direction(
491491+ game,
492492+ piece,
493493+ position,
494494+ position,
495495+ direction,
496496+ moves,
497497+ ),
419498 directions,
420499 )
421500 }
···423502424503fn sliding_moves_in_direction(
425504 game: Game,
505505+ piece: board.Piece,
426506 start_position: Int,
427507 position: Int,
428508 direction: Direction,
···433513 board.Empty ->
434514 sliding_moves_in_direction(
435515 game,
516516+ piece,
436517 start_position,
437518 new_position,
438519 direction,
439520 case can_move(start_position, new_position, game.attack_information) {
440521 False -> moves
441441- True -> [Move(from: start_position, to: new_position), ..moves]
522522+ True -> [Move(piece, from: start_position, to: new_position), ..moves]
442523 },
443524 )
444444- board.Occupied(colour:, ..) if colour != game.to_move ->
525525+ board.Occupied(colour:, piece: captured_piece) if colour != game.to_move ->
445526 case can_move(start_position, new_position, game.attack_information) {
446527 False -> moves
447447- True -> [Capture(from: start_position, to: new_position), ..moves]
528528+ True -> [
529529+ Capture(
530530+ piece,
531531+ from: start_position,
532532+ to: new_position,
533533+ captured_piece:,
534534+ ),
535535+ ..moves
536536+ ]
448537 }
449538 board.Occupied(_, _) | board.OffBoard -> moves
450539 }
···452541453542pub fn apply(game: Game, move: Move) -> game.Game {
454543 case move {
455455- Capture(from:, to:) -> do_apply(game, from, to, False, None, True)
544544+ Capture(from:, to:, piece:, captured_piece:) ->
545545+ do_apply(game, piece, from, to, False, None, Some(captured_piece))
456546 Castle(from:, to:) -> apply_castle(game, from, to, to % 8 == 2)
457457- EnPassant(from:, to:) -> do_apply(game, from, to, True, None, True)
458458- Move(from:, to:) -> do_apply(game, from, to, False, None, False)
459459- Promotion(from:, to:, piece:) ->
460460- do_apply(game, from, to, False, Some(piece), False)
547547+ EnPassant(from:, to:) ->
548548+ do_apply(game, board.Pawn, from, to, True, None, None)
549549+ Move(from:, to:, piece:) ->
550550+ do_apply(game, piece, from, to, False, None, None)
551551+ Promotion(from:, to:, piece:, captured_piece:) ->
552552+ do_apply(game, board.Pawn, from, to, False, Some(piece), captured_piece)
461553 }
462554}
463555···597689598690fn do_apply(
599691 game: Game,
692692+ piece: board.Piece,
600693 from: Int,
601694 to: Int,
602695 en_passant: Bool,
603696 promotion: Option(board.Piece),
604604- capture: Bool,
697697+ captured_piece: Option(board.Piece),
605698) -> Game {
606699 let Game(
607700 board:,
···635728 board.White -> #(white_pieces, black_pieces)
636729 }
637730638638- let assert board.Occupied(piece:, colour:) = board.get(board, from)
639639- as "Tried to apply move from invalid position"
731731+ let our_colour = to_move
732732+ let enemy_colour = case to_move {
733733+ board.Black -> board.White
734734+ board.White -> board.Black
735735+ }
640736641737 let castling =
642738 castling
643739 |> remove_castling(from)
644740 |> remove_castling(to)
645741646646- let one_way_move = capture || piece == board.Pawn
742742+ let one_way_move = captured_piece != None || piece == board.Pawn
647743648744 let zobrist_hash =
649745 previous_hash
650746 |> hash.toggle_to_move
651651- |> hash.toggle_piece(from, piece, colour)
747747+ |> hash.toggle_piece(from, piece, our_colour)
652748653749 let phase =
654750 game.phase(white_pieces.non_pawn_material, black_pieces.non_pawn_material)
655751656752 let our_piece_square_score =
657657- our_piece_square_score - piece_table.piece_score(piece, colour, from, phase)
753753+ our_piece_square_score
754754+ - piece_table.piece_score(piece, our_colour, from, phase)
658755659756 let #(piece, our_pawn_material, our_non_pawn_material) = case promotion {
660757 None -> #(piece, our_pawn_material, our_non_pawn_material)
···666763 }
667764668765 let our_piece_square_score =
669669- our_piece_square_score + piece_table.piece_score(piece, colour, to, phase)
766766+ our_piece_square_score
767767+ + piece_table.piece_score(piece, our_colour, to, phase)
670768671671- let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, colour)
769769+ let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, our_colour)
672770673771 let #(
674772 zobrist_hash,
675773 opposing_pawn_material,
676774 opposing_non_pawn_material,
677775 opposing_piece_square_score,
678678- ) = case board.get(board, to) {
679679- board.Occupied(piece: board.Pawn, colour:) -> #(
680680- hash.toggle_piece(zobrist_hash, to, board.Pawn, colour),
776776+ ) = case captured_piece {
777777+ Some(board.Pawn) -> #(
778778+ hash.toggle_piece(zobrist_hash, to, board.Pawn, enemy_colour),
681779 opposing_pawn_material - board.pawn_value,
682780 opposing_non_pawn_material,
683781 opposing_piece_square_score
684684- - piece_table.piece_score(board.Pawn, colour, to, phase),
782782+ - piece_table.piece_score(board.Pawn, enemy_colour, to, phase),
685783 )
686686- board.Occupied(piece:, colour:) -> #(
687687- hash.toggle_piece(zobrist_hash, to, piece, colour),
784784+ Some(piece) -> #(
785785+ hash.toggle_piece(zobrist_hash, to, piece, enemy_colour),
688786 opposing_pawn_material,
689787 opposing_non_pawn_material - board.piece_value(piece),
690788 opposing_piece_square_score
691691- - piece_table.piece_score(piece, colour, to, phase),
789789+ - piece_table.piece_score(piece, enemy_colour, to, phase),
692790 )
693693- board.Empty | board.OffBoard -> #(
791791+ None -> #(
694792 zobrist_hash,
695793 opposing_pawn_material,
696794 opposing_non_pawn_material,
···701799 let board =
702800 board
703801 |> dict.delete(from)
704704- |> dict.insert(to, #(piece, colour))
802802+ |> dict.insert(to, #(piece, our_colour))
705803706706- let #(board, zobrist_hash) = case en_passant, en_passant_square, colour {
804804+ let #(
805805+ board,
806806+ zobrist_hash,
807807+ opposing_pawn_material,
808808+ opposing_piece_square_score,
809809+ ) = case en_passant, en_passant_square, our_colour {
707810 True, Some(square), board.White -> {
708811 let ep_square = square - 8
709812 #(
710813 dict.delete(board, ep_square),
711814 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.Black),
815815+ opposing_pawn_material - board.pawn_value,
816816+ opposing_piece_square_score
817817+ - piece_table.piece_score(board.Pawn, board.Black, ep_square, phase),
712818 )
713819 }
714820 True, Some(square), board.Black -> {
···716822 #(
717823 dict.delete(board, ep_square),
718824 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.White),
825825+ opposing_pawn_material - board.pawn_value,
826826+ opposing_piece_square_score
827827+ - piece_table.piece_score(board.Pawn, board.White, ep_square, phase),
719828 )
720829 }
721721- _, _, _ -> #(board, zobrist_hash)
830830+ _, _, _ -> #(
831831+ board,
832832+ zobrist_hash,
833833+ opposing_pawn_material,
834834+ opposing_piece_square_score,
835835+ )
722836 }
723837724838 let en_passant_square = case piece, to - from {
···758872 board.Black -> #(opposing_pieces, our_pieces)
759873 }
760874761761- let to_move = case to_move {
762762- board.Black -> board.White
763763- board.White -> board.Black
764764- }
875875+ let to_move = enemy_colour
765876766877 let #(half_moves, previous_positions) = case one_way_move {
767878 True -> #(0, [])
···823934}
824935825936pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String {
826826- let assert board.Occupied(piece:, colour: _) =
827827- board.get(game.board, move.from)
828828- as "Legal moves should only move valid pieces"
937937+ let piece = moving_piece(move)
829938830939 case move {
831940 Castle(from: _, to:) -> {
···835944 True -> "O-O-O"
836945 }
837946 }
838838- Capture(from:, to:) if piece == board.Pawn ->
947947+ Capture(from:, to:, ..) if piece == board.Pawn ->
839948 pawn_move_to_san(from, to, True, None)
840949 EnPassant(from:, to:) -> pawn_move_to_san(from, to, True, None)
841841- Promotion(from:, to:, piece:) -> {
842842- let is_capture = case board.get(game.board, move.to) {
843843- board.Occupied(..) -> True
844844- board.Empty | board.OffBoard -> False
845845- }
846846- pawn_move_to_san(from, to, is_capture, Some(piece))
847847- }
848848- Move(from:, to:) -> move_to_san(game, piece, from, to, False)
849849- Capture(from:, to:) -> move_to_san(game, piece, from, to, True)
950950+ Promotion(from:, to:, piece:, captured_piece: None) ->
951951+ pawn_move_to_san(from, to, False, Some(piece))
952952+ Promotion(from:, to:, piece:, captured_piece: Some(_)) ->
953953+ pawn_move_to_san(from, to, True, Some(piece))
954954+ Move(from:, to:, ..) -> move_to_san(game, piece, from, to, False)
955955+ Capture(from:, to:, ..) -> move_to_san(game, piece, from, to, True)
850956 }
851957}
852958···886992 use <- bool.guard(move.to != to, disambiguation)
887993 use <- bool.guard(move.from == from, disambiguation)
888994889889- let assert board.Occupied(piece: moving_piece, colour: _) =
890890- board.get(game.board, move.from)
891891- as "Legal moves should only move valid pieces"
995995+ let moving_piece = moving_piece(move)
892996893997 use <- bool.guard(moving_piece != piece, disambiguation)
894998···10191123 use #(first, move) <- result.try(parse_move_part(move))
10201124 use #(second, move) <- result.try(parse_move_part(move))
1021112510221022- use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try(
11261126+ use #(from_file, from_rank, to_file, to_rank, move) <- result.try(
10231127 case first, second {
10241128 // `xx` is not an allowed move
10251129 CaptureSpecifier, CaptureSpecifier -> Error(Nil)
···10271131 File(file), CaptureSpecifier -> {
10281132 let from_file = Some(file)
10291133 let from_rank = None
10301030- let capture = True
10311134 use #(to_file, move) <- result.try(parse_file(move))
10321135 use #(to_rank, move) <- result.try(parse_rank(move))
1033113610341034- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11371137+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10351138 }
10361139 // We disambiguate the rank and it's a capture (e.g. `R5xc4`)
10371140 Rank(rank), CaptureSpecifier -> {
10381141 let from_file = None
10391142 let from_rank = Some(rank)
10401040- let capture = True
10411143 use #(to_file, move) <- result.try(parse_file(move))
10421144 use #(to_rank, move) <- result.try(parse_rank(move))
1043114510441044- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11461146+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10451147 }
10461148 // It's a capture, and we've parsed the file of the destination (e.g.
10471149 // `Bxa5`)
10481150 CaptureSpecifier, File(to_file) -> {
10491151 let from_file = None
10501152 let from_rank = None
10511051- let capture = True
10521153 use #(to_rank, move) <- result.try(parse_rank(move))
1053115410541054- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11551155+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10551156 }
10561157 // We disambiguate the file and we've parsed the file of the destination
10571158 // (e.g. `Qhd4`)
10581159 File(from_file), File(to_file) -> {
10591160 let from_file = Some(from_file)
10601161 let from_rank = None
10611061- let capture = False
10621162 use #(to_rank, move) <- result.try(parse_rank(move))
1063116310641064- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11641164+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10651165 }
10661166 // We disambiguate the rank and we've parsed the file of the destination
10671167 // (e.g. `R7d2`)
10681168 Rank(rank), File(to_file) -> {
10691169 let from_file = None
10701170 let from_rank = Some(rank)
10711071- let capture = False
10721171 use #(to_rank, move) <- result.try(parse_rank(move))
1073117210741074- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11731173+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10751174 }
10761175 // Capture followed by a rank is not allowed, e.g. `Rx1`
10771176 CaptureSpecifier, Rank(_) -> Error(Nil)
10781177 // We've parsed the file and rank, and there's no more move to parse,
10791178 // so we're done. (e.g. `Nf3`)
10801179 File(file), Rank(rank) if move == "" ->
10811081- Ok(#(None, None, False, file, rank, move))
11801180+ Ok(#(None, None, file, rank, move))
10821181 // We've disambiguated the rank and file, and we still need to parse
10831182 // the rest of the move. (e.g. `Qh4xe1`)
10841183 File(from_file), Rank(from_rank) ->
···10861185 Ok(#(CaptureSpecifier, move)) -> {
10871186 let from_file = Some(from_file)
10881187 let from_rank = Some(from_rank)
10891089- let capture = True
10901188 use #(to_file, move) <- result.try(parse_file(move))
10911189 use #(to_rank, move) <- result.try(parse_rank(move))
1092119010931093- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11911191+ Ok(#(from_file, from_rank, to_file, to_rank, move))
10941192 }
10951193 Ok(#(File(to_file), _)) -> {
10961194 let from_file = Some(from_file)
10971195 let from_rank = Some(from_rank)
10981098- let capture = False
10991196 use #(to_rank, move) <- result.try(parse_rank(move))
1100119711011101- Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
11981198+ Ok(#(from_file, from_rank, to_file, to_rank, move))
11021199 }
11031200 Ok(#(Rank(_), _)) | Error(_) -> Error(Nil)
11041201 }
···1111120811121209 let to = to_rank * 8 + to_file
1113121011141114- case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) {
11151115- [from] if capture -> Ok(Capture(from:, to:))
11161116- [from] -> Ok(Move(from:, to:))
12111211+ case
12121212+ get_moves(game, piece_kind, legal_moves, from_file, from_rank, to, None)
12131213+ {
12141214+ [move] -> Ok(move)
11171215 // If there is more than one valid move, the notation is ambiguous, and
11181216 // so we error. If there are no valid moves, we also error.
11191217 _ -> Error(Nil)
···11781276) -> Result(Move, Nil) {
11791277 use #(file, move) <- result.try(parse_file(move))
1180127811811181- use #(from_file, is_capture, to_file, move) <- result.try(case move {
12791279+ use #(from_file, to_file, move) <- result.try(case move {
11821280 "x" <> move ->
11831281 parse_file(move)
11841282 |> result.map(fn(pair) {
11851283 let #(to_file, move) = pair
11861186- #(Some(file), True, to_file, move)
12841284+ #(Some(file), to_file, move)
11871285 })
11881188- _ -> Ok(#(None, False, file, move))
12861286+ _ -> Ok(#(None, file, move))
11891287 })
1190128811911289 use #(rank, move) <- result.try(parse_rank(move))
···12021300 let to = rank * 8 + to_file
1203130112041302 case
12051205- get_pieces(game, board.Pawn, legal_moves, from_file, None, to),
12061206- promotion
13031303+ get_moves(game, board.Pawn, legal_moves, from_file, None, to, promotion)
12071304 {
12081208- [from], Some(piece) -> Ok(Promotion(from:, to:, piece:))
12091209- [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:))
12101210- [from], _ if is_capture -> Ok(Capture(from:, to:))
12111211- [from], _ -> Ok(Move(from:, to:))
12121212- _, _ -> Error(Nil)
13051305+ [move] -> Ok(move)
13061306+ _ -> Error(Nil)
12131307 }
12141308}
1215130912161216-/// Gets the possible destination squares for a move, based on the information
12171217-/// we know.
12181218-fn get_pieces(
13101310+/// Gets the possible moves for a piece, based on the information we know from
13111311+/// SAN.
13121312+fn get_moves(
12191313 game: Game,
12201314 find_piece: board.Piece,
12211315 legal_moves: List(Move),
12221316 from_file: option.Option(Int),
12231317 from_rank: option.Option(Int),
12241318 to: Int,
12251225-) -> List(Int) {
12261226- use pieces, position, #(piece, colour) <- dict.fold(game.board, [])
13191319+ promotion: Option(board.Piece),
13201320+) -> List(Move) {
13211321+ use moves, position, #(piece, colour) <- dict.fold(game.board, [])
12271322 let is_valid =
12281323 colour == game.to_move
12291324 && piece == find_piece
···12351330 None -> True
12361331 Some(rank) -> rank == position / 8
12371332 }
12381238- && list.any(legal_moves, fn(move) { move.to == to && move.from == position })
1239133312401334 case is_valid {
12411241- False -> pieces
12421242- True -> [position, ..pieces]
13351335+ False -> moves
13361336+ True ->
13371337+ case
13381338+ list.find(legal_moves, fn(move) {
13391339+ let valid = move.to == to && move.from == position
13401340+ case move, promotion {
13411341+ Promotion(piece:, ..), Some(promotion) if piece == promotion ->
13421342+ valid
13431343+ Promotion(..), _ -> False
13441344+ _, _ -> valid
13451345+ }
13461346+ })
13471347+ {
13481348+ Error(_) -> moves
13491349+ Ok(move) -> [move, ..moves]
13501350+ }
12431351 }
12441352}
12451353
+3-9
src/starfish/internal/search.gleam
···408408/// order than random. Searching better moves first improves alpha-beta pruning,
409409/// allowing us to search more positions.
410410fn guess_eval(game: Game, move: Move, phase: Int) -> Int {
411411- let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from)
412412- as "Invalid move trying to move empty piece"
411411+ let piece = move.moving_piece(move)
412412+ let colour = game.to_move
413413414414 let moving_piece = case move {
415415 move.Promotion(piece:, ..) -> piece
···422422 let position_improvement = to_score - from_score
423423424424 let move_specific_score = case move {
425425- // TODO store information in moves so we don't have to retrieve it from the
426426- // board every time.
427427- move.Capture(..) -> {
428428- let assert board.Occupied(piece: captured_piece, colour: _) =
429429- board.get(game.board, move.to)
430430- as "Invalid capture moving to empty square"
431431-425425+ move.Capture(captured_piece:, ..) -> {
432426 capture_promotion_bonus
433427 // Capturing a more valuable piece is better, and using a less valuable
434428 // piece to capture is usually better. However, we prioritise the value of
+78-59
test/starfish_test.gleam
···11import gleam/int
22import gleam/io
33import gleam/list
44+import gleam/option.{None, Some}
45import gleeunit
56import pocket_watch
67import starfish
···1213 gleeunit.main()
1314}
14151515-/// Compare the state of two games, ignoring additional fields
1616-fn game_equal(a: game.Game, b: game.Game) -> Bool {
1717- a.board == b.board
1818- && a.to_move == b.to_move
1919- && a.castling == b.castling
2020- && a.en_passant_square == b.en_passant_square
2121- && a.half_moves == b.half_moves
2222- && a.full_moves == b.full_moves
2323-}
2424-2516pub fn from_fen_test() {
2617 let initial = starfish.new()
2718 let parsed = starfish.from_fen(starfish.starting_fen)
2828- assert game_equal(initial, parsed)
1919+ assert initial == parsed
29203021 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
3122 let parsed = starfish.from_fen(initial_with_only_position)
3232- assert game_equal(initial, parsed)
2323+ assert initial == parsed
3324}
34253526pub fn try_from_fen_test() {
3627 let initial = starfish.new()
3728 let assert Ok(parsed) = starfish.try_from_fen(starfish.starting_fen)
3838- assert game_equal(parsed, initial)
2929+ assert parsed == initial
39304031 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
4132 let assert Error(error) = starfish.try_from_fen(initial_with_only_position)
···5142}
52435344pub fn to_long_algebraic_notation_test() {
5454- assert move.Move(from: 8, to: 24) |> starfish.to_long_algebraic_notation
4545+ assert move.Move(board.Pawn, from: 8, to: 24)
4646+ |> starfish.to_long_algebraic_notation
5547 == "a2a4"
5656- assert move.Move(from: 6, to: 21) |> starfish.to_long_algebraic_notation
4848+ assert move.Move(board.Pawn, from: 6, to: 21)
4949+ |> starfish.to_long_algebraic_notation
5750 == "g1f3"
5858- assert move.Move(from: 57, to: 42) |> starfish.to_long_algebraic_notation
5151+ assert move.Move(board.Pawn, from: 57, to: 42)
5252+ |> starfish.to_long_algebraic_notation
5953 == "b8c6"
6060- assert move.Move(from: 49, to: 33) |> starfish.to_long_algebraic_notation
5454+ assert move.Move(board.Pawn, from: 49, to: 33)
5555+ |> starfish.to_long_algebraic_notation
6156 == "b7b5"
6257 assert move.EnPassant(from: 32, to: 41) |> starfish.to_long_algebraic_notation
6358 == "a5b6"
···6964 == "e8g8"
7065 assert move.Castle(from: 60, to: 58) |> starfish.to_long_algebraic_notation
7166 == "e8c8"
7272- assert move.Promotion(from: 51, to: 58, piece: board.Queen)
6767+ assert move.Promotion(
6868+ from: 51,
6969+ to: 58,
7070+ piece: board.Queen,
7171+ captured_piece: None,
7272+ )
7373 |> starfish.to_long_algebraic_notation
7474 == "d7c8q"
7575- assert move.Promotion(from: 11, to: 2, piece: board.Knight)
7575+ assert move.Promotion(
7676+ from: 11,
7777+ to: 2,
7878+ piece: board.Knight,
7979+ captured_piece: Some(board.Rook),
8080+ )
7681 |> starfish.to_long_algebraic_notation
7782 == "d2c1n"
7878- assert move.Capture(from: 49, to: 7) |> starfish.to_long_algebraic_notation
8383+ assert move.Capture(board.Bishop, from: 49, to: 7, captured_piece: board.Pawn)
8484+ |> starfish.to_long_algebraic_notation
7985 == "b7h1"
8086}
81878288pub fn parse_long_algebraic_notation_test() {
8389 let assert Ok(move) = starfish.parse_move("a2a4", starfish.new())
8484- assert move == move.Move(from: 8, to: 24)
9090+ assert move == move.Move(board.Pawn, from: 8, to: 24)
8591 let assert Ok(move) = starfish.parse_move("g1f3", starfish.new())
8686- assert move == move.Move(from: 6, to: 21)
9292+ assert move == move.Move(board.Knight, from: 6, to: 21)
8793 let assert Ok(move) =
8894 starfish.parse_move(
8995 "b8c6",
···9197 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
9298 ),
9399 )
9494- assert move == move.Move(from: 57, to: 42)
100100+ assert move == move.Move(board.Knight, from: 57, to: 42)
95101 let assert Ok(move) =
96102 starfish.parse_move(
97103 "B7b5",
···99105 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
100106 ),
101107 )
102102- assert move == move.Move(from: 49, to: 33)
108108+ assert move == move.Move(board.Pawn, from: 49, to: 33)
103109 let assert Ok(move) =
104110 starfish.parse_move(
105111 "a5b6",
···147153 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
148154 ),
149155 )
150150- assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
156156+ assert move
157157+ == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)
151158 let assert Ok(move) =
152159 starfish.parse_move(
153160 "d2c1N",
···155162 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
156163 ),
157164 )
158158- assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
165165+ assert move
166166+ == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)
159167 let assert Ok(move) =
160168 starfish.parse_move(
161169 "b7h1",
···163171 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
164172 ),
165173 )
166166- assert move == move.Capture(from: 49, to: 7)
174174+ assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7)
167175168176 let assert Error(Nil) = starfish.parse_move("abcd", starfish.new())
169177 let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new())
···172180173181pub fn parse_standard_algebraic_notation_test() {
174182 let assert Ok(move) = starfish.parse_move("a4", starfish.new())
175175- assert move == move.Move(from: 8, to: 24)
183183+ assert move == move.Move(board.Pawn, from: 8, to: 24)
176184 let assert Ok(move) = starfish.parse_move("Nf3", starfish.new())
177177- assert move == move.Move(from: 6, to: 21)
185185+ assert move == move.Move(board.Knight, from: 6, to: 21)
178186 let assert Ok(move) =
179187 starfish.parse_move(
180188 "Nc6",
···182190 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
183191 ),
184192 )
185185- assert move == move.Move(from: 57, to: 42)
193193+ assert move == move.Move(board.Knight, from: 57, to: 42)
186194 let assert Ok(move) =
187195 starfish.parse_move(
188196 "b5",
···190198 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
191199 ),
192200 )
193193- assert move == move.Move(from: 49, to: 33)
201201+ assert move == move.Move(board.Pawn, from: 49, to: 33)
194202 let assert Ok(move) =
195203 starfish.parse_move(
196204 "axb6",
···238246 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
239247 ),
240248 )
241241- assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
249249+ assert move
250250+ == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)
242251 let assert Ok(move) =
243252 starfish.parse_move(
244253 "c1=N",
···246255 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
247256 ),
248257 )
249249- assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
258258+ assert move
259259+ == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)
250260 let assert Ok(move) =
251261 starfish.parse_move(
252262 "Bxh1",
···254264 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
255265 ),
256266 )
257257- assert move == move.Capture(from: 49, to: 7)
267267+ assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7)
258268 let assert Ok(move) =
259269 starfish.parse_move(
260270 "Rac4",
261271 starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
262272 )
263263- assert move == move.Move(from: 24, to: 26)
273273+ assert move == move.Move(board.Rook, from: 24, to: 26)
264274265275 let assert Ok(move) =
266276 starfish.parse_move(
267277 "R7c6",
268278 starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
269279 )
270270- assert move == move.Move(from: 50, to: 42)
280280+ assert move == move.Move(board.Rook, from: 50, to: 42)
271281272282 let assert Error(Nil) = starfish.parse_move("e2", starfish.new())
273283 let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new())
···276286277287pub fn to_standard_algebraic_notation_test() {
278288 assert starfish.to_standard_algebraic_notation(
279279- move.Move(from: 8, to: 24),
289289+ move.Move(board.Pawn, from: 8, to: 24),
280290 starfish.new(),
281291 )
282292 == "a4"
283293 assert starfish.to_standard_algebraic_notation(
284284- move.Move(from: 6, to: 21),
294294+ move.Move(board.Knight, from: 6, to: 21),
285295 starfish.new(),
286296 )
287297 == "Nf3"
288298 assert starfish.to_standard_algebraic_notation(
289289- move.Move(from: 57, to: 42),
299299+ move.Move(board.Knight, from: 57, to: 42),
290300 starfish.from_fen(
291301 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
292302 ),
···294304 == "Nc6"
295305296306 assert starfish.to_standard_algebraic_notation(
297297- move.Move(from: 49, to: 33),
307307+ move.Move(board.Pawn, from: 49, to: 33),
298308 starfish.from_fen(
299309 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
300310 ),
···342352 == "O-O-O"
343353344354 assert starfish.to_standard_algebraic_notation(
345345- move.Promotion(from: 51, to: 58, piece: board.Queen),
355355+ move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen),
346356 starfish.from_fen(
347357 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
348358 ),
···350360 == "dxc8=Q"
351361352362 assert starfish.to_standard_algebraic_notation(
353353- move.Promotion(from: 11, to: 2, piece: board.Knight),
363363+ move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight),
354364 starfish.from_fen(
355365 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
356366 ),
···358368 == "dxc1=N"
359369360370 assert starfish.to_standard_algebraic_notation(
361361- move.Capture(from: 49, to: 7),
371371+ move.Capture(board.Bishop, board.Rook, from: 49, to: 7),
362372 starfish.from_fen(
363373 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
364374 ),
···366376 == "Bxh1"
367377368378 assert starfish.to_standard_algebraic_notation(
369369- move.Move(from: 24, to: 26),
379379+ move.Move(board.Rook, from: 24, to: 26),
370380 starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
371381 )
372382 == "Rac4"
373383374384 assert starfish.to_standard_algebraic_notation(
375375- move.Move(from: 50, to: 42),
385385+ move.Move(board.Rook, from: 50, to: 42),
376386 starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
377387 )
378388 == "R7c6"
379389380390 assert starfish.to_standard_algebraic_notation(
381381- move.Capture(from: 31, to: 13),
391391+ move.Capture(board.Queen, board.Bishop, from: 31, to: 13),
382392 starfish.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"),
383393 )
384394 == "Qh4xf2"
···501511 until: starfish.Depth(5),
502512 )
503513 // b4f4
504504- assert move == move.Capture(from: 25, to: 29)
514514+ assert move == move.Capture(board.Rook, board.Pawn, from: 25, to: 29)
505515506516 let assert Ok(move) =
507517 starfish.search(
508518 starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"),
509519 until: starfish.Depth(10),
510520 )
511511- assert move == move.Move(from: 29, to: 31)
521521+ assert move == move.Move(board.Rook, from: 29, to: 31)
512522}
513523514524pub fn perft_initial_position_test_() {
···713723 moves: List(move.Move),
714724 expected_fen: String,
715725) {
716716- let final_fen =
726726+ let game =
717727 starting_fen
718718- |> game.from_fen
728728+ |> starfish.from_fen
719729 |> list.fold(moves, _, starfish.apply_move)
720720- |> game.to_fen
721730722722- assert final_fen == expected_fen
731731+ let game = game.Game(..game, previous_positions: [])
732732+733733+ let expected_game = starfish.from_fen(expected_fen)
734734+735735+ assert game == expected_game
723736}
724737725738pub fn apply_move_test() {
726739 test_apply_move(
727740 starfish.starting_fen,
728741 // a2a4
729729- [move.Move(from: 8, to: 24)],
742742+ [move.Move(board.Pawn, from: 8, to: 24)],
730743 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq a3 0 1",
731744 )
732745733746 test_apply_move(
734747 starfish.starting_fen,
735748 // g1f3
736736- [move.Move(from: 6, to: 21)],
749749+ [move.Move(board.Knight, from: 6, to: 21)],
737750 "rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1",
738751 )
739752740753 test_apply_move(
741754 starfish.starting_fen,
742755 // a2a4, b8c6
743743- [move.Move(from: 8, to: 24), move.Move(from: 57, to: 42)],
756756+ [
757757+ move.Move(board.Pawn, from: 8, to: 24),
758758+ move.Move(board.Knight, from: 57, to: 42),
759759+ ],
744760 "r1bqkbnr/pppppppp/2n5/8/P7/8/1PPPPPPP/RNBQKBNR w KQkq - 1 2",
745761 )
746762747763 test_apply_move(
748764 starfish.starting_fen,
749765 // a2a4, b7b5
750750- [move.Move(from: 8, to: 24), move.Move(from: 49, to: 33)],
766766+ [
767767+ move.Move(board.Pawn, from: 8, to: 24),
768768+ move.Move(board.Pawn, from: 49, to: 33),
769769+ ],
751770 "rnbqkbnr/p1pppppp/8/1p6/P7/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 2",
752771 )
753772···789808 test_apply_move(
790809 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
791810 // d7c8q
792792- [move.Promotion(from: 51, to: 58, piece: board.Queen)],
811811+ [move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)],
793812 "rnQq1bnr/ppp1kpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR b KQ - 0 5",
794813 )
795814796815 test_apply_move(
797816 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
798817 // d2c1n
799799- [move.Promotion(from: 11, to: 2, piece: board.Knight)],
818818+ [move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)],
800819 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPP1KPP1/RNnQ1BNR w kq - 0 6",
801820 )
802821···804823 test_apply_move(
805824 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
806825 // b7h1
807807- [move.Capture(from: 49, to: 7)],
826826+ [move.Capture(board.Bishop, board.Rook, from: 49, to: 7)],
808827 "rn1qkbnr/p1pppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNb w Qkq - 0 4",
809828 )
810829}