From 24372f9250881535cb547d7c4e30675c5bfbc4e9 Mon Sep 17 00:00:00 2001 From: Gears Date: Fri, 26 Sep 2025 23:31:54 +0100 Subject: [PATCH] Store more information in moves to make move.apply and search easier --- src/starfish/internal/move.gleam | 322 +++++++++++++++++++---------- src/starfish/internal/search.gleam | 12 +- test/starfish_test.gleam | 137 ++++++------ 3 files changed, 296 insertions(+), 175 deletions(-) diff --git a/src/starfish/internal/move.gleam b/src/starfish/internal/move.gleam index 0e57f75..6306ffd 100644 --- a/src/starfish/internal/move.gleam +++ b/src/starfish/internal/move.gleam @@ -13,10 +13,23 @@ import starfish/internal/piece_table pub type Move { Castle(from: Int, to: Int) - Move(from: Int, to: Int) - Capture(from: Int, to: Int) + Move(from: Int, to: Int, piece: board.Piece) + Capture(from: Int, to: Int, piece: board.Piece, captured_piece: board.Piece) EnPassant(from: Int, to: Int) - Promotion(from: Int, to: Int, piece: board.Piece) + Promotion( + from: Int, + to: Int, + piece: board.Piece, + captured_piece: Option(board.Piece), + ) +} + +pub fn moving_piece(move: Move) -> board.Piece { + case move { + Capture(piece:, ..) | Move(piece:, ..) -> piece + Castle(..) -> board.King + EnPassant(..) | Promotion(..) -> board.Pawn + } } pub fn legal(game: Game) -> List(Move) { @@ -83,11 +96,11 @@ fn moves_for_piece( ) -> List(Move) { case piece { board.Bishop -> - sliding_moves(game, position, moves, direction.bishop_directions) + sliding_moves(game, piece, position, moves, direction.bishop_directions) board.Rook -> - sliding_moves(game, position, moves, direction.rook_directions) + sliding_moves(game, piece, position, moves, direction.rook_directions) board.Queen -> - sliding_moves(game, position, moves, direction.queen_directions) + sliding_moves(game, piece, position, moves, direction.queen_directions) board.King -> king_moves(game, position, moves, direction.queen_directions) board.Knight -> knight_moves(game, position, moves, direction.knight_directions) @@ -120,8 +133,14 @@ fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) { { False -> moves True if is_promotion -> - add_promotions(position, forward_one, moves, board.pawn_promotions) - True -> [Move(from: position, to: forward_one), ..moves] + add_promotions( + position, + forward_one, + None, + moves, + board.pawn_promotions, + ) + True -> [Move(board.Pawn, from: position, to: forward_one), ..moves] } let can_double_move = case game.to_move, position / 8 { @@ -136,7 +155,7 @@ fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) { board.Empty -> case can_move(position, forward_two, game.attack_information) { False -> moves - True -> [Move(from: position, to: forward_two), ..moves] + True -> [Move(board.Pawn, from: position, to: forward_two), ..moves] } board.Occupied(_, _) | board.OffBoard -> moves } @@ -146,12 +165,21 @@ fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) { let new_position = direction.in_direction(position, left) let moves = case board.get(game.board, new_position) { - board.Occupied(colour:, ..) if colour != game.to_move -> + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> case can_move(position, new_position, game.attack_information) { False -> moves True if is_promotion -> - add_promotions(position, new_position, moves, board.pawn_promotions) - True -> [Capture(from: position, to: new_position), ..moves] + add_promotions( + position, + new_position, + Some(captured_piece), + moves, + board.pawn_promotions, + ) + True -> [ + Capture(board.Pawn, from: position, to: new_position, captured_piece:), + ..moves + ] } board.Empty if game.en_passant_square == Some(new_position) -> case en_passant_is_valid(game, position, new_position) { @@ -163,12 +191,21 @@ fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) { let new_position = direction.in_direction(position, right) case board.get(game.board, new_position) { - board.Occupied(colour:, ..) if colour != game.to_move -> + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> case can_move(position, new_position, game.attack_information) { False -> moves True if is_promotion -> - add_promotions(position, new_position, moves, board.pawn_promotions) - True -> [Capture(from: position, to: new_position), ..moves] + add_promotions( + position, + new_position, + Some(captured_piece), + moves, + board.pawn_promotions, + ) + True -> [ + Capture(board.Pawn, from: position, to: new_position, captured_piece:), + ..moves + ] } board.Empty if game.en_passant_square == Some(new_position) -> case en_passant_is_valid(game, position, new_position) { @@ -288,13 +325,20 @@ fn in_check_after_en_passant_loop( fn add_promotions( from: Int, to: Int, + captured_piece: Option(board.Piece), moves: List(Move), pieces: List(board.Piece), ) -> List(Move) { case pieces { [] -> moves [piece, ..pieces] -> - add_promotions(from, to, [Promotion(from:, to:, piece:), ..moves], pieces) + add_promotions( + from, + to, + captured_piece, + [Promotion(from:, to:, piece:, captured_piece:), ..moves], + pieces, + ) } } @@ -312,12 +356,25 @@ fn knight_moves( board.Empty -> case can_move(position, new_position, game.attack_information) { False -> moves - True -> [Move(from: position, to: new_position), ..moves] + True -> [ + Move(board.Knight, from: position, to: new_position), + ..moves + ] } - board.Occupied(colour:, ..) if colour != game.to_move -> + board.Occupied(colour:, piece: captured_piece) + if colour != game.to_move + -> case can_move(position, new_position, game.attack_information) { False -> moves - True -> [Capture(from: position, to: new_position), ..moves] + True -> [ + Capture( + board.Knight, + from: position, + to: new_position, + captured_piece:, + ), + ..moves + ] } board.Occupied(_, _) | board.OffBoard -> moves } @@ -388,12 +445,25 @@ fn regular_king_moves( board.Empty -> case king_can_move(new_position, game.attack_information) { False -> moves - True -> [Move(from: position, to: new_position), ..moves] + True -> [ + Move(board.King, from: position, to: new_position), + ..moves + ] } - board.Occupied(colour:, ..) if colour != game.to_move -> + board.Occupied(colour:, piece: captured_piece) + if colour != game.to_move + -> case king_can_move(new_position, game.attack_information) { False -> moves - True -> [Capture(from: position, to: new_position), ..moves] + True -> [ + Capture( + board.King, + from: position, + to: new_position, + captured_piece:, + ), + ..moves + ] } board.Occupied(_, _) | board.OffBoard -> moves } @@ -405,6 +475,7 @@ fn regular_king_moves( fn sliding_moves( game: Game, + piece: board.Piece, position: Int, moves: List(Move), directions: List(Direction), @@ -414,8 +485,16 @@ fn sliding_moves( [direction, ..directions] -> sliding_moves( game, + piece, position, - sliding_moves_in_direction(game, position, position, direction, moves), + sliding_moves_in_direction( + game, + piece, + position, + position, + direction, + moves, + ), directions, ) } @@ -423,6 +502,7 @@ fn sliding_moves( fn sliding_moves_in_direction( game: Game, + piece: board.Piece, start_position: Int, position: Int, direction: Direction, @@ -433,18 +513,27 @@ fn sliding_moves_in_direction( board.Empty -> sliding_moves_in_direction( game, + piece, start_position, new_position, direction, case can_move(start_position, new_position, game.attack_information) { False -> moves - True -> [Move(from: start_position, to: new_position), ..moves] + True -> [Move(piece, from: start_position, to: new_position), ..moves] }, ) - board.Occupied(colour:, ..) if colour != game.to_move -> + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> case can_move(start_position, new_position, game.attack_information) { False -> moves - True -> [Capture(from: start_position, to: new_position), ..moves] + True -> [ + Capture( + piece, + from: start_position, + to: new_position, + captured_piece:, + ), + ..moves + ] } board.Occupied(_, _) | board.OffBoard -> moves } @@ -452,12 +541,15 @@ fn sliding_moves_in_direction( pub fn apply(game: Game, move: Move) -> game.Game { case move { - Capture(from:, to:) -> do_apply(game, from, to, False, None, True) + Capture(from:, to:, piece:, captured_piece:) -> + do_apply(game, piece, from, to, False, None, Some(captured_piece)) Castle(from:, to:) -> apply_castle(game, from, to, to % 8 == 2) - EnPassant(from:, to:) -> do_apply(game, from, to, True, None, True) - Move(from:, to:) -> do_apply(game, from, to, False, None, False) - Promotion(from:, to:, piece:) -> - do_apply(game, from, to, False, Some(piece), False) + EnPassant(from:, to:) -> + do_apply(game, board.Pawn, from, to, True, None, None) + Move(from:, to:, piece:) -> + do_apply(game, piece, from, to, False, None, None) + Promotion(from:, to:, piece:, captured_piece:) -> + do_apply(game, board.Pawn, from, to, False, Some(piece), captured_piece) } } @@ -597,11 +689,12 @@ fn apply_castle(game: Game, from: Int, to: Int, long: Bool) -> Game { fn do_apply( game: Game, + piece: board.Piece, from: Int, to: Int, en_passant: Bool, promotion: Option(board.Piece), - capture: Bool, + captured_piece: Option(board.Piece), ) -> Game { let Game( board:, @@ -635,26 +728,30 @@ fn do_apply( board.White -> #(white_pieces, black_pieces) } - let assert board.Occupied(piece:, colour:) = board.get(board, from) - as "Tried to apply move from invalid position" + let our_colour = to_move + let enemy_colour = case to_move { + board.Black -> board.White + board.White -> board.Black + } let castling = castling |> remove_castling(from) |> remove_castling(to) - let one_way_move = capture || piece == board.Pawn + let one_way_move = captured_piece != None || piece == board.Pawn let zobrist_hash = previous_hash |> hash.toggle_to_move - |> hash.toggle_piece(from, piece, colour) + |> hash.toggle_piece(from, piece, our_colour) let phase = game.phase(white_pieces.non_pawn_material, black_pieces.non_pawn_material) let our_piece_square_score = - our_piece_square_score - piece_table.piece_score(piece, colour, from, phase) + our_piece_square_score + - piece_table.piece_score(piece, our_colour, from, phase) let #(piece, our_pawn_material, our_non_pawn_material) = case promotion { None -> #(piece, our_pawn_material, our_non_pawn_material) @@ -666,31 +763,32 @@ fn do_apply( } let our_piece_square_score = - our_piece_square_score + piece_table.piece_score(piece, colour, to, phase) + our_piece_square_score + + piece_table.piece_score(piece, our_colour, to, phase) - let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, colour) + let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, our_colour) let #( zobrist_hash, opposing_pawn_material, opposing_non_pawn_material, opposing_piece_square_score, - ) = case board.get(board, to) { - board.Occupied(piece: board.Pawn, colour:) -> #( - hash.toggle_piece(zobrist_hash, to, board.Pawn, colour), + ) = case captured_piece { + Some(board.Pawn) -> #( + hash.toggle_piece(zobrist_hash, to, board.Pawn, enemy_colour), opposing_pawn_material - board.pawn_value, opposing_non_pawn_material, opposing_piece_square_score - - piece_table.piece_score(board.Pawn, colour, to, phase), + - piece_table.piece_score(board.Pawn, enemy_colour, to, phase), ) - board.Occupied(piece:, colour:) -> #( - hash.toggle_piece(zobrist_hash, to, piece, colour), + Some(piece) -> #( + hash.toggle_piece(zobrist_hash, to, piece, enemy_colour), opposing_pawn_material, opposing_non_pawn_material - board.piece_value(piece), opposing_piece_square_score - - piece_table.piece_score(piece, colour, to, phase), + - piece_table.piece_score(piece, enemy_colour, to, phase), ) - board.Empty | board.OffBoard -> #( + None -> #( zobrist_hash, opposing_pawn_material, opposing_non_pawn_material, @@ -701,14 +799,22 @@ fn do_apply( let board = board |> dict.delete(from) - |> dict.insert(to, #(piece, colour)) + |> dict.insert(to, #(piece, our_colour)) - let #(board, zobrist_hash) = case en_passant, en_passant_square, colour { + let #( + board, + zobrist_hash, + opposing_pawn_material, + opposing_piece_square_score, + ) = case en_passant, en_passant_square, our_colour { True, Some(square), board.White -> { let ep_square = square - 8 #( dict.delete(board, ep_square), hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.Black), + opposing_pawn_material - board.pawn_value, + opposing_piece_square_score + - piece_table.piece_score(board.Pawn, board.Black, ep_square, phase), ) } True, Some(square), board.Black -> { @@ -716,9 +822,17 @@ fn do_apply( #( dict.delete(board, ep_square), hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.White), + opposing_pawn_material - board.pawn_value, + opposing_piece_square_score + - piece_table.piece_score(board.Pawn, board.White, ep_square, phase), ) } - _, _, _ -> #(board, zobrist_hash) + _, _, _ -> #( + board, + zobrist_hash, + opposing_pawn_material, + opposing_piece_square_score, + ) } let en_passant_square = case piece, to - from { @@ -758,10 +872,7 @@ fn do_apply( board.Black -> #(opposing_pieces, our_pieces) } - let to_move = case to_move { - board.Black -> board.White - board.White -> board.Black - } + let to_move = enemy_colour let #(half_moves, previous_positions) = case one_way_move { True -> #(0, []) @@ -823,9 +934,7 @@ pub fn to_long_algebraic_notation(move: Move) -> String { } pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String { - let assert board.Occupied(piece:, colour: _) = - board.get(game.board, move.from) - as "Legal moves should only move valid pieces" + let piece = moving_piece(move) case move { Castle(from: _, to:) -> { @@ -835,18 +944,15 @@ pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String { True -> "O-O-O" } } - Capture(from:, to:) if piece == board.Pawn -> + Capture(from:, to:, ..) if piece == board.Pawn -> pawn_move_to_san(from, to, True, None) EnPassant(from:, to:) -> pawn_move_to_san(from, to, True, None) - Promotion(from:, to:, piece:) -> { - let is_capture = case board.get(game.board, move.to) { - board.Occupied(..) -> True - board.Empty | board.OffBoard -> False - } - pawn_move_to_san(from, to, is_capture, Some(piece)) - } - Move(from:, to:) -> move_to_san(game, piece, from, to, False) - Capture(from:, to:) -> move_to_san(game, piece, from, to, True) + Promotion(from:, to:, piece:, captured_piece: None) -> + pawn_move_to_san(from, to, False, Some(piece)) + Promotion(from:, to:, piece:, captured_piece: Some(_)) -> + pawn_move_to_san(from, to, True, Some(piece)) + Move(from:, to:, ..) -> move_to_san(game, piece, from, to, False) + Capture(from:, to:, ..) -> move_to_san(game, piece, from, to, True) } } @@ -886,9 +992,7 @@ fn disambiguation( use <- bool.guard(move.to != to, disambiguation) use <- bool.guard(move.from == from, disambiguation) - let assert board.Occupied(piece: moving_piece, colour: _) = - board.get(game.board, move.from) - as "Legal moves should only move valid pieces" + let moving_piece = moving_piece(move) use <- bool.guard(moving_piece != piece, disambiguation) @@ -1019,7 +1123,7 @@ pub fn from_standard_algebraic_notation( use #(first, move) <- result.try(parse_move_part(move)) use #(second, move) <- result.try(parse_move_part(move)) - use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try( + use #(from_file, from_rank, to_file, to_rank, move) <- result.try( case first, second { // `xx` is not an allowed move CaptureSpecifier, CaptureSpecifier -> Error(Nil) @@ -1027,58 +1131,53 @@ pub fn from_standard_algebraic_notation( File(file), CaptureSpecifier -> { let from_file = Some(file) let from_rank = None - let capture = True use #(to_file, move) <- result.try(parse_file(move)) use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } // We disambiguate the rank and it's a capture (e.g. `R5xc4`) Rank(rank), CaptureSpecifier -> { let from_file = None let from_rank = Some(rank) - let capture = True use #(to_file, move) <- result.try(parse_file(move)) use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } // It's a capture, and we've parsed the file of the destination (e.g. // `Bxa5`) CaptureSpecifier, File(to_file) -> { let from_file = None let from_rank = None - let capture = True use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } // We disambiguate the file and we've parsed the file of the destination // (e.g. `Qhd4`) File(from_file), File(to_file) -> { let from_file = Some(from_file) let from_rank = None - let capture = False use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } // We disambiguate the rank and we've parsed the file of the destination // (e.g. `R7d2`) Rank(rank), File(to_file) -> { let from_file = None let from_rank = Some(rank) - let capture = False use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } // Capture followed by a rank is not allowed, e.g. `Rx1` CaptureSpecifier, Rank(_) -> Error(Nil) // We've parsed the file and rank, and there's no more move to parse, // so we're done. (e.g. `Nf3`) File(file), Rank(rank) if move == "" -> - Ok(#(None, None, False, file, rank, move)) + Ok(#(None, None, file, rank, move)) // We've disambiguated the rank and file, and we still need to parse // the rest of the move. (e.g. `Qh4xe1`) File(from_file), Rank(from_rank) -> @@ -1086,19 +1185,17 @@ pub fn from_standard_algebraic_notation( Ok(#(CaptureSpecifier, move)) -> { let from_file = Some(from_file) let from_rank = Some(from_rank) - let capture = True use #(to_file, move) <- result.try(parse_file(move)) use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } Ok(#(File(to_file), _)) -> { let from_file = Some(from_file) let from_rank = Some(from_rank) - let capture = False use #(to_rank, move) <- result.try(parse_rank(move)) - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) + Ok(#(from_file, from_rank, to_file, to_rank, move)) } Ok(#(Rank(_), _)) | Error(_) -> Error(Nil) } @@ -1111,9 +1208,10 @@ pub fn from_standard_algebraic_notation( let to = to_rank * 8 + to_file - case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) { - [from] if capture -> Ok(Capture(from:, to:)) - [from] -> Ok(Move(from:, to:)) + case + get_moves(game, piece_kind, legal_moves, from_file, from_rank, to, None) + { + [move] -> Ok(move) // If there is more than one valid move, the notation is ambiguous, and // so we error. If there are no valid moves, we also error. _ -> Error(Nil) @@ -1178,14 +1276,14 @@ fn parse_pawn_move( ) -> Result(Move, Nil) { use #(file, move) <- result.try(parse_file(move)) - use #(from_file, is_capture, to_file, move) <- result.try(case move { + use #(from_file, to_file, move) <- result.try(case move { "x" <> move -> parse_file(move) |> result.map(fn(pair) { let #(to_file, move) = pair - #(Some(file), True, to_file, move) + #(Some(file), to_file, move) }) - _ -> Ok(#(None, False, file, move)) + _ -> Ok(#(None, file, move)) }) use #(rank, move) <- result.try(parse_rank(move)) @@ -1202,28 +1300,25 @@ fn parse_pawn_move( let to = rank * 8 + to_file case - get_pieces(game, board.Pawn, legal_moves, from_file, None, to), - promotion + get_moves(game, board.Pawn, legal_moves, from_file, None, to, promotion) { - [from], Some(piece) -> Ok(Promotion(from:, to:, piece:)) - [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:)) - [from], _ if is_capture -> Ok(Capture(from:, to:)) - [from], _ -> Ok(Move(from:, to:)) - _, _ -> Error(Nil) + [move] -> Ok(move) + _ -> Error(Nil) } } -/// Gets the possible destination squares for a move, based on the information -/// we know. -fn get_pieces( +/// Gets the possible moves for a piece, based on the information we know from +/// SAN. +fn get_moves( game: Game, find_piece: board.Piece, legal_moves: List(Move), from_file: option.Option(Int), from_rank: option.Option(Int), to: Int, -) -> List(Int) { - use pieces, position, #(piece, colour) <- dict.fold(game.board, []) + promotion: Option(board.Piece), +) -> List(Move) { + use moves, position, #(piece, colour) <- dict.fold(game.board, []) let is_valid = colour == game.to_move && piece == find_piece @@ -1235,11 +1330,24 @@ fn get_pieces( None -> True Some(rank) -> rank == position / 8 } - && list.any(legal_moves, fn(move) { move.to == to && move.from == position }) case is_valid { - False -> pieces - True -> [position, ..pieces] + False -> moves + True -> + case + list.find(legal_moves, fn(move) { + let valid = move.to == to && move.from == position + case move, promotion { + Promotion(piece:, ..), Some(promotion) if piece == promotion -> + valid + Promotion(..), _ -> False + _, _ -> valid + } + }) + { + Error(_) -> moves + Ok(move) -> [move, ..moves] + } } } diff --git a/src/starfish/internal/search.gleam b/src/starfish/internal/search.gleam index 34a6191..a70e3ec 100644 --- a/src/starfish/internal/search.gleam +++ b/src/starfish/internal/search.gleam @@ -408,8 +408,8 @@ const capture_promotion_bonus = 10_000 /// order than random. Searching better moves first improves alpha-beta pruning, /// allowing us to search more positions. fn guess_eval(game: Game, move: Move, phase: Int) -> Int { - let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from) - as "Invalid move trying to move empty piece" + let piece = move.moving_piece(move) + let colour = game.to_move let moving_piece = case move { move.Promotion(piece:, ..) -> piece @@ -422,13 +422,7 @@ fn guess_eval(game: Game, move: Move, phase: Int) -> Int { let position_improvement = to_score - from_score let move_specific_score = case move { - // TODO store information in moves so we don't have to retrieve it from the - // board every time. - move.Capture(..) -> { - let assert board.Occupied(piece: captured_piece, colour: _) = - board.get(game.board, move.to) - as "Invalid capture moving to empty square" - + move.Capture(captured_piece:, ..) -> { capture_promotion_bonus // Capturing a more valuable piece is better, and using a less valuable // piece to capture is usually better. However, we prioritise the value of diff --git a/test/starfish_test.gleam b/test/starfish_test.gleam index c35cd49..18da369 100644 --- a/test/starfish_test.gleam +++ b/test/starfish_test.gleam @@ -1,6 +1,7 @@ import gleam/int import gleam/io import gleam/list +import gleam/option.{None, Some} import gleeunit import pocket_watch import starfish @@ -12,30 +13,20 @@ pub fn main() -> Nil { gleeunit.main() } -/// Compare the state of two games, ignoring additional fields -fn game_equal(a: game.Game, b: game.Game) -> Bool { - a.board == b.board - && a.to_move == b.to_move - && a.castling == b.castling - && a.en_passant_square == b.en_passant_square - && a.half_moves == b.half_moves - && a.full_moves == b.full_moves -} - pub fn from_fen_test() { let initial = starfish.new() let parsed = starfish.from_fen(starfish.starting_fen) - assert game_equal(initial, parsed) + assert initial == parsed let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" let parsed = starfish.from_fen(initial_with_only_position) - assert game_equal(initial, parsed) + assert initial == parsed } pub fn try_from_fen_test() { let initial = starfish.new() let assert Ok(parsed) = starfish.try_from_fen(starfish.starting_fen) - assert game_equal(parsed, initial) + assert parsed == initial let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" let assert Error(error) = starfish.try_from_fen(initial_with_only_position) @@ -51,13 +42,17 @@ pub fn to_fen_test() { } pub fn to_long_algebraic_notation_test() { - assert move.Move(from: 8, to: 24) |> starfish.to_long_algebraic_notation + assert move.Move(board.Pawn, from: 8, to: 24) + |> starfish.to_long_algebraic_notation == "a2a4" - assert move.Move(from: 6, to: 21) |> starfish.to_long_algebraic_notation + assert move.Move(board.Pawn, from: 6, to: 21) + |> starfish.to_long_algebraic_notation == "g1f3" - assert move.Move(from: 57, to: 42) |> starfish.to_long_algebraic_notation + assert move.Move(board.Pawn, from: 57, to: 42) + |> starfish.to_long_algebraic_notation == "b8c6" - assert move.Move(from: 49, to: 33) |> starfish.to_long_algebraic_notation + assert move.Move(board.Pawn, from: 49, to: 33) + |> starfish.to_long_algebraic_notation == "b7b5" assert move.EnPassant(from: 32, to: 41) |> starfish.to_long_algebraic_notation == "a5b6" @@ -69,21 +64,32 @@ pub fn to_long_algebraic_notation_test() { == "e8g8" assert move.Castle(from: 60, to: 58) |> starfish.to_long_algebraic_notation == "e8c8" - assert move.Promotion(from: 51, to: 58, piece: board.Queen) + assert move.Promotion( + from: 51, + to: 58, + piece: board.Queen, + captured_piece: None, + ) |> starfish.to_long_algebraic_notation == "d7c8q" - assert move.Promotion(from: 11, to: 2, piece: board.Knight) + assert move.Promotion( + from: 11, + to: 2, + piece: board.Knight, + captured_piece: Some(board.Rook), + ) |> starfish.to_long_algebraic_notation == "d2c1n" - assert move.Capture(from: 49, to: 7) |> starfish.to_long_algebraic_notation + assert move.Capture(board.Bishop, from: 49, to: 7, captured_piece: board.Pawn) + |> starfish.to_long_algebraic_notation == "b7h1" } pub fn parse_long_algebraic_notation_test() { let assert Ok(move) = starfish.parse_move("a2a4", starfish.new()) - assert move == move.Move(from: 8, to: 24) + assert move == move.Move(board.Pawn, from: 8, to: 24) let assert Ok(move) = starfish.parse_move("g1f3", starfish.new()) - assert move == move.Move(from: 6, to: 21) + assert move == move.Move(board.Knight, from: 6, to: 21) let assert Ok(move) = starfish.parse_move( "b8c6", @@ -91,7 +97,7 @@ pub fn parse_long_algebraic_notation_test() { "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", ), ) - assert move == move.Move(from: 57, to: 42) + assert move == move.Move(board.Knight, from: 57, to: 42) let assert Ok(move) = starfish.parse_move( "B7b5", @@ -99,7 +105,7 @@ pub fn parse_long_algebraic_notation_test() { "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", ), ) - assert move == move.Move(from: 49, to: 33) + assert move == move.Move(board.Pawn, from: 49, to: 33) let assert Ok(move) = starfish.parse_move( "a5b6", @@ -147,7 +153,8 @@ pub fn parse_long_algebraic_notation_test() { "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", ), ) - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) + assert move + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) let assert Ok(move) = starfish.parse_move( "d2c1N", @@ -155,7 +162,8 @@ pub fn parse_long_algebraic_notation_test() { "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", ), ) - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) + assert move + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) let assert Ok(move) = starfish.parse_move( "b7h1", @@ -163,7 +171,7 @@ pub fn parse_long_algebraic_notation_test() { "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", ), ) - assert move == move.Capture(from: 49, to: 7) + assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) let assert Error(Nil) = starfish.parse_move("abcd", starfish.new()) let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new()) @@ -172,9 +180,9 @@ pub fn parse_long_algebraic_notation_test() { pub fn parse_standard_algebraic_notation_test() { let assert Ok(move) = starfish.parse_move("a4", starfish.new()) - assert move == move.Move(from: 8, to: 24) + assert move == move.Move(board.Pawn, from: 8, to: 24) let assert Ok(move) = starfish.parse_move("Nf3", starfish.new()) - assert move == move.Move(from: 6, to: 21) + assert move == move.Move(board.Knight, from: 6, to: 21) let assert Ok(move) = starfish.parse_move( "Nc6", @@ -182,7 +190,7 @@ pub fn parse_standard_algebraic_notation_test() { "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", ), ) - assert move == move.Move(from: 57, to: 42) + assert move == move.Move(board.Knight, from: 57, to: 42) let assert Ok(move) = starfish.parse_move( "b5", @@ -190,7 +198,7 @@ pub fn parse_standard_algebraic_notation_test() { "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", ), ) - assert move == move.Move(from: 49, to: 33) + assert move == move.Move(board.Pawn, from: 49, to: 33) let assert Ok(move) = starfish.parse_move( "axb6", @@ -238,7 +246,8 @@ pub fn parse_standard_algebraic_notation_test() { "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", ), ) - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) + assert move + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) let assert Ok(move) = starfish.parse_move( "c1=N", @@ -246,7 +255,8 @@ pub fn parse_standard_algebraic_notation_test() { "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", ), ) - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) + assert move + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) let assert Ok(move) = starfish.parse_move( "Bxh1", @@ -254,20 +264,20 @@ pub fn parse_standard_algebraic_notation_test() { "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", ), ) - assert move == move.Capture(from: 49, to: 7) + assert move == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) let assert Ok(move) = starfish.parse_move( "Rac4", starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), ) - assert move == move.Move(from: 24, to: 26) + assert move == move.Move(board.Rook, from: 24, to: 26) let assert Ok(move) = starfish.parse_move( "R7c6", starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), ) - assert move == move.Move(from: 50, to: 42) + assert move == move.Move(board.Rook, from: 50, to: 42) let assert Error(Nil) = starfish.parse_move("e2", starfish.new()) let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new()) @@ -276,17 +286,17 @@ pub fn parse_standard_algebraic_notation_test() { pub fn to_standard_algebraic_notation_test() { assert starfish.to_standard_algebraic_notation( - move.Move(from: 8, to: 24), + move.Move(board.Pawn, from: 8, to: 24), starfish.new(), ) == "a4" assert starfish.to_standard_algebraic_notation( - move.Move(from: 6, to: 21), + move.Move(board.Knight, from: 6, to: 21), starfish.new(), ) == "Nf3" assert starfish.to_standard_algebraic_notation( - move.Move(from: 57, to: 42), + move.Move(board.Knight, from: 57, to: 42), starfish.from_fen( "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", ), @@ -294,7 +304,7 @@ pub fn to_standard_algebraic_notation_test() { == "Nc6" assert starfish.to_standard_algebraic_notation( - move.Move(from: 49, to: 33), + move.Move(board.Pawn, from: 49, to: 33), starfish.from_fen( "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", ), @@ -342,7 +352,7 @@ pub fn to_standard_algebraic_notation_test() { == "O-O-O" assert starfish.to_standard_algebraic_notation( - move.Promotion(from: 51, to: 58, piece: board.Queen), + move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen), starfish.from_fen( "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", ), @@ -350,7 +360,7 @@ pub fn to_standard_algebraic_notation_test() { == "dxc8=Q" assert starfish.to_standard_algebraic_notation( - move.Promotion(from: 11, to: 2, piece: board.Knight), + move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight), starfish.from_fen( "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", ), @@ -358,7 +368,7 @@ pub fn to_standard_algebraic_notation_test() { == "dxc1=N" assert starfish.to_standard_algebraic_notation( - move.Capture(from: 49, to: 7), + move.Capture(board.Bishop, board.Rook, from: 49, to: 7), starfish.from_fen( "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", ), @@ -366,19 +376,19 @@ pub fn to_standard_algebraic_notation_test() { == "Bxh1" assert starfish.to_standard_algebraic_notation( - move.Move(from: 24, to: 26), + move.Move(board.Rook, from: 24, to: 26), starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), ) == "Rac4" assert starfish.to_standard_algebraic_notation( - move.Move(from: 50, to: 42), + move.Move(board.Rook, from: 50, to: 42), starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), ) == "R7c6" assert starfish.to_standard_algebraic_notation( - move.Capture(from: 31, to: 13), + move.Capture(board.Queen, board.Bishop, from: 31, to: 13), starfish.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"), ) == "Qh4xf2" @@ -501,14 +511,14 @@ pub fn search_test_() { until: starfish.Depth(5), ) // b4f4 - assert move == move.Capture(from: 25, to: 29) + assert move == move.Capture(board.Rook, board.Pawn, from: 25, to: 29) let assert Ok(move) = starfish.search( starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"), until: starfish.Depth(10), ) - assert move == move.Move(from: 29, to: 31) + assert move == move.Move(board.Rook, from: 29, to: 31) } pub fn perft_initial_position_test_() { @@ -713,41 +723,50 @@ fn test_apply_move( moves: List(move.Move), expected_fen: String, ) { - let final_fen = + let game = starting_fen - |> game.from_fen + |> starfish.from_fen |> list.fold(moves, _, starfish.apply_move) - |> game.to_fen - assert final_fen == expected_fen + let game = game.Game(..game, previous_positions: []) + + let expected_game = starfish.from_fen(expected_fen) + + assert game == expected_game } pub fn apply_move_test() { test_apply_move( starfish.starting_fen, // a2a4 - [move.Move(from: 8, to: 24)], + [move.Move(board.Pawn, from: 8, to: 24)], "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq a3 0 1", ) test_apply_move( starfish.starting_fen, // g1f3 - [move.Move(from: 6, to: 21)], + [move.Move(board.Knight, from: 6, to: 21)], "rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1", ) test_apply_move( starfish.starting_fen, // a2a4, b8c6 - [move.Move(from: 8, to: 24), move.Move(from: 57, to: 42)], + [ + move.Move(board.Pawn, from: 8, to: 24), + move.Move(board.Knight, from: 57, to: 42), + ], "r1bqkbnr/pppppppp/2n5/8/P7/8/1PPPPPPP/RNBQKBNR w KQkq - 1 2", ) test_apply_move( starfish.starting_fen, // a2a4, b7b5 - [move.Move(from: 8, to: 24), move.Move(from: 49, to: 33)], + [ + move.Move(board.Pawn, from: 8, to: 24), + move.Move(board.Pawn, from: 49, to: 33), + ], "rnbqkbnr/p1pppppp/8/1p6/P7/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 2", ) @@ -789,14 +808,14 @@ pub fn apply_move_test() { test_apply_move( "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", // d7c8q - [move.Promotion(from: 51, to: 58, piece: board.Queen)], + [move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)], "rnQq1bnr/ppp1kpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR b KQ - 0 5", ) test_apply_move( "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", // d2c1n - [move.Promotion(from: 11, to: 2, piece: board.Knight)], + [move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)], "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPP1KPP1/RNnQ1BNR w kq - 0 6", ) @@ -804,7 +823,7 @@ pub fn apply_move_test() { test_apply_move( "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", // b7h1 - [move.Capture(from: 49, to: 7)], + [move.Capture(board.Bishop, board.Rook, from: 49, to: 7)], "rn1qkbnr/p1pppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNb w Qkq - 0 4", ) } -- 2.51.2