···161161 move.to_long_algebraic_notation(move)
162162}
163163164164-/// Parses a move from long algebraic notation, in the same format as
165165-/// [`to_long_algebraic_notation`](#to_long_algebraic_notation). Returns an error
166166-/// if the syntax is invalid or the move is not legal.
167167-pub fn parse_long_algebraic_notation(
168168- string: String,
169169- game: Game,
170170-) -> Result(Move, Nil) {
171171- move.from_long_algebraic_notation(string, game)
172172-}
173173-164164+/// Parses a move from either long algebraic notation, in the same format as
165165+/// [`to_long_algebraic_notation`](#to_long_algebraic_notation), or from [Standard
166166+/// Algebraic Notation](https://en.wikipedia.org/wiki/Algebraic_notation_(chess)).
167167+/// Returns an error if the syntax is invalid or the move is not legal on the
168168+/// board.
174169pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) {
175175- todo
170170+ let legal_moves = legal_moves(game)
171171+ case move.from_long_algebraic_notation(move, legal_moves) {
172172+ Ok(move) -> Ok(move)
173173+ Error(_) -> move.from_standard_algebraic_notation(move, game, legal_moves)
174174+ }
176175}
177176178177pub type GameState {
+281-3
src/starfish/internal/move.gleam
···659659// calculate legal moves for the specific square we need.
660660pub fn from_long_algebraic_notation(
661661 string: String,
662662- game: Game,
662662+ legal_moves: List(Move),
663663) -> Result(Move, Nil) {
664664 use #(from, string) <- result.try(board.parse_position(string))
665665 use #(to, string) <- result.try(board.parse_position(string))
···671671 "r" | "R" -> Ok(Some(board.Rook))
672672 _ -> Error(Nil)
673673 })
674674-675675- let legal_moves = legal(game)
676674677675 let valid_moves =
678676 list.filter(legal_moves, fn(move) {
···690688 _ -> Error(Nil)
691689 }
692690}
691691+692692+pub fn from_standard_algebraic_notation(
693693+ move: String,
694694+ game: Game,
695695+ legal_moves: List(Move),
696696+) -> Result(Move, Nil) {
697697+ case parse_special_move(move, game, legal_moves) {
698698+ Illegal -> Error(Nil)
699699+ Legal(move) -> Ok(move)
700700+ Invalid -> {
701701+ let #(piece_kind, move) = case move {
702702+ "R" <> move -> #(board.Rook, move)
703703+ "B" <> move -> #(board.Bishop, move)
704704+ "N" <> move -> #(board.Knight, move)
705705+ "K" <> move -> #(board.King, move)
706706+ "Q" <> move -> #(board.Queen, move)
707707+ _ -> #(board.Pawn, move)
708708+ }
709709+710710+ // Pawn moves have entirely different syntax than non-pawn moves
711711+ use <- bool.lazy_guard(piece_kind == board.Pawn, fn() {
712712+ parse_pawn_move(move, game, legal_moves)
713713+ })
714714+715715+ // In order to determine which move syntax this is, we need to parse the
716716+ // first two characters and match on what they are.
717717+ use #(first, move) <- result.try(parse_move_part(move))
718718+ use #(second, move) <- result.try(parse_move_part(move))
719719+720720+ use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try(
721721+ case first, second {
722722+ // `xx` is not an allowed move
723723+ CaptureSpecifier, CaptureSpecifier -> Error(Nil)
724724+ // We disambiguate the file and it's a capture (e.g. `Baxc6`)
725725+ File(file), CaptureSpecifier -> {
726726+ let from_file = Some(file)
727727+ let from_rank = None
728728+ let capture = True
729729+ use #(to_file, move) <- result.try(parse_file(move))
730730+ use #(to_rank, move) <- result.try(parse_rank(move))
731731+732732+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
733733+ }
734734+ // We disambiguate the rank and it's a capture (e.g. `R5xc4`)
735735+ Rank(rank), CaptureSpecifier -> {
736736+ let from_file = None
737737+ let from_rank = Some(rank)
738738+ let capture = True
739739+ use #(to_file, move) <- result.try(parse_file(move))
740740+ use #(to_rank, move) <- result.try(parse_rank(move))
741741+742742+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
743743+ }
744744+ // It's a capture, and we've parsed the file of the destination (e.g.
745745+ // `Bxa5`)
746746+ CaptureSpecifier, File(to_file) -> {
747747+ let from_file = None
748748+ let from_rank = None
749749+ let capture = True
750750+ use #(to_rank, move) <- result.try(parse_rank(move))
751751+752752+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
753753+ }
754754+ // We disambiguate the file and we've parsed the file of the destination
755755+ // (e.g. `Qhd4`)
756756+ File(from_file), File(to_file) -> {
757757+ let from_file = Some(from_file)
758758+ let from_rank = None
759759+ let capture = False
760760+ use #(to_rank, move) <- result.try(parse_rank(move))
761761+762762+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
763763+ }
764764+ // We disambiguate the rank and we've parsed the file of the destination
765765+ // (e.g. `R7d2`)
766766+ Rank(rank), File(to_file) -> {
767767+ let from_file = None
768768+ let from_rank = Some(rank)
769769+ let capture = False
770770+ use #(to_rank, move) <- result.try(parse_rank(move))
771771+772772+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
773773+ }
774774+ // Capture followed by a rank is not allowed, e.g. `Rx1`
775775+ CaptureSpecifier, Rank(_) -> Error(Nil)
776776+ // We've parsed the file and rank, and there's no more move to parse,
777777+ // so we're done. (e.g. `Nf3`)
778778+ File(file), Rank(rank) if move == "" ->
779779+ Ok(#(None, None, False, file, rank, move))
780780+ // We've disambiguated the rank and file, and we still need to parse
781781+ // the rest of the move. (e.g. `Qh4xe1`)
782782+ File(from_file), Rank(from_rank) ->
783783+ case parse_move_part(move) {
784784+ Ok(#(CaptureSpecifier, move)) -> {
785785+ let from_file = Some(from_file)
786786+ let from_rank = Some(from_rank)
787787+ let capture = True
788788+ use #(to_file, move) <- result.try(parse_file(move))
789789+ use #(to_rank, move) <- result.try(parse_rank(move))
790790+791791+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
792792+ }
793793+ Ok(#(File(to_file), _)) -> {
794794+ let from_file = Some(from_file)
795795+ let from_rank = Some(from_rank)
796796+ let capture = False
797797+ use #(to_rank, move) <- result.try(parse_rank(move))
798798+799799+ Ok(#(from_file, from_rank, capture, to_file, to_rank, move))
800800+ }
801801+ Ok(#(Rank(_), _)) | Error(_) -> Error(Nil)
802802+ }
803803+ // Two ranks are not allowed, e.g. `R15`
804804+ Rank(_), Rank(_) -> Error(Nil)
805805+ },
806806+ )
807807+808808+ use <- bool.guard(move != "", Error(Nil))
809809+810810+ let to = board.position(file: to_file, rank: to_rank)
811811+812812+ case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) {
813813+ [from] if capture -> Ok(Capture(from:, to:))
814814+ [from] -> Ok(Move(from:, to:))
815815+ // If there is more than one valid move, the notation is ambiguous, and
816816+ // so we error. If there are no valid moves, we also error.
817817+ _ -> Error(Nil)
818818+ }
819819+ }
820820+ }
821821+}
822822+823823+type MovePart {
824824+ CaptureSpecifier
825825+ File(Int)
826826+ Rank(Int)
827827+}
828828+829829+fn parse_move_part(move: String) -> Result(#(MovePart, String), Nil) {
830830+ case move {
831831+ "x" <> move -> Ok(#(CaptureSpecifier, move))
832832+ _ ->
833833+ case parse_file(move) {
834834+ Ok(#(file, move)) -> Ok(#(File(file), move))
835835+ Error(_) ->
836836+ case parse_rank(move) {
837837+ Ok(#(rank, move)) -> Ok(#(Rank(rank), move))
838838+ Error(Nil) -> Error(Nil)
839839+ }
840840+ }
841841+ }
842842+}
843843+844844+type SpecialMove {
845845+ Invalid
846846+ Illegal
847847+ Legal(Move)
848848+}
849849+850850+fn parse_special_move(
851851+ move: String,
852852+ game: Game,
853853+ legal_moves: List(Move),
854854+) -> SpecialMove {
855855+ let move = case move {
856856+ "O-O" | "0-0" if game.to_move == board.White -> Ok(Castle(4, 6))
857857+ "O-O" | "0-0" -> Ok(Castle(60, 62))
858858+ "O-O-O" | "0-0-0" if game.to_move == board.White -> Ok(Castle(4, 2))
859859+ "O-O-O" | "0-0-0" -> Ok(Castle(60, 58))
860860+ _ -> Error(Nil)
861861+ }
862862+ case move {
863863+ Error(_) -> Invalid
864864+ Ok(move) ->
865865+ case list.contains(legal_moves, move) {
866866+ False -> Illegal
867867+ True -> Legal(move)
868868+ }
869869+ }
870870+}
871871+872872+fn parse_pawn_move(
873873+ move: String,
874874+ game: Game,
875875+ legal_moves: List(Move),
876876+) -> Result(Move, Nil) {
877877+ use #(file, move) <- result.try(parse_file(move))
878878+879879+ use #(from_file, is_capture, to_file, move) <- result.try(case move {
880880+ "x" <> move ->
881881+ parse_file(move)
882882+ |> result.map(fn(pair) {
883883+ let #(to_file, move) = pair
884884+ #(Some(file), True, to_file, move)
885885+ })
886886+ _ -> Ok(#(None, False, file, move))
887887+ })
888888+889889+ use #(rank, move) <- result.try(parse_rank(move))
890890+891891+ use promotion <- result.try(case move {
892892+ "" -> Ok(None)
893893+ "n" | "N" | "=n" | "=N" -> Ok(Some(board.Knight))
894894+ "q" | "Q" | "=q" | "=Q" -> Ok(Some(board.Queen))
895895+ "b" | "B" | "=b" | "=B" -> Ok(Some(board.Bishop))
896896+ "r" | "R" | "=r" | "=R" -> Ok(Some(board.Rook))
897897+ _ -> Error(Nil)
898898+ })
899899+900900+ let to = board.position(file: to_file, rank:)
901901+902902+ case
903903+ get_pieces(game, board.Pawn, legal_moves, from_file, None, to),
904904+ promotion
905905+ {
906906+ [from], Some(piece) -> Ok(Promotion(from:, to:, piece:))
907907+ [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:))
908908+ [from], _ if is_capture -> Ok(Capture(from:, to:))
909909+ [from], _ -> Ok(Move(from:, to:))
910910+ _, _ -> Error(Nil)
911911+ }
912912+}
913913+914914+/// Gets the possible destination squares for a move, based on the information
915915+/// we know.
916916+fn get_pieces(
917917+ game: Game,
918918+ find_piece: board.Piece,
919919+ legal_moves: List(Move),
920920+ from_file: option.Option(Int),
921921+ from_rank: option.Option(Int),
922922+ to: Int,
923923+) -> List(Int) {
924924+ use pieces, position, #(piece, colour) <- dict.fold(game.board, [])
925925+ let is_valid =
926926+ colour == game.to_move
927927+ && piece == find_piece
928928+ && case from_file {
929929+ None -> True
930930+ Some(file) -> file == board.file(position)
931931+ }
932932+ && case from_rank {
933933+ None -> True
934934+ Some(rank) -> rank == board.rank(position)
935935+ }
936936+ && list.any(legal_moves, fn(move) { move.to == to && move.from == position })
937937+938938+ case is_valid {
939939+ False -> pieces
940940+ True -> [position, ..pieces]
941941+ }
942942+}
943943+944944+fn parse_file(move: String) -> Result(#(Int, String), Nil) {
945945+ case move {
946946+ "a" <> move | "A" <> move -> Ok(#(0, move))
947947+ "b" <> move | "B" <> move -> Ok(#(1, move))
948948+ "c" <> move | "C" <> move -> Ok(#(2, move))
949949+ "d" <> move | "D" <> move -> Ok(#(3, move))
950950+ "e" <> move | "E" <> move -> Ok(#(4, move))
951951+ "f" <> move | "F" <> move -> Ok(#(5, move))
952952+ "g" <> move | "G" <> move -> Ok(#(6, move))
953953+ "h" <> move | "H" <> move -> Ok(#(7, move))
954954+ _ -> Error(Nil)
955955+ }
956956+}
957957+958958+fn parse_rank(move: String) -> Result(#(Int, String), Nil) {
959959+ case move {
960960+ "1" <> move -> Ok(#(0, move))
961961+ "2" <> move -> Ok(#(1, move))
962962+ "3" <> move -> Ok(#(2, move))
963963+ "4" <> move -> Ok(#(3, move))
964964+ "5" <> move -> Ok(#(4, move))
965965+ "6" <> move -> Ok(#(5, move))
966966+ "7" <> move -> Ok(#(6, move))
967967+ "8" <> move -> Ok(#(7, move))
968968+ _ -> Error(Nil)
969969+ }
970970+}
+119-22
test/starfish_test.gleam
···8080}
81818282pub fn parse_long_algebraic_notation_test() {
8383- let assert Ok(move) =
8484- starfish.parse_long_algebraic_notation("a2a4", starfish.new())
8383+ let assert Ok(move) = starfish.parse_move("a2a4", starfish.new())
8584 assert move == move.Move(from: 8, to: 24)
8686- let assert Ok(move) =
8787- starfish.parse_long_algebraic_notation("g1f3", starfish.new())
8585+ let assert Ok(move) = starfish.parse_move("g1f3", starfish.new())
8886 assert move == move.Move(from: 6, to: 21)
8987 let assert Ok(move) =
9090- starfish.parse_long_algebraic_notation(
8888+ starfish.parse_move(
9189 "b8c6",
9290 starfish.from_fen(
9391 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
···9593 )
9694 assert move == move.Move(from: 57, to: 42)
9795 let assert Ok(move) =
9898- starfish.parse_long_algebraic_notation(
9696+ starfish.parse_move(
9997 "B7b5",
10098 starfish.from_fen(
10199 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
···103101 )
104102 assert move == move.Move(from: 49, to: 33)
105103 let assert Ok(move) =
106106- starfish.parse_long_algebraic_notation(
104104+ starfish.parse_move(
107105 "a5b6",
108106 starfish.from_fen(
109107 "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
···111109 )
112110 assert move == move.EnPassant(from: 32, to: 41)
113111 let assert Ok(move) =
114114- starfish.parse_long_algebraic_notation(
112112+ starfish.parse_move(
115113 "e1G1",
116114 starfish.from_fen(
117115 "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
···119117 )
120118 assert move == move.Castle(from: 4, to: 6)
121119 let assert Ok(move) =
122122- starfish.parse_long_algebraic_notation(
120120+ starfish.parse_move(
123121 "e1c1",
124122 starfish.from_fen(
125123 "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
···127125 )
128126 assert move == move.Castle(from: 4, to: 2)
129127 let assert Ok(move) =
130130- starfish.parse_long_algebraic_notation(
128128+ starfish.parse_move(
131129 "e8g8",
132130 starfish.from_fen(
133131 "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
···135133 )
136134 assert move == move.Castle(from: 60, to: 62)
137135 let assert Ok(move) =
138138- starfish.parse_long_algebraic_notation(
136136+ starfish.parse_move(
139137 "E8C8",
140138 starfish.from_fen(
141139 "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
···143141 )
144142 assert move == move.Castle(from: 60, to: 58)
145143 let assert Ok(move) =
146146- starfish.parse_long_algebraic_notation(
144144+ starfish.parse_move(
147145 "d7c8q",
148146 starfish.from_fen(
149147 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
···151149 )
152150 assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
153151 let assert Ok(move) =
154154- starfish.parse_long_algebraic_notation(
152152+ starfish.parse_move(
155153 "d2c1N",
156154 starfish.from_fen(
157155 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
···159157 )
160158 assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
161159 let assert Ok(move) =
162162- starfish.parse_long_algebraic_notation(
160160+ starfish.parse_move(
163161 "b7h1",
164162 starfish.from_fen(
165163 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
···167165 )
168166 assert move == move.Capture(from: 49, to: 7)
169167170170- let assert Error(Nil) =
171171- starfish.parse_long_algebraic_notation("abcd", starfish.new())
172172- let assert Error(Nil) =
173173- starfish.parse_long_algebraic_notation("e2e4extra", starfish.new())
174174- let assert Error(Nil) =
175175- starfish.parse_long_algebraic_notation("e2", starfish.new())
176176- let assert Error(Nil) =
177177- starfish.parse_long_algebraic_notation("Bxe4", starfish.new())
168168+ let assert Error(Nil) = starfish.parse_move("abcd", starfish.new())
169169+ let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new())
170170+ let assert Error(Nil) = starfish.parse_move("a1c8", starfish.new())
171171+}
172172+173173+pub fn parse_standard_algebraic_notation_test() {
174174+ let assert Ok(move) = starfish.parse_move("a4", starfish.new())
175175+ assert move == move.Move(from: 8, to: 24)
176176+ let assert Ok(move) = starfish.parse_move("Nf3", starfish.new())
177177+ assert move == move.Move(from: 6, to: 21)
178178+ let assert Ok(move) =
179179+ starfish.parse_move(
180180+ "Nc6",
181181+ starfish.from_fen(
182182+ "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
183183+ ),
184184+ )
185185+ assert move == move.Move(from: 57, to: 42)
186186+ let assert Ok(move) =
187187+ starfish.parse_move(
188188+ "b5",
189189+ starfish.from_fen(
190190+ "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1",
191191+ ),
192192+ )
193193+ assert move == move.Move(from: 49, to: 33)
194194+ let assert Ok(move) =
195195+ starfish.parse_move(
196196+ "axb6",
197197+ starfish.from_fen(
198198+ "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3",
199199+ ),
200200+ )
201201+ assert move == move.EnPassant(from: 32, to: 41)
202202+ let assert Ok(move) =
203203+ starfish.parse_move(
204204+ "O-O",
205205+ starfish.from_fen(
206206+ "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4",
207207+ ),
208208+ )
209209+ assert move == move.Castle(from: 4, to: 6)
210210+ let assert Ok(move) =
211211+ starfish.parse_move(
212212+ "O-O-O",
213213+ starfish.from_fen(
214214+ "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5",
215215+ ),
216216+ )
217217+ assert move == move.Castle(from: 4, to: 2)
218218+ let assert Ok(move) =
219219+ starfish.parse_move(
220220+ "O-O",
221221+ starfish.from_fen(
222222+ "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4",
223223+ ),
224224+ )
225225+ assert move == move.Castle(from: 60, to: 62)
226226+ let assert Ok(move) =
227227+ starfish.parse_move(
228228+ "0-0-0",
229229+ starfish.from_fen(
230230+ "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5",
231231+ ),
232232+ )
233233+ assert move == move.Castle(from: 60, to: 58)
234234+ let assert Ok(move) =
235235+ starfish.parse_move(
236236+ "c8q",
237237+ starfish.from_fen(
238238+ "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5",
239239+ ),
240240+ )
241241+ assert move == move.Promotion(from: 51, to: 58, piece: board.Queen)
242242+ let assert Ok(move) =
243243+ starfish.parse_move(
244244+ "c1=N",
245245+ starfish.from_fen(
246246+ "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5",
247247+ ),
248248+ )
249249+ assert move == move.Promotion(from: 11, to: 2, piece: board.Knight)
250250+ let assert Ok(move) =
251251+ starfish.parse_move(
252252+ "Bxh1",
253253+ starfish.from_fen(
254254+ "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3",
255255+ ),
256256+ )
257257+ assert move == move.Capture(from: 49, to: 7)
258258+ let assert Ok(move) =
259259+ starfish.parse_move(
260260+ "Rac4",
261261+ starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"),
262262+ )
263263+ assert move == move.Move(from: 24, to: 26)
264264+265265+ let assert Ok(move) =
266266+ starfish.parse_move(
267267+ "R7c6",
268268+ starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"),
269269+ )
270270+ assert move == move.Move(from: 50, to: 42)
271271+272272+ let assert Error(Nil) = starfish.parse_move("e2", starfish.new())
273273+ let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new())
274274+ let assert Error(Nil) = starfish.parse_move("Ndf3", starfish.new())
178275}
179276180277pub fn state_test() {