-module(fen). -compile([no_auto_import, nowarn_unused_vars]). -export([parse_board/1, from_string/1]). -export_type([castling_status/0, fen/0]). -type castling_status() :: {castling_status, boolean(), boolean(), boolean(), boolean()}. -type fen() :: {fen, board:board_bb(), color:color(), castling_status(), gleam@option:option(position:position()), integer(), integer()}. -spec expand_rank(list(binary())) -> list(binary()). expand_rank(Rank) -> Accumulator = [], gleam@list:fold(Rank, Accumulator, fun(Acc, Part) -> case Part of <<"1"/utf8>> -> gleam@list:append(Acc, [<<""/utf8>>]); <<"2"/utf8>> -> gleam@list:append(Acc, [<<""/utf8>>, <<""/utf8>>]); <<"3"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"4"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"5"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"6"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"7"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"8"/utf8>> -> gleam@list:append( Acc, [<<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>, <<""/utf8>>] ); <<"K"/utf8>> -> gleam@list:append(Acc, [<<"K"/utf8>>]); <<"Q"/utf8>> -> gleam@list:append(Acc, [<<"Q"/utf8>>]); <<"R"/utf8>> -> gleam@list:append(Acc, [<<"R"/utf8>>]); <<"B"/utf8>> -> gleam@list:append(Acc, [<<"B"/utf8>>]); <<"N"/utf8>> -> gleam@list:append(Acc, [<<"N"/utf8>>]); <<"P"/utf8>> -> gleam@list:append(Acc, [<<"P"/utf8>>]); <<"k"/utf8>> -> gleam@list:append(Acc, [<<"k"/utf8>>]); <<"q"/utf8>> -> gleam@list:append(Acc, [<<"q"/utf8>>]); <<"r"/utf8>> -> gleam@list:append(Acc, [<<"r"/utf8>>]); <<"b"/utf8>> -> gleam@list:append(Acc, [<<"b"/utf8>>]); <<"n"/utf8>> -> gleam@list:append(Acc, [<<"n"/utf8>>]); <<"p"/utf8>> -> gleam@list:append(Acc, [<<"p"/utf8>>]); _ -> gleam@list:append(Acc, [Part]) end end). -spec parse_board(binary()) -> board:board_bb(). parse_board(Board_string) -> List_of_ranks_as_strings = gleam@string:split(Board_string, <<"/"/utf8>>), Accumulator = {board_bb, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}, {bitboard, 0}}, gleam@list:index_fold( List_of_ranks_as_strings, Accumulator, fun(Acc, Rank_as_string, Rank_index) -> Rank_index@1 = position:int_to_rank(7 - Rank_index), Rank_parts = gleam@string:to_graphemes(Rank_as_string), Expanded_rank = expand_rank(Rank_parts), gleam@list:index_fold( Expanded_rank, Acc, fun(Acc@1, Square, File_index) -> File_index@1 = position:int_to_file(File_index), case Square of <<""/utf8>> -> Acc@1; <<"K"/utf8>> -> New_white_king_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), bitboard:'or'( erlang:element(8, Acc@1), New_white_king_bitboard ), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"Q"/utf8>> -> New_white_queen_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), bitboard:'or'( erlang:element(9, Acc@1), New_white_queen_bitboard ), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"R"/utf8>> -> New_white_rook_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), bitboard:'or'( erlang:element(10, Acc@1), New_white_rook_bitboard ), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"B"/utf8>> -> New_white_bishop_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), bitboard:'or'( erlang:element(11, Acc@1), New_white_bishop_bitboard ), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"N"/utf8>> -> New_white_knight_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), bitboard:'or'( erlang:element(12, Acc@1), New_white_knight_bitboard ), erlang:element(13, Acc@1)}; <<"P"/utf8>> -> New_white_pawn_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), bitboard:'or'( erlang:element(13, Acc@1), New_white_pawn_bitboard )}; <<"k"/utf8>> -> New_black_king_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, bitboard:'or'( erlang:element(2, Acc@1), New_black_king_bitboard ), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"q"/utf8>> -> New_black_queen_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), bitboard:'or'( erlang:element(3, Acc@1), New_black_queen_bitboard ), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"r"/utf8>> -> New_black_rook_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), bitboard:'or'( erlang:element(4, Acc@1), New_black_rook_bitboard ), erlang:element(5, Acc@1), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"b"/utf8>> -> New_black_bishop_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), bitboard:'or'( erlang:element(5, Acc@1), New_black_bishop_bitboard ), erlang:element(6, Acc@1), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"n"/utf8>> -> New_black_knight_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), bitboard:'or'( erlang:element(6, Acc@1), New_black_knight_bitboard ), erlang:element(7, Acc@1), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; <<"p"/utf8>> -> New_black_pawns_bitboard = board:from_position( {position, File_index@1, Rank_index@1} ), {board_bb, erlang:element(2, Acc@1), erlang:element(3, Acc@1), erlang:element(4, Acc@1), erlang:element(5, Acc@1), erlang:element(6, Acc@1), bitboard:'or'( erlang:element(7, Acc@1), New_black_pawns_bitboard ), erlang:element(8, Acc@1), erlang:element(9, Acc@1), erlang:element(10, Acc@1), erlang:element(11, Acc@1), erlang:element(12, Acc@1), erlang:element(13, Acc@1)}; _ -> erlang:error(#{gleam_error => panic, message => <<"panic expression evaluated"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_board"/utf8>>, line => 396}) end end ) end ). -spec parse_turn(binary()) -> color:color(). parse_turn(Turn_string) -> case Turn_string of <<"w"/utf8>> -> white; <<"b"/utf8>> -> black; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid turn string, must be 'w' or 'b'"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_turn"/utf8>>, line => 442}) end. -spec parse_castling(binary()) -> castling_status(). parse_castling(Castling_string) -> case gleam@string:length(Castling_string) =< 4 of true -> Cs_parts = gleam@string:split(Castling_string, <<""/utf8>>), gleam@list:each(Cs_parts, fun(Part) -> case Part of <<"K"/utf8>> -> nil; <<"Q"/utf8>> -> nil; <<"k"/utf8>> -> nil; <<"q"/utf8>> -> nil; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid castling string"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_castling"/utf8>>, line => 458}) end end), White_queenside_castling = gleam@string:contains( Castling_string, <<"Q"/utf8>> ), White_kingside_castling = gleam@string:contains( Castling_string, <<"K"/utf8>> ), Black_queenside_castling = gleam@string:contains( Castling_string, <<"q"/utf8>> ), Black_kingside_castling = gleam@string:contains( Castling_string, <<"k"/utf8>> ), {castling_status, White_kingside_castling, White_queenside_castling, Black_kingside_castling, Black_queenside_castling}; false -> erlang:error(#{gleam_error => panic, message => <<"Invalid castling string"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_castling"/utf8>>, line => 474}) end. -spec parse_file(binary()) -> position:file(). parse_file(File_string) -> case File_string of <<"a"/utf8>> -> a; <<"b"/utf8>> -> b; <<"c"/utf8>> -> c; <<"d"/utf8>> -> d; <<"e"/utf8>> -> e; <<"f"/utf8>> -> f; <<"g"/utf8>> -> g; <<"h"/utf8>> -> h; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid file string"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_file"/utf8>>, line => 505}) end. -spec parse_rank(binary()) -> position:rank(). parse_rank(Rank_string) -> case Rank_string of <<"1"/utf8>> -> one; <<"2"/utf8>> -> two; <<"3"/utf8>> -> three; <<"4"/utf8>> -> four; <<"5"/utf8>> -> five; <<"6"/utf8>> -> six; <<"7"/utf8>> -> seven; <<"8"/utf8>> -> eight; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid rank string"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_rank"/utf8>>, line => 519}) end. -spec parse_en_passant(binary()) -> gleam@option:option(position:position()). parse_en_passant(En_passant_string) -> case En_passant_string of <<"-"/utf8>> -> none; _ -> case gleam@string:length(En_passant_string) =:= 2 of true -> nil; false -> erlang:error(#{gleam_error => panic, message => <<"Invalid en passant string"/utf8>>, module => <<"fen"/utf8>>, function => <<"parse_en_passant"/utf8>>, line => 484}) end, En_passant_parts = gleam@string:split( En_passant_string, <<""/utf8>> ), [File_string, Rank_string] = En_passant_parts, File = parse_file(File_string), Rank = parse_rank(Rank_string), {some, {position, File, Rank}} end. -spec string_to_int(binary()) -> integer(). string_to_int(String) -> case gleam@string:length(String) of 1 -> case String of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 1; <<"2"/utf8>> -> 2; <<"3"/utf8>> -> 3; <<"4"/utf8>> -> 4; <<"5"/utf8>> -> 5; <<"6"/utf8>> -> 6; <<"7"/utf8>> -> 7; <<"8"/utf8>> -> 8; <<"9"/utf8>> -> 9; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 545}) end; 2 -> [Tenths_place_string, Ones_place_string] = gleam@string:split( String, <<""/utf8>> ), Tenths_place = case Tenths_place_string of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 10; <<"2"/utf8>> -> 20; <<"3"/utf8>> -> 30; <<"4"/utf8>> -> 40; <<"5"/utf8>> -> 50; <<"6"/utf8>> -> 60; <<"7"/utf8>> -> 70; <<"8"/utf8>> -> 80; <<"9"/utf8>> -> 90; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 561}) end, Ones_place = case Ones_place_string of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 1; <<"2"/utf8>> -> 2; <<"3"/utf8>> -> 3; <<"4"/utf8>> -> 4; <<"5"/utf8>> -> 5; <<"6"/utf8>> -> 6; <<"7"/utf8>> -> 7; <<"8"/utf8>> -> 8; <<"9"/utf8>> -> 9; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 575}) end, Tenths_place + Ones_place; 3 -> [Hundreds_place_string, Tenths_place_string@1, Ones_place_string@1] = gleam@string:split( String, <<""/utf8>> ), Hundreds_place = case Hundreds_place_string of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 100; <<"2"/utf8>> -> 200; <<"3"/utf8>> -> 300; <<"4"/utf8>> -> 400; <<"5"/utf8>> -> 500; <<"6"/utf8>> -> 600; <<"7"/utf8>> -> 700; <<"8"/utf8>> -> 800; <<"9"/utf8>> -> 900; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 593}) end, Tenths_place@1 = case Tenths_place_string@1 of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 10; <<"2"/utf8>> -> 20; <<"3"/utf8>> -> 30; <<"4"/utf8>> -> 40; <<"5"/utf8>> -> 50; <<"6"/utf8>> -> 60; <<"7"/utf8>> -> 70; <<"8"/utf8>> -> 80; <<"9"/utf8>> -> 90; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 607}) end, Ones_place@1 = case Ones_place_string@1 of <<"0"/utf8>> -> 0; <<"1"/utf8>> -> 1; <<"2"/utf8>> -> 2; <<"3"/utf8>> -> 3; <<"4"/utf8>> -> 4; <<"5"/utf8>> -> 5; <<"6"/utf8>> -> 6; <<"7"/utf8>> -> 7; <<"8"/utf8>> -> 8; <<"9"/utf8>> -> 9; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 621}) end, (Hundreds_place + Tenths_place@1) + Ones_place@1; _ -> erlang:error(#{gleam_error => panic, message => <<"Invalid halfmove string"/utf8>>, module => <<"fen"/utf8>>, function => <<"string_to_int"/utf8>>, line => 625}) end. -spec parse_halfmove(binary()) -> integer(). parse_halfmove(Halfmove_string) -> string_to_int(Halfmove_string). -spec parse_fullmove(binary()) -> integer(). parse_fullmove(Fullmove_string) -> string_to_int(Fullmove_string). -spec from_string(binary()) -> fen(). from_string(Fen) -> Fen_string_parts = gleam@string:split(Fen, <<" "/utf8>>), case gleam@list:length(Fen_string_parts) =:= 6 of false -> erlang:error(#{gleam_error => panic, message => <<"Invalid FEN string"/utf8>>, module => <<"fen"/utf8>>, function => <<"from_string"/utf8>>, line => 38}); true -> [Board_string | Rest] = Fen_string_parts, [Turn_string | Rest@1] = Rest, [Castling_string | Rest@2] = Rest@1, [En_passant_string | Rest@3] = Rest@2, [Halfmove_string | Rest@4] = Rest@3, [Fullmove_string | _] = Rest@4, Parsed_board = parse_board(Board_string), Parsed_turn = parse_turn(Turn_string), Parsed_castling = parse_castling(Castling_string), Parsed_en_passant = parse_en_passant(En_passant_string), Parsed_halfmove = parse_halfmove(Halfmove_string), Parsed_fullmove = parse_fullmove(Fullmove_string), Fen@1 = {fen, Parsed_board, Parsed_turn, Parsed_castling, Parsed_en_passant, Parsed_halfmove, Parsed_fullmove}, Fen@1 end.