A chess library for Gleam

Store more information in moves to make move.apply and search easier

+296 -175
+215 -107
src/starfish/internal/move.gleam
··· 13 13 14 14 pub type Move { 15 15 Castle(from: Int, to: Int) 16 - Move(from: Int, to: Int) 17 - Capture(from: Int, to: Int) 16 + Move(from: Int, to: Int, piece: board.Piece) 17 + Capture(from: Int, to: Int, piece: board.Piece, captured_piece: board.Piece) 18 18 EnPassant(from: Int, to: Int) 19 - Promotion(from: Int, to: Int, piece: board.Piece) 19 + Promotion( 20 + from: Int, 21 + to: Int, 22 + piece: board.Piece, 23 + captured_piece: Option(board.Piece), 24 + ) 25 + } 26 + 27 + pub fn moving_piece(move: Move) -> board.Piece { 28 + case move { 29 + Capture(piece:, ..) | Move(piece:, ..) -> piece 30 + Castle(..) -> board.King 31 + EnPassant(..) | Promotion(..) -> board.Pawn 32 + } 20 33 } 21 34 22 35 pub fn legal(game: Game) -> List(Move) { ··· 83 96 ) -> List(Move) { 84 97 case piece { 85 98 board.Bishop -> 86 - sliding_moves(game, position, moves, direction.bishop_directions) 99 + sliding_moves(game, piece, position, moves, direction.bishop_directions) 87 100 board.Rook -> 88 - sliding_moves(game, position, moves, direction.rook_directions) 101 + sliding_moves(game, piece, position, moves, direction.rook_directions) 89 102 board.Queen -> 90 - sliding_moves(game, position, moves, direction.queen_directions) 103 + sliding_moves(game, piece, position, moves, direction.queen_directions) 91 104 board.King -> king_moves(game, position, moves, direction.queen_directions) 92 105 board.Knight -> 93 106 knight_moves(game, position, moves, direction.knight_directions) ··· 120 133 { 121 134 False -> moves 122 135 True if is_promotion -> 123 - add_promotions(position, forward_one, moves, board.pawn_promotions) 124 - True -> [Move(from: position, to: forward_one), ..moves] 136 + add_promotions( 137 + position, 138 + forward_one, 139 + None, 140 + moves, 141 + board.pawn_promotions, 142 + ) 143 + True -> [Move(board.Pawn, from: position, to: forward_one), ..moves] 125 144 } 126 145 127 146 let can_double_move = case game.to_move, position / 8 { ··· 136 155 board.Empty -> 137 156 case can_move(position, forward_two, game.attack_information) { 138 157 False -> moves 139 - True -> [Move(from: position, to: forward_two), ..moves] 158 + True -> [Move(board.Pawn, from: position, to: forward_two), ..moves] 140 159 } 141 160 board.Occupied(_, _) | board.OffBoard -> moves 142 161 } ··· 146 165 147 166 let new_position = direction.in_direction(position, left) 148 167 let moves = case board.get(game.board, new_position) { 149 - board.Occupied(colour:, ..) if colour != game.to_move -> 168 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 150 169 case can_move(position, new_position, game.attack_information) { 151 170 False -> moves 152 171 True if is_promotion -> 153 - add_promotions(position, new_position, moves, board.pawn_promotions) 154 - True -> [Capture(from: position, to: new_position), ..moves] 172 + add_promotions( 173 + position, 174 + new_position, 175 + Some(captured_piece), 176 + moves, 177 + board.pawn_promotions, 178 + ) 179 + True -> [ 180 + Capture(board.Pawn, from: position, to: new_position, captured_piece:), 181 + ..moves 182 + ] 155 183 } 156 184 board.Empty if game.en_passant_square == Some(new_position) -> 157 185 case en_passant_is_valid(game, position, new_position) { ··· 163 191 164 192 let new_position = direction.in_direction(position, right) 165 193 case board.get(game.board, new_position) { 166 - board.Occupied(colour:, ..) if colour != game.to_move -> 194 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 167 195 case can_move(position, new_position, game.attack_information) { 168 196 False -> moves 169 197 True if is_promotion -> 170 - add_promotions(position, new_position, moves, board.pawn_promotions) 171 - True -> [Capture(from: position, to: new_position), ..moves] 198 + add_promotions( 199 + position, 200 + new_position, 201 + Some(captured_piece), 202 + moves, 203 + board.pawn_promotions, 204 + ) 205 + True -> [ 206 + Capture(board.Pawn, from: position, to: new_position, captured_piece:), 207 + ..moves 208 + ] 172 209 } 173 210 board.Empty if game.en_passant_square == Some(new_position) -> 174 211 case en_passant_is_valid(game, position, new_position) { ··· 288 325 fn add_promotions( 289 326 from: Int, 290 327 to: Int, 328 + captured_piece: Option(board.Piece), 291 329 moves: List(Move), 292 330 pieces: List(board.Piece), 293 331 ) -> List(Move) { 294 332 case pieces { 295 333 [] -> moves 296 334 [piece, ..pieces] -> 297 - add_promotions(from, to, [Promotion(from:, to:, piece:), ..moves], pieces) 335 + add_promotions( 336 + from, 337 + to, 338 + captured_piece, 339 + [Promotion(from:, to:, piece:, captured_piece:), ..moves], 340 + pieces, 341 + ) 298 342 } 299 343 } 300 344 ··· 312 356 board.Empty -> 313 357 case can_move(position, new_position, game.attack_information) { 314 358 False -> moves 315 - True -> [Move(from: position, to: new_position), ..moves] 359 + True -> [ 360 + Move(board.Knight, from: position, to: new_position), 361 + ..moves 362 + ] 316 363 } 317 - board.Occupied(colour:, ..) if colour != game.to_move -> 364 + board.Occupied(colour:, piece: captured_piece) 365 + if colour != game.to_move 366 + -> 318 367 case can_move(position, new_position, game.attack_information) { 319 368 False -> moves 320 - True -> [Capture(from: position, to: new_position), ..moves] 369 + True -> [ 370 + Capture( 371 + board.Knight, 372 + from: position, 373 + to: new_position, 374 + captured_piece:, 375 + ), 376 + ..moves 377 + ] 321 378 } 322 379 board.Occupied(_, _) | board.OffBoard -> moves 323 380 } ··· 388 445 board.Empty -> 389 446 case king_can_move(new_position, game.attack_information) { 390 447 False -> moves 391 - True -> [Move(from: position, to: new_position), ..moves] 448 + True -> [ 449 + Move(board.King, from: position, to: new_position), 450 + ..moves 451 + ] 392 452 } 393 - board.Occupied(colour:, ..) if colour != game.to_move -> 453 + board.Occupied(colour:, piece: captured_piece) 454 + if colour != game.to_move 455 + -> 394 456 case king_can_move(new_position, game.attack_information) { 395 457 False -> moves 396 - True -> [Capture(from: position, to: new_position), ..moves] 458 + True -> [ 459 + Capture( 460 + board.King, 461 + from: position, 462 + to: new_position, 463 + captured_piece:, 464 + ), 465 + ..moves 466 + ] 397 467 } 398 468 board.Occupied(_, _) | board.OffBoard -> moves 399 469 } ··· 405 475 406 476 fn sliding_moves( 407 477 game: Game, 478 + piece: board.Piece, 408 479 position: Int, 409 480 moves: List(Move), 410 481 directions: List(Direction), ··· 414 485 [direction, ..directions] -> 415 486 sliding_moves( 416 487 game, 488 + piece, 417 489 position, 418 - sliding_moves_in_direction(game, position, position, direction, moves), 490 + sliding_moves_in_direction( 491 + game, 492 + piece, 493 + position, 494 + position, 495 + direction, 496 + moves, 497 + ), 419 498 directions, 420 499 ) 421 500 } ··· 423 502 424 503 fn sliding_moves_in_direction( 425 504 game: Game, 505 + piece: board.Piece, 426 506 start_position: Int, 427 507 position: Int, 428 508 direction: Direction, ··· 433 513 board.Empty -> 434 514 sliding_moves_in_direction( 435 515 game, 516 + piece, 436 517 start_position, 437 518 new_position, 438 519 direction, 439 520 case can_move(start_position, new_position, game.attack_information) { 440 521 False -> moves 441 - True -> [Move(from: start_position, to: new_position), ..moves] 522 + True -> [Move(piece, from: start_position, to: new_position), ..moves] 442 523 }, 443 524 ) 444 - board.Occupied(colour:, ..) if colour != game.to_move -> 525 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 445 526 case can_move(start_position, new_position, game.attack_information) { 446 527 False -> moves 447 - True -> [Capture(from: start_position, to: new_position), ..moves] 528 + True -> [ 529 + Capture( 530 + piece, 531 + from: start_position, 532 + to: new_position, 533 + captured_piece:, 534 + ), 535 + ..moves 536 + ] 448 537 } 449 538 board.Occupied(_, _) | board.OffBoard -> moves 450 539 } ··· 452 541 453 542 pub fn apply(game: Game, move: Move) -> game.Game { 454 543 case move { 455 - Capture(from:, to:) -> do_apply(game, from, to, False, None, True) 544 + Capture(from:, to:, piece:, captured_piece:) -> 545 + do_apply(game, piece, from, to, False, None, Some(captured_piece)) 456 546 Castle(from:, to:) -> apply_castle(game, from, to, to % 8 == 2) 457 - EnPassant(from:, to:) -> do_apply(game, from, to, True, None, True) 458 - Move(from:, to:) -> do_apply(game, from, to, False, None, False) 459 - Promotion(from:, to:, piece:) -> 460 - do_apply(game, from, to, False, Some(piece), False) 547 + EnPassant(from:, to:) -> 548 + do_apply(game, board.Pawn, from, to, True, None, None) 549 + Move(from:, to:, piece:) -> 550 + do_apply(game, piece, from, to, False, None, None) 551 + Promotion(from:, to:, piece:, captured_piece:) -> 552 + do_apply(game, board.Pawn, from, to, False, Some(piece), captured_piece) 461 553 } 462 554 } 463 555 ··· 597 689 598 690 fn do_apply( 599 691 game: Game, 692 + piece: board.Piece, 600 693 from: Int, 601 694 to: Int, 602 695 en_passant: Bool, 603 696 promotion: Option(board.Piece), 604 - capture: Bool, 697 + captured_piece: Option(board.Piece), 605 698 ) -> Game { 606 699 let Game( 607 700 board:, ··· 635 728 board.White -> #(white_pieces, black_pieces) 636 729 } 637 730 638 - let assert board.Occupied(piece:, colour:) = board.get(board, from) 639 - as "Tried to apply move from invalid position" 731 + let our_colour = to_move 732 + let enemy_colour = case to_move { 733 + board.Black -> board.White 734 + board.White -> board.Black 735 + } 640 736 641 737 let castling = 642 738 castling 643 739 |> remove_castling(from) 644 740 |> remove_castling(to) 645 741 646 - let one_way_move = capture || piece == board.Pawn 742 + let one_way_move = captured_piece != None || piece == board.Pawn 647 743 648 744 let zobrist_hash = 649 745 previous_hash 650 746 |> hash.toggle_to_move 651 - |> hash.toggle_piece(from, piece, colour) 747 + |> hash.toggle_piece(from, piece, our_colour) 652 748 653 749 let phase = 654 750 game.phase(white_pieces.non_pawn_material, black_pieces.non_pawn_material) 655 751 656 752 let our_piece_square_score = 657 - our_piece_square_score - piece_table.piece_score(piece, colour, from, phase) 753 + our_piece_square_score 754 + - piece_table.piece_score(piece, our_colour, from, phase) 658 755 659 756 let #(piece, our_pawn_material, our_non_pawn_material) = case promotion { 660 757 None -> #(piece, our_pawn_material, our_non_pawn_material) ··· 666 763 } 667 764 668 765 let our_piece_square_score = 669 - our_piece_square_score + piece_table.piece_score(piece, colour, to, phase) 766 + our_piece_square_score 767 + + piece_table.piece_score(piece, our_colour, to, phase) 670 768 671 - let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, colour) 769 + let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, our_colour) 672 770 673 771 let #( 674 772 zobrist_hash, 675 773 opposing_pawn_material, 676 774 opposing_non_pawn_material, 677 775 opposing_piece_square_score, 678 - ) = case board.get(board, to) { 679 - board.Occupied(piece: board.Pawn, colour:) -> #( 680 - hash.toggle_piece(zobrist_hash, to, board.Pawn, colour), 776 + ) = case captured_piece { 777 + Some(board.Pawn) -> #( 778 + hash.toggle_piece(zobrist_hash, to, board.Pawn, enemy_colour), 681 779 opposing_pawn_material - board.pawn_value, 682 780 opposing_non_pawn_material, 683 781 opposing_piece_square_score 684 - - piece_table.piece_score(board.Pawn, colour, to, phase), 782 + - piece_table.piece_score(board.Pawn, enemy_colour, to, phase), 685 783 ) 686 - board.Occupied(piece:, colour:) -> #( 687 - hash.toggle_piece(zobrist_hash, to, piece, colour), 784 + Some(piece) -> #( 785 + hash.toggle_piece(zobrist_hash, to, piece, enemy_colour), 688 786 opposing_pawn_material, 689 787 opposing_non_pawn_material - board.piece_value(piece), 690 788 opposing_piece_square_score 691 - - piece_table.piece_score(piece, colour, to, phase), 789 + - piece_table.piece_score(piece, enemy_colour, to, phase), 692 790 ) 693 - board.Empty | board.OffBoard -> #( 791 + None -> #( 694 792 zobrist_hash, 695 793 opposing_pawn_material, 696 794 opposing_non_pawn_material, ··· 701 799 let board = 702 800 board 703 801 |> dict.delete(from) 704 - |> dict.insert(to, #(piece, colour)) 802 + |> dict.insert(to, #(piece, our_colour)) 705 803 706 - let #(board, zobrist_hash) = case en_passant, en_passant_square, colour { 804 + let #( 805 + board, 806 + zobrist_hash, 807 + opposing_pawn_material, 808 + opposing_piece_square_score, 809 + ) = case en_passant, en_passant_square, our_colour { 707 810 True, Some(square), board.White -> { 708 811 let ep_square = square - 8 709 812 #( 710 813 dict.delete(board, ep_square), 711 814 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.Black), 815 + opposing_pawn_material - board.pawn_value, 816 + opposing_piece_square_score 817 + - piece_table.piece_score(board.Pawn, board.Black, ep_square, phase), 712 818 ) 713 819 } 714 820 True, Some(square), board.Black -> { ··· 716 822 #( 717 823 dict.delete(board, ep_square), 718 824 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.White), 825 + opposing_pawn_material - board.pawn_value, 826 + opposing_piece_square_score 827 + - piece_table.piece_score(board.Pawn, board.White, ep_square, phase), 719 828 ) 720 829 } 721 - _, _, _ -> #(board, zobrist_hash) 830 + _, _, _ -> #( 831 + board, 832 + zobrist_hash, 833 + opposing_pawn_material, 834 + opposing_piece_square_score, 835 + ) 722 836 } 723 837 724 838 let en_passant_square = case piece, to - from { ··· 758 872 board.Black -> #(opposing_pieces, our_pieces) 759 873 } 760 874 761 - let to_move = case to_move { 762 - board.Black -> board.White 763 - board.White -> board.Black 764 - } 875 + let to_move = enemy_colour 765 876 766 877 let #(half_moves, previous_positions) = case one_way_move { 767 878 True -> #(0, []) ··· 823 934 } 824 935 825 936 pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String { 826 - let assert board.Occupied(piece:, colour: _) = 827 - board.get(game.board, move.from) 828 - as "Legal moves should only move valid pieces" 937 + let piece = moving_piece(move) 829 938 830 939 case move { 831 940 Castle(from: _, to:) -> { ··· 835 944 True -> "O-O-O" 836 945 } 837 946 } 838 - Capture(from:, to:) if piece == board.Pawn -> 947 + Capture(from:, to:, ..) if piece == board.Pawn -> 839 948 pawn_move_to_san(from, to, True, None) 840 949 EnPassant(from:, to:) -> pawn_move_to_san(from, to, True, None) 841 - Promotion(from:, to:, piece:) -> { 842 - let is_capture = case board.get(game.board, move.to) { 843 - board.Occupied(..) -> True 844 - board.Empty | board.OffBoard -> False 845 - } 846 - pawn_move_to_san(from, to, is_capture, Some(piece)) 847 - } 848 - Move(from:, to:) -> move_to_san(game, piece, from, to, False) 849 - Capture(from:, to:) -> move_to_san(game, piece, from, to, True) 950 + Promotion(from:, to:, piece:, captured_piece: None) -> 951 + pawn_move_to_san(from, to, False, Some(piece)) 952 + Promotion(from:, to:, piece:, captured_piece: Some(_)) -> 953 + pawn_move_to_san(from, to, True, Some(piece)) 954 + Move(from:, to:, ..) -> move_to_san(game, piece, from, to, False) 955 + Capture(from:, to:, ..) -> move_to_san(game, piece, from, to, True) 850 956 } 851 957 } 852 958 ··· 886 992 use <- bool.guard(move.to != to, disambiguation) 887 993 use <- bool.guard(move.from == from, disambiguation) 888 994 889 - let assert board.Occupied(piece: moving_piece, colour: _) = 890 - board.get(game.board, move.from) 891 - as "Legal moves should only move valid pieces" 995 + let moving_piece = moving_piece(move) 892 996 893 997 use <- bool.guard(moving_piece != piece, disambiguation) 894 998 ··· 1019 1123 use #(first, move) <- result.try(parse_move_part(move)) 1020 1124 use #(second, move) <- result.try(parse_move_part(move)) 1021 1125 1022 - use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try( 1126 + use #(from_file, from_rank, to_file, to_rank, move) <- result.try( 1023 1127 case first, second { 1024 1128 // `xx` is not an allowed move 1025 1129 CaptureSpecifier, CaptureSpecifier -> Error(Nil) ··· 1027 1131 File(file), CaptureSpecifier -> { 1028 1132 let from_file = Some(file) 1029 1133 let from_rank = None 1030 - let capture = True 1031 1134 use #(to_file, move) <- result.try(parse_file(move)) 1032 1135 use #(to_rank, move) <- result.try(parse_rank(move)) 1033 1136 1034 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1137 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1035 1138 } 1036 1139 // We disambiguate the rank and it's a capture (e.g. `R5xc4`) 1037 1140 Rank(rank), CaptureSpecifier -> { 1038 1141 let from_file = None 1039 1142 let from_rank = Some(rank) 1040 - let capture = True 1041 1143 use #(to_file, move) <- result.try(parse_file(move)) 1042 1144 use #(to_rank, move) <- result.try(parse_rank(move)) 1043 1145 1044 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1146 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1045 1147 } 1046 1148 // It's a capture, and we've parsed the file of the destination (e.g. 1047 1149 // `Bxa5`) 1048 1150 CaptureSpecifier, File(to_file) -> { 1049 1151 let from_file = None 1050 1152 let from_rank = None 1051 - let capture = True 1052 1153 use #(to_rank, move) <- result.try(parse_rank(move)) 1053 1154 1054 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1155 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1055 1156 } 1056 1157 // We disambiguate the file and we've parsed the file of the destination 1057 1158 // (e.g. `Qhd4`) 1058 1159 File(from_file), File(to_file) -> { 1059 1160 let from_file = Some(from_file) 1060 1161 let from_rank = None 1061 - let capture = False 1062 1162 use #(to_rank, move) <- result.try(parse_rank(move)) 1063 1163 1064 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1164 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1065 1165 } 1066 1166 // We disambiguate the rank and we've parsed the file of the destination 1067 1167 // (e.g. `R7d2`) 1068 1168 Rank(rank), File(to_file) -> { 1069 1169 let from_file = None 1070 1170 let from_rank = Some(rank) 1071 - let capture = False 1072 1171 use #(to_rank, move) <- result.try(parse_rank(move)) 1073 1172 1074 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1173 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1075 1174 } 1076 1175 // Capture followed by a rank is not allowed, e.g. `Rx1` 1077 1176 CaptureSpecifier, Rank(_) -> Error(Nil) 1078 1177 // We've parsed the file and rank, and there's no more move to parse, 1079 1178 // so we're done. (e.g. `Nf3`) 1080 1179 File(file), Rank(rank) if move == "" -> 1081 - Ok(#(None, None, False, file, rank, move)) 1180 + Ok(#(None, None, file, rank, move)) 1082 1181 // We've disambiguated the rank and file, and we still need to parse 1083 1182 // the rest of the move. (e.g. `Qh4xe1`) 1084 1183 File(from_file), Rank(from_rank) -> ··· 1086 1185 Ok(#(CaptureSpecifier, move)) -> { 1087 1186 let from_file = Some(from_file) 1088 1187 let from_rank = Some(from_rank) 1089 - let capture = True 1090 1188 use #(to_file, move) <- result.try(parse_file(move)) 1091 1189 use #(to_rank, move) <- result.try(parse_rank(move)) 1092 1190 1093 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1191 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1094 1192 } 1095 1193 Ok(#(File(to_file), _)) -> { 1096 1194 let from_file = Some(from_file) 1097 1195 let from_rank = Some(from_rank) 1098 - let capture = False 1099 1196 use #(to_rank, move) <- result.try(parse_rank(move)) 1100 1197 1101 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1198 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1102 1199 } 1103 1200 Ok(#(Rank(_), _)) | Error(_) -> Error(Nil) 1104 1201 } ··· 1111 1208 1112 1209 let to = to_rank * 8 + to_file 1113 1210 1114 - case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) { 1115 - [from] if capture -> Ok(Capture(from:, to:)) 1116 - [from] -> Ok(Move(from:, to:)) 1211 + case 1212 + get_moves(game, piece_kind, legal_moves, from_file, from_rank, to, None) 1213 + { 1214 + [move] -> Ok(move) 1117 1215 // If there is more than one valid move, the notation is ambiguous, and 1118 1216 // so we error. If there are no valid moves, we also error. 1119 1217 _ -> Error(Nil) ··· 1178 1276 ) -> Result(Move, Nil) { 1179 1277 use #(file, move) <- result.try(parse_file(move)) 1180 1278 1181 - use #(from_file, is_capture, to_file, move) <- result.try(case move { 1279 + use #(from_file, to_file, move) <- result.try(case move { 1182 1280 "x" <> move -> 1183 1281 parse_file(move) 1184 1282 |> result.map(fn(pair) { 1185 1283 let #(to_file, move) = pair 1186 - #(Some(file), True, to_file, move) 1284 + #(Some(file), to_file, move) 1187 1285 }) 1188 - _ -> Ok(#(None, False, file, move)) 1286 + _ -> Ok(#(None, file, move)) 1189 1287 }) 1190 1288 1191 1289 use #(rank, move) <- result.try(parse_rank(move)) ··· 1202 1300 let to = rank * 8 + to_file 1203 1301 1204 1302 case 1205 - get_pieces(game, board.Pawn, legal_moves, from_file, None, to), 1206 - promotion 1303 + get_moves(game, board.Pawn, legal_moves, from_file, None, to, promotion) 1207 1304 { 1208 - [from], Some(piece) -> Ok(Promotion(from:, to:, piece:)) 1209 - [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:)) 1210 - [from], _ if is_capture -> Ok(Capture(from:, to:)) 1211 - [from], _ -> Ok(Move(from:, to:)) 1212 - _, _ -> Error(Nil) 1305 + [move] -> Ok(move) 1306 + _ -> Error(Nil) 1213 1307 } 1214 1308 } 1215 1309 1216 - /// Gets the possible destination squares for a move, based on the information 1217 - /// we know. 1218 - fn get_pieces( 1310 + /// Gets the possible moves for a piece, based on the information we know from 1311 + /// SAN. 1312 + fn get_moves( 1219 1313 game: Game, 1220 1314 find_piece: board.Piece, 1221 1315 legal_moves: List(Move), 1222 1316 from_file: option.Option(Int), 1223 1317 from_rank: option.Option(Int), 1224 1318 to: Int, 1225 - ) -> List(Int) { 1226 - use pieces, position, #(piece, colour) <- dict.fold(game.board, []) 1319 + promotion: Option(board.Piece), 1320 + ) -> List(Move) { 1321 + use moves, position, #(piece, colour) <- dict.fold(game.board, []) 1227 1322 let is_valid = 1228 1323 colour == game.to_move 1229 1324 && piece == find_piece ··· 1235 1330 None -> True 1236 1331 Some(rank) -> rank == position / 8 1237 1332 } 1238 - && list.any(legal_moves, fn(move) { move.to == to && move.from == position }) 1239 1333 1240 1334 case is_valid { 1241 - False -> pieces 1242 - True -> [position, ..pieces] 1335 + False -> moves 1336 + True -> 1337 + case 1338 + list.find(legal_moves, fn(move) { 1339 + let valid = move.to == to && move.from == position 1340 + case move, promotion { 1341 + Promotion(piece:, ..), Some(promotion) if piece == promotion -> 1342 + valid 1343 + Promotion(..), _ -> False 1344 + _, _ -> valid 1345 + } 1346 + }) 1347 + { 1348 + Error(_) -> moves 1349 + Ok(move) -> [move, ..moves] 1350 + } 1243 1351 } 1244 1352 } 1245 1353
+3 -9
src/starfish/internal/search.gleam
··· 408 408 /// order than random. Searching better moves first improves alpha-beta pruning, 409 409 /// allowing us to search more positions. 410 410 fn guess_eval(game: Game, move: Move, phase: Int) -> Int { 411 - let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from) 412 - as "Invalid move trying to move empty piece" 411 + let piece = move.moving_piece(move) 412 + let colour = game.to_move 413 413 414 414 let moving_piece = case move { 415 415 move.Promotion(piece:, ..) -> piece ··· 422 422 let position_improvement = to_score - from_score 423 423 424 424 let move_specific_score = case move { 425 - // TODO store information in moves so we don't have to retrieve it from the 426 - // board every time. 427 - move.Capture(..) -> { 428 - let assert board.Occupied(piece: captured_piece, colour: _) = 429 - board.get(game.board, move.to) 430 - as "Invalid capture moving to empty square" 431 - 425 + move.Capture(captured_piece:, ..) -> { 432 426 capture_promotion_bonus 433 427 // Capturing a more valuable piece is better, and using a less valuable 434 428 // piece to capture is usually better. However, we prioritise the value of
+78 -59
test/starfish_test.gleam
··· 1 1 import gleam/int 2 2 import gleam/io 3 3 import gleam/list 4 + import gleam/option.{None, Some} 4 5 import gleeunit 5 6 import pocket_watch 6 7 import starfish ··· 12 13 gleeunit.main() 13 14 } 14 15 15 - /// Compare the state of two games, ignoring additional fields 16 - fn game_equal(a: game.Game, b: game.Game) -> Bool { 17 - a.board == b.board 18 - && a.to_move == b.to_move 19 - && a.castling == b.castling 20 - && a.en_passant_square == b.en_passant_square 21 - && a.half_moves == b.half_moves 22 - && a.full_moves == b.full_moves 23 - } 24 - 25 16 pub fn from_fen_test() { 26 17 let initial = starfish.new() 27 18 let parsed = starfish.from_fen(starfish.starting_fen) 28 - assert game_equal(initial, parsed) 19 + assert initial == parsed 29 20 30 21 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 31 22 let parsed = starfish.from_fen(initial_with_only_position) 32 - assert game_equal(initial, parsed) 23 + assert initial == parsed 33 24 } 34 25 35 26 pub fn try_from_fen_test() { 36 27 let initial = starfish.new() 37 28 let assert Ok(parsed) = starfish.try_from_fen(starfish.starting_fen) 38 - assert game_equal(parsed, initial) 29 + assert parsed == initial 39 30 40 31 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 41 32 let assert Error(error) = starfish.try_from_fen(initial_with_only_position) ··· 51 42 } 52 43 53 44 pub fn to_long_algebraic_notation_test() { 54 - assert move.Move(from: 8, to: 24) |> starfish.to_long_algebraic_notation 45 + assert move.Move(board.Pawn, from: 8, to: 24) 46 + |> starfish.to_long_algebraic_notation 55 47 == "a2a4" 56 - assert move.Move(from: 6, to: 21) |> starfish.to_long_algebraic_notation 48 + assert move.Move(board.Pawn, from: 6, to: 21) 49 + |> starfish.to_long_algebraic_notation 57 50 == "g1f3" 58 - assert move.Move(from: 57, to: 42) |> starfish.to_long_algebraic_notation 51 + assert move.Move(board.Pawn, from: 57, to: 42) 52 + |> starfish.to_long_algebraic_notation 59 53 == "b8c6" 60 - assert move.Move(from: 49, to: 33) |> starfish.to_long_algebraic_notation 54 + assert move.Move(board.Pawn, from: 49, to: 33) 55 + |> starfish.to_long_algebraic_notation 61 56 == "b7b5" 62 57 assert move.EnPassant(from: 32, to: 41) |> starfish.to_long_algebraic_notation 63 58 == "a5b6" ··· 69 64 == "e8g8" 70 65 assert move.Castle(from: 60, to: 58) |> starfish.to_long_algebraic_notation 71 66 == "e8c8" 72 - assert move.Promotion(from: 51, to: 58, piece: board.Queen) 67 + assert move.Promotion( 68 + from: 51, 69 + to: 58, 70 + piece: board.Queen, 71 + captured_piece: None, 72 + ) 73 73 |> starfish.to_long_algebraic_notation 74 74 == "d7c8q" 75 - assert move.Promotion(from: 11, to: 2, piece: board.Knight) 75 + assert move.Promotion( 76 + from: 11, 77 + to: 2, 78 + piece: board.Knight, 79 + captured_piece: Some(board.Rook), 80 + ) 76 81 |> starfish.to_long_algebraic_notation 77 82 == "d2c1n" 78 - assert move.Capture(from: 49, to: 7) |> starfish.to_long_algebraic_notation 83 + assert move.Capture(board.Bishop, from: 49, to: 7, captured_piece: board.Pawn) 84 + |> starfish.to_long_algebraic_notation 79 85 == "b7h1" 80 86 } 81 87 82 88 pub fn parse_long_algebraic_notation_test() { 83 89 let assert Ok(move) = starfish.parse_move("a2a4", starfish.new()) 84 - assert move == move.Move(from: 8, to: 24) 90 + assert move == move.Move(board.Pawn, from: 8, to: 24) 85 91 let assert Ok(move) = starfish.parse_move("g1f3", starfish.new()) 86 - assert move == move.Move(from: 6, to: 21) 92 + assert move == move.Move(board.Knight, from: 6, to: 21) 87 93 let assert Ok(move) = 88 94 starfish.parse_move( 89 95 "b8c6", ··· 91 97 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 92 98 ), 93 99 ) 94 - assert move == move.Move(from: 57, to: 42) 100 + assert move == move.Move(board.Knight, from: 57, to: 42) 95 101 let assert Ok(move) = 96 102 starfish.parse_move( 97 103 "B7b5", ··· 99 105 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 100 106 ), 101 107 ) 102 - assert move == move.Move(from: 49, to: 33) 108 + assert move == move.Move(board.Pawn, from: 49, to: 33) 103 109 let assert Ok(move) = 104 110 starfish.parse_move( 105 111 "a5b6", ··· 147 153 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 148 154 ), 149 155 ) 150 - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 156 + assert move 157 + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) 151 158 let assert Ok(move) = 152 159 starfish.parse_move( 153 160 "d2c1N", ··· 155 162 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 156 163 ), 157 164 ) 158 - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 165 + assert move 166 + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) 159 167 let assert Ok(move) = 160 168 starfish.parse_move( 161 169 "b7h1", ··· 163 171 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 164 172 ), 165 173 ) 166 - assert move == move.Capture(from: 49, to: 7) 174 + assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) 167 175 168 176 let assert Error(Nil) = starfish.parse_move("abcd", starfish.new()) 169 177 let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new()) ··· 172 180 173 181 pub fn parse_standard_algebraic_notation_test() { 174 182 let assert Ok(move) = starfish.parse_move("a4", starfish.new()) 175 - assert move == move.Move(from: 8, to: 24) 183 + assert move == move.Move(board.Pawn, from: 8, to: 24) 176 184 let assert Ok(move) = starfish.parse_move("Nf3", starfish.new()) 177 - assert move == move.Move(from: 6, to: 21) 185 + assert move == move.Move(board.Knight, from: 6, to: 21) 178 186 let assert Ok(move) = 179 187 starfish.parse_move( 180 188 "Nc6", ··· 182 190 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 183 191 ), 184 192 ) 185 - assert move == move.Move(from: 57, to: 42) 193 + assert move == move.Move(board.Knight, from: 57, to: 42) 186 194 let assert Ok(move) = 187 195 starfish.parse_move( 188 196 "b5", ··· 190 198 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 191 199 ), 192 200 ) 193 - assert move == move.Move(from: 49, to: 33) 201 + assert move == move.Move(board.Pawn, from: 49, to: 33) 194 202 let assert Ok(move) = 195 203 starfish.parse_move( 196 204 "axb6", ··· 238 246 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 239 247 ), 240 248 ) 241 - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 249 + assert move 250 + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) 242 251 let assert Ok(move) = 243 252 starfish.parse_move( 244 253 "c1=N", ··· 246 255 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 247 256 ), 248 257 ) 249 - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 258 + assert move 259 + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) 250 260 let assert Ok(move) = 251 261 starfish.parse_move( 252 262 "Bxh1", ··· 254 264 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 255 265 ), 256 266 ) 257 - assert move == move.Capture(from: 49, to: 7) 267 + assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) 258 268 let assert Ok(move) = 259 269 starfish.parse_move( 260 270 "Rac4", 261 271 starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 262 272 ) 263 - assert move == move.Move(from: 24, to: 26) 273 + assert move == move.Move(board.Rook, from: 24, to: 26) 264 274 265 275 let assert Ok(move) = 266 276 starfish.parse_move( 267 277 "R7c6", 268 278 starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 269 279 ) 270 - assert move == move.Move(from: 50, to: 42) 280 + assert move == move.Move(board.Rook, from: 50, to: 42) 271 281 272 282 let assert Error(Nil) = starfish.parse_move("e2", starfish.new()) 273 283 let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new()) ··· 276 286 277 287 pub fn to_standard_algebraic_notation_test() { 278 288 assert starfish.to_standard_algebraic_notation( 279 - move.Move(from: 8, to: 24), 289 + move.Move(board.Pawn, from: 8, to: 24), 280 290 starfish.new(), 281 291 ) 282 292 == "a4" 283 293 assert starfish.to_standard_algebraic_notation( 284 - move.Move(from: 6, to: 21), 294 + move.Move(board.Knight, from: 6, to: 21), 285 295 starfish.new(), 286 296 ) 287 297 == "Nf3" 288 298 assert starfish.to_standard_algebraic_notation( 289 - move.Move(from: 57, to: 42), 299 + move.Move(board.Knight, from: 57, to: 42), 290 300 starfish.from_fen( 291 301 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 292 302 ), ··· 294 304 == "Nc6" 295 305 296 306 assert starfish.to_standard_algebraic_notation( 297 - move.Move(from: 49, to: 33), 307 + move.Move(board.Pawn, from: 49, to: 33), 298 308 starfish.from_fen( 299 309 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 300 310 ), ··· 342 352 == "O-O-O" 343 353 344 354 assert starfish.to_standard_algebraic_notation( 345 - move.Promotion(from: 51, to: 58, piece: board.Queen), 355 + move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen), 346 356 starfish.from_fen( 347 357 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 348 358 ), ··· 350 360 == "dxc8=Q" 351 361 352 362 assert starfish.to_standard_algebraic_notation( 353 - move.Promotion(from: 11, to: 2, piece: board.Knight), 363 + move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight), 354 364 starfish.from_fen( 355 365 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 356 366 ), ··· 358 368 == "dxc1=N" 359 369 360 370 assert starfish.to_standard_algebraic_notation( 361 - move.Capture(from: 49, to: 7), 371 + move.Capture(board.Bishop, board.Rook, from: 49, to: 7), 362 372 starfish.from_fen( 363 373 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 364 374 ), ··· 366 376 == "Bxh1" 367 377 368 378 assert starfish.to_standard_algebraic_notation( 369 - move.Move(from: 24, to: 26), 379 + move.Move(board.Rook, from: 24, to: 26), 370 380 starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 371 381 ) 372 382 == "Rac4" 373 383 374 384 assert starfish.to_standard_algebraic_notation( 375 - move.Move(from: 50, to: 42), 385 + move.Move(board.Rook, from: 50, to: 42), 376 386 starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 377 387 ) 378 388 == "R7c6" 379 389 380 390 assert starfish.to_standard_algebraic_notation( 381 - move.Capture(from: 31, to: 13), 391 + move.Capture(board.Queen, board.Bishop, from: 31, to: 13), 382 392 starfish.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"), 383 393 ) 384 394 == "Qh4xf2" ··· 501 511 until: starfish.Depth(5), 502 512 ) 503 513 // b4f4 504 - assert move == move.Capture(from: 25, to: 29) 514 + assert move == move.Capture(board.Rook, board.Pawn, from: 25, to: 29) 505 515 506 516 let assert Ok(move) = 507 517 starfish.search( 508 518 starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"), 509 519 until: starfish.Depth(10), 510 520 ) 511 - assert move == move.Move(from: 29, to: 31) 521 + assert move == move.Move(board.Rook, from: 29, to: 31) 512 522 } 513 523 514 524 pub fn perft_initial_position_test_() { ··· 713 723 moves: List(move.Move), 714 724 expected_fen: String, 715 725 ) { 716 - let final_fen = 726 + let game = 717 727 starting_fen 718 - |> game.from_fen 728 + |> starfish.from_fen 719 729 |> list.fold(moves, _, starfish.apply_move) 720 - |> game.to_fen 721 730 722 - assert final_fen == expected_fen 731 + let game = game.Game(..game, previous_positions: []) 732 + 733 + let expected_game = starfish.from_fen(expected_fen) 734 + 735 + assert game == expected_game 723 736 } 724 737 725 738 pub fn apply_move_test() { 726 739 test_apply_move( 727 740 starfish.starting_fen, 728 741 // a2a4 729 - [move.Move(from: 8, to: 24)], 742 + [move.Move(board.Pawn, from: 8, to: 24)], 730 743 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq a3 0 1", 731 744 ) 732 745 733 746 test_apply_move( 734 747 starfish.starting_fen, 735 748 // g1f3 736 - [move.Move(from: 6, to: 21)], 749 + [move.Move(board.Knight, from: 6, to: 21)], 737 750 "rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1", 738 751 ) 739 752 740 753 test_apply_move( 741 754 starfish.starting_fen, 742 755 // a2a4, b8c6 743 - [move.Move(from: 8, to: 24), move.Move(from: 57, to: 42)], 756 + [ 757 + move.Move(board.Pawn, from: 8, to: 24), 758 + move.Move(board.Knight, from: 57, to: 42), 759 + ], 744 760 "r1bqkbnr/pppppppp/2n5/8/P7/8/1PPPPPPP/RNBQKBNR w KQkq - 1 2", 745 761 ) 746 762 747 763 test_apply_move( 748 764 starfish.starting_fen, 749 765 // a2a4, b7b5 750 - [move.Move(from: 8, to: 24), move.Move(from: 49, to: 33)], 766 + [ 767 + move.Move(board.Pawn, from: 8, to: 24), 768 + move.Move(board.Pawn, from: 49, to: 33), 769 + ], 751 770 "rnbqkbnr/p1pppppp/8/1p6/P7/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 2", 752 771 ) 753 772 ··· 789 808 test_apply_move( 790 809 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 791 810 // d7c8q 792 - [move.Promotion(from: 51, to: 58, piece: board.Queen)], 811 + [move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)], 793 812 "rnQq1bnr/ppp1kpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR b KQ - 0 5", 794 813 ) 795 814 796 815 test_apply_move( 797 816 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 798 817 // d2c1n 799 - [move.Promotion(from: 11, to: 2, piece: board.Knight)], 818 + [move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)], 800 819 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPP1KPP1/RNnQ1BNR w kq - 0 6", 801 820 ) 802 821 ··· 804 823 test_apply_move( 805 824 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 806 825 // b7h1 807 - [move.Capture(from: 49, to: 7)], 826 + [move.Capture(board.Bishop, board.Rook, from: 49, to: 7)], 808 827 "rn1qkbnr/p1pppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNb w Qkq - 0 4", 809 828 ) 810 829 }