A chess library for Gleam
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement parsing of standard algebraic notation

+410 -36
+10 -11
src/starfish.gleam
··· 161 161 move.to_long_algebraic_notation(move) 162 162 } 163 163 164 - /// Parses a move from long algebraic notation, in the same format as 165 - /// [`to_long_algebraic_notation`](#to_long_algebraic_notation). Returns an error 166 - /// if the syntax is invalid or the move is not legal. 167 - pub fn parse_long_algebraic_notation( 168 - string: String, 169 - game: Game, 170 - ) -> Result(Move, Nil) { 171 - move.from_long_algebraic_notation(string, game) 172 - } 173 - 164 + /// Parses a move from either long algebraic notation, in the same format as 165 + /// [`to_long_algebraic_notation`](#to_long_algebraic_notation), or from [Standard 166 + /// Algebraic Notation](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)). 167 + /// Returns an error if the syntax is invalid or the move is not legal on the 168 + /// board. 174 169 pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) { 175 - todo 170 + let legal_moves = legal_moves(game) 171 + case move.from_long_algebraic_notation(move, legal_moves) { 172 + Ok(move) -> Ok(move) 173 + Error(_) -> move.from_standard_algebraic_notation(move, game, legal_moves) 174 + } 176 175 } 177 176 178 177 pub type GameState {
+281 -3
src/starfish/internal/move.gleam
··· 659 659 // calculate legal moves for the specific square we need. 660 660 pub fn from_long_algebraic_notation( 661 661 string: String, 662 - game: Game, 662 + legal_moves: List(Move), 663 663 ) -> Result(Move, Nil) { 664 664 use #(from, string) <- result.try(board.parse_position(string)) 665 665 use #(to, string) <- result.try(board.parse_position(string)) ··· 671 671 "r" | "R" -> Ok(Some(board.Rook)) 672 672 _ -> Error(Nil) 673 673 }) 674 - 675 - let legal_moves = legal(game) 676 674 677 675 let valid_moves = 678 676 list.filter(legal_moves, fn(move) { ··· 690 688 _ -> Error(Nil) 691 689 } 692 690 } 691 + 692 + pub fn from_standard_algebraic_notation( 693 + move: String, 694 + game: Game, 695 + legal_moves: List(Move), 696 + ) -> Result(Move, Nil) { 697 + case parse_special_move(move, game, legal_moves) { 698 + Illegal -> Error(Nil) 699 + Legal(move) -> Ok(move) 700 + Invalid -> { 701 + let #(piece_kind, move) = case move { 702 + "R" <> move -> #(board.Rook, move) 703 + "B" <> move -> #(board.Bishop, move) 704 + "N" <> move -> #(board.Knight, move) 705 + "K" <> move -> #(board.King, move) 706 + "Q" <> move -> #(board.Queen, move) 707 + _ -> #(board.Pawn, move) 708 + } 709 + 710 + // Pawn moves have entirely different syntax than non-pawn moves 711 + use <- bool.lazy_guard(piece_kind == board.Pawn, fn() { 712 + parse_pawn_move(move, game, legal_moves) 713 + }) 714 + 715 + // In order to determine which move syntax this is, we need to parse the 716 + // first two characters and match on what they are. 717 + use #(first, move) <- result.try(parse_move_part(move)) 718 + use #(second, move) <- result.try(parse_move_part(move)) 719 + 720 + use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try( 721 + case first, second { 722 + // `xx` is not an allowed move 723 + CaptureSpecifier, CaptureSpecifier -> Error(Nil) 724 + // We disambiguate the file and it's a capture (e.g. `Baxc6`) 725 + File(file), CaptureSpecifier -> { 726 + let from_file = Some(file) 727 + let from_rank = None 728 + let capture = True 729 + use #(to_file, move) <- result.try(parse_file(move)) 730 + use #(to_rank, move) <- result.try(parse_rank(move)) 731 + 732 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 733 + } 734 + // We disambiguate the rank and it's a capture (e.g. `R5xc4`) 735 + Rank(rank), CaptureSpecifier -> { 736 + let from_file = None 737 + let from_rank = Some(rank) 738 + let capture = True 739 + use #(to_file, move) <- result.try(parse_file(move)) 740 + use #(to_rank, move) <- result.try(parse_rank(move)) 741 + 742 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 743 + } 744 + // It's a capture, and we've parsed the file of the destination (e.g. 745 + // `Bxa5`) 746 + CaptureSpecifier, File(to_file) -> { 747 + let from_file = None 748 + let from_rank = None 749 + let capture = True 750 + use #(to_rank, move) <- result.try(parse_rank(move)) 751 + 752 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 753 + } 754 + // We disambiguate the file and we've parsed the file of the destination 755 + // (e.g. `Qhd4`) 756 + File(from_file), File(to_file) -> { 757 + let from_file = Some(from_file) 758 + let from_rank = None 759 + let capture = False 760 + use #(to_rank, move) <- result.try(parse_rank(move)) 761 + 762 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 763 + } 764 + // We disambiguate the rank and we've parsed the file of the destination 765 + // (e.g. `R7d2`) 766 + Rank(rank), File(to_file) -> { 767 + let from_file = None 768 + let from_rank = Some(rank) 769 + let capture = False 770 + use #(to_rank, move) <- result.try(parse_rank(move)) 771 + 772 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 773 + } 774 + // Capture followed by a rank is not allowed, e.g. `Rx1` 775 + CaptureSpecifier, Rank(_) -> Error(Nil) 776 + // We've parsed the file and rank, and there's no more move to parse, 777 + // so we're done. (e.g. `Nf3`) 778 + File(file), Rank(rank) if move == "" -> 779 + Ok(#(None, None, False, file, rank, move)) 780 + // We've disambiguated the rank and file, and we still need to parse 781 + // the rest of the move. (e.g. `Qh4xe1`) 782 + File(from_file), Rank(from_rank) -> 783 + case parse_move_part(move) { 784 + Ok(#(CaptureSpecifier, move)) -> { 785 + let from_file = Some(from_file) 786 + let from_rank = Some(from_rank) 787 + let capture = True 788 + use #(to_file, move) <- result.try(parse_file(move)) 789 + use #(to_rank, move) <- result.try(parse_rank(move)) 790 + 791 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 792 + } 793 + Ok(#(File(to_file), _)) -> { 794 + let from_file = Some(from_file) 795 + let from_rank = Some(from_rank) 796 + let capture = False 797 + use #(to_rank, move) <- result.try(parse_rank(move)) 798 + 799 + Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 800 + } 801 + Ok(#(Rank(_), _)) | Error(_) -> Error(Nil) 802 + } 803 + // Two ranks are not allowed, e.g. `R15` 804 + Rank(_), Rank(_) -> Error(Nil) 805 + }, 806 + ) 807 + 808 + use <- bool.guard(move != "", Error(Nil)) 809 + 810 + let to = board.position(file: to_file, rank: to_rank) 811 + 812 + case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) { 813 + [from] if capture -> Ok(Capture(from:, to:)) 814 + [from] -> Ok(Move(from:, to:)) 815 + // If there is more than one valid move, the notation is ambiguous, and 816 + // so we error. If there are no valid moves, we also error. 817 + _ -> Error(Nil) 818 + } 819 + } 820 + } 821 + } 822 + 823 + type MovePart { 824 + CaptureSpecifier 825 + File(Int) 826 + Rank(Int) 827 + } 828 + 829 + fn parse_move_part(move: String) -> Result(#(MovePart, String), Nil) { 830 + case move { 831 + "x" <> move -> Ok(#(CaptureSpecifier, move)) 832 + _ -> 833 + case parse_file(move) { 834 + Ok(#(file, move)) -> Ok(#(File(file), move)) 835 + Error(_) -> 836 + case parse_rank(move) { 837 + Ok(#(rank, move)) -> Ok(#(Rank(rank), move)) 838 + Error(Nil) -> Error(Nil) 839 + } 840 + } 841 + } 842 + } 843 + 844 + type SpecialMove { 845 + Invalid 846 + Illegal 847 + Legal(Move) 848 + } 849 + 850 + fn parse_special_move( 851 + move: String, 852 + game: Game, 853 + legal_moves: List(Move), 854 + ) -> SpecialMove { 855 + let move = case move { 856 + "O-O" | "0-0" if game.to_move == board.White -> Ok(Castle(4, 6)) 857 + "O-O" | "0-0" -> Ok(Castle(60, 62)) 858 + "O-O-O" | "0-0-0" if game.to_move == board.White -> Ok(Castle(4, 2)) 859 + "O-O-O" | "0-0-0" -> Ok(Castle(60, 58)) 860 + _ -> Error(Nil) 861 + } 862 + case move { 863 + Error(_) -> Invalid 864 + Ok(move) -> 865 + case list.contains(legal_moves, move) { 866 + False -> Illegal 867 + True -> Legal(move) 868 + } 869 + } 870 + } 871 + 872 + fn parse_pawn_move( 873 + move: String, 874 + game: Game, 875 + legal_moves: List(Move), 876 + ) -> Result(Move, Nil) { 877 + use #(file, move) <- result.try(parse_file(move)) 878 + 879 + use #(from_file, is_capture, to_file, move) <- result.try(case move { 880 + "x" <> move -> 881 + parse_file(move) 882 + |> result.map(fn(pair) { 883 + let #(to_file, move) = pair 884 + #(Some(file), True, to_file, move) 885 + }) 886 + _ -> Ok(#(None, False, file, move)) 887 + }) 888 + 889 + use #(rank, move) <- result.try(parse_rank(move)) 890 + 891 + use promotion <- result.try(case move { 892 + "" -> Ok(None) 893 + "n" | "N" | "=n" | "=N" -> Ok(Some(board.Knight)) 894 + "q" | "Q" | "=q" | "=Q" -> Ok(Some(board.Queen)) 895 + "b" | "B" | "=b" | "=B" -> Ok(Some(board.Bishop)) 896 + "r" | "R" | "=r" | "=R" -> Ok(Some(board.Rook)) 897 + _ -> Error(Nil) 898 + }) 899 + 900 + let to = board.position(file: to_file, rank:) 901 + 902 + case 903 + get_pieces(game, board.Pawn, legal_moves, from_file, None, to), 904 + promotion 905 + { 906 + [from], Some(piece) -> Ok(Promotion(from:, to:, piece:)) 907 + [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:)) 908 + [from], _ if is_capture -> Ok(Capture(from:, to:)) 909 + [from], _ -> Ok(Move(from:, to:)) 910 + _, _ -> Error(Nil) 911 + } 912 + } 913 + 914 + /// Gets the possible destination squares for a move, based on the information 915 + /// we know. 916 + fn get_pieces( 917 + game: Game, 918 + find_piece: board.Piece, 919 + legal_moves: List(Move), 920 + from_file: option.Option(Int), 921 + from_rank: option.Option(Int), 922 + to: Int, 923 + ) -> List(Int) { 924 + use pieces, position, #(piece, colour) <- dict.fold(game.board, []) 925 + let is_valid = 926 + colour == game.to_move 927 + && piece == find_piece 928 + && case from_file { 929 + None -> True 930 + Some(file) -> file == board.file(position) 931 + } 932 + && case from_rank { 933 + None -> True 934 + Some(rank) -> rank == board.rank(position) 935 + } 936 + && list.any(legal_moves, fn(move) { move.to == to && move.from == position }) 937 + 938 + case is_valid { 939 + False -> pieces 940 + True -> [position, ..pieces] 941 + } 942 + } 943 + 944 + fn parse_file(move: String) -> Result(#(Int, String), Nil) { 945 + case move { 946 + "a" <> move | "A" <> move -> Ok(#(0, move)) 947 + "b" <> move | "B" <> move -> Ok(#(1, move)) 948 + "c" <> move | "C" <> move -> Ok(#(2, move)) 949 + "d" <> move | "D" <> move -> Ok(#(3, move)) 950 + "e" <> move | "E" <> move -> Ok(#(4, move)) 951 + "f" <> move | "F" <> move -> Ok(#(5, move)) 952 + "g" <> move | "G" <> move -> Ok(#(6, move)) 953 + "h" <> move | "H" <> move -> Ok(#(7, move)) 954 + _ -> Error(Nil) 955 + } 956 + } 957 + 958 + fn parse_rank(move: String) -> Result(#(Int, String), Nil) { 959 + case move { 960 + "1" <> move -> Ok(#(0, move)) 961 + "2" <> move -> Ok(#(1, move)) 962 + "3" <> move -> Ok(#(2, move)) 963 + "4" <> move -> Ok(#(3, move)) 964 + "5" <> move -> Ok(#(4, move)) 965 + "6" <> move -> Ok(#(5, move)) 966 + "7" <> move -> Ok(#(6, move)) 967 + "8" <> move -> Ok(#(7, move)) 968 + _ -> Error(Nil) 969 + } 970 + }
+119 -22
test/starfish_test.gleam
··· 80 80 } 81 81 82 82 pub fn parse_long_algebraic_notation_test() { 83 - let assert Ok(move) = 84 - starfish.parse_long_algebraic_notation("a2a4", starfish.new()) 83 + let assert Ok(move) = starfish.parse_move("a2a4", starfish.new()) 85 84 assert move == move.Move(from: 8, to: 24) 86 - let assert Ok(move) = 87 - starfish.parse_long_algebraic_notation("g1f3", starfish.new()) 85 + let assert Ok(move) = starfish.parse_move("g1f3", starfish.new()) 88 86 assert move == move.Move(from: 6, to: 21) 89 87 let assert Ok(move) = 90 - starfish.parse_long_algebraic_notation( 88 + starfish.parse_move( 91 89 "b8c6", 92 90 starfish.from_fen( 93 91 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", ··· 95 93 ) 96 94 assert move == move.Move(from: 57, to: 42) 97 95 let assert Ok(move) = 98 - starfish.parse_long_algebraic_notation( 96 + starfish.parse_move( 99 97 "B7b5", 100 98 starfish.from_fen( 101 99 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", ··· 103 101 ) 104 102 assert move == move.Move(from: 49, to: 33) 105 103 let assert Ok(move) = 106 - starfish.parse_long_algebraic_notation( 104 + starfish.parse_move( 107 105 "a5b6", 108 106 starfish.from_fen( 109 107 "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", ··· 111 109 ) 112 110 assert move == move.EnPassant(from: 32, to: 41) 113 111 let assert Ok(move) = 114 - starfish.parse_long_algebraic_notation( 112 + starfish.parse_move( 115 113 "e1G1", 116 114 starfish.from_fen( 117 115 "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", ··· 119 117 ) 120 118 assert move == move.Castle(from: 4, to: 6) 121 119 let assert Ok(move) = 122 - starfish.parse_long_algebraic_notation( 120 + starfish.parse_move( 123 121 "e1c1", 124 122 starfish.from_fen( 125 123 "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", ··· 127 125 ) 128 126 assert move == move.Castle(from: 4, to: 2) 129 127 let assert Ok(move) = 130 - starfish.parse_long_algebraic_notation( 128 + starfish.parse_move( 131 129 "e8g8", 132 130 starfish.from_fen( 133 131 "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", ··· 135 133 ) 136 134 assert move == move.Castle(from: 60, to: 62) 137 135 let assert Ok(move) = 138 - starfish.parse_long_algebraic_notation( 136 + starfish.parse_move( 139 137 "E8C8", 140 138 starfish.from_fen( 141 139 "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", ··· 143 141 ) 144 142 assert move == move.Castle(from: 60, to: 58) 145 143 let assert Ok(move) = 146 - starfish.parse_long_algebraic_notation( 144 + starfish.parse_move( 147 145 "d7c8q", 148 146 starfish.from_fen( 149 147 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", ··· 151 149 ) 152 150 assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 153 151 let assert Ok(move) = 154 - starfish.parse_long_algebraic_notation( 152 + starfish.parse_move( 155 153 "d2c1N", 156 154 starfish.from_fen( 157 155 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", ··· 159 157 ) 160 158 assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 161 159 let assert Ok(move) = 162 - starfish.parse_long_algebraic_notation( 160 + starfish.parse_move( 163 161 "b7h1", 164 162 starfish.from_fen( 165 163 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", ··· 167 165 ) 168 166 assert move == move.Capture(from: 49, to: 7) 169 167 170 - let assert Error(Nil) = 171 - starfish.parse_long_algebraic_notation("abcd", starfish.new()) 172 - let assert Error(Nil) = 173 - starfish.parse_long_algebraic_notation("e2e4extra", starfish.new()) 174 - let assert Error(Nil) = 175 - starfish.parse_long_algebraic_notation("e2", starfish.new()) 176 - let assert Error(Nil) = 177 - starfish.parse_long_algebraic_notation("Bxe4", starfish.new()) 168 + let assert Error(Nil) = starfish.parse_move("abcd", starfish.new()) 169 + let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new()) 170 + let assert Error(Nil) = starfish.parse_move("a1c8", starfish.new()) 171 + } 172 + 173 + pub fn parse_standard_algebraic_notation_test() { 174 + let assert Ok(move) = starfish.parse_move("a4", starfish.new()) 175 + assert move == move.Move(from: 8, to: 24) 176 + let assert Ok(move) = starfish.parse_move("Nf3", starfish.new()) 177 + assert move == move.Move(from: 6, to: 21) 178 + let assert Ok(move) = 179 + starfish.parse_move( 180 + "Nc6", 181 + starfish.from_fen( 182 + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 183 + ), 184 + ) 185 + assert move == move.Move(from: 57, to: 42) 186 + let assert Ok(move) = 187 + starfish.parse_move( 188 + "b5", 189 + starfish.from_fen( 190 + "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 191 + ), 192 + ) 193 + assert move == move.Move(from: 49, to: 33) 194 + let assert Ok(move) = 195 + starfish.parse_move( 196 + "axb6", 197 + starfish.from_fen( 198 + "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", 199 + ), 200 + ) 201 + assert move == move.EnPassant(from: 32, to: 41) 202 + let assert Ok(move) = 203 + starfish.parse_move( 204 + "O-O", 205 + starfish.from_fen( 206 + "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", 207 + ), 208 + ) 209 + assert move == move.Castle(from: 4, to: 6) 210 + let assert Ok(move) = 211 + starfish.parse_move( 212 + "O-O-O", 213 + starfish.from_fen( 214 + "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", 215 + ), 216 + ) 217 + assert move == move.Castle(from: 4, to: 2) 218 + let assert Ok(move) = 219 + starfish.parse_move( 220 + "O-O", 221 + starfish.from_fen( 222 + "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", 223 + ), 224 + ) 225 + assert move == move.Castle(from: 60, to: 62) 226 + let assert Ok(move) = 227 + starfish.parse_move( 228 + "0-0-0", 229 + starfish.from_fen( 230 + "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", 231 + ), 232 + ) 233 + assert move == move.Castle(from: 60, to: 58) 234 + let assert Ok(move) = 235 + starfish.parse_move( 236 + "c8q", 237 + starfish.from_fen( 238 + "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 239 + ), 240 + ) 241 + assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 242 + let assert Ok(move) = 243 + starfish.parse_move( 244 + "c1=N", 245 + starfish.from_fen( 246 + "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 247 + ), 248 + ) 249 + assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 250 + let assert Ok(move) = 251 + starfish.parse_move( 252 + "Bxh1", 253 + starfish.from_fen( 254 + "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 255 + ), 256 + ) 257 + assert move == move.Capture(from: 49, to: 7) 258 + let assert Ok(move) = 259 + starfish.parse_move( 260 + "Rac4", 261 + starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 262 + ) 263 + assert move == move.Move(from: 24, to: 26) 264 + 265 + let assert Ok(move) = 266 + starfish.parse_move( 267 + "R7c6", 268 + starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 269 + ) 270 + assert move == move.Move(from: 50, to: 42) 271 + 272 + let assert Error(Nil) = starfish.parse_move("e2", starfish.new()) 273 + let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new()) 274 + let assert Error(Nil) = starfish.parse_move("Ndf3", starfish.new()) 178 275 } 179 276 180 277 pub fn state_test() {