A chess library for Gleam
2
fork

Configure Feed

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

Change `Move` to only represent legal moves

+173 -110
+25 -37
src/starfish.gleam
··· 8 8 pub type Game = 9 9 game.Game 10 10 11 - /// A single move on the chess board. `Move(Legal)` represents a move which has 12 - /// been verified to be legal for a particular position. `Move(Valid)` is a move 13 - /// which is syntactically valid, but is not necessarily legal to play. 14 - pub type Move(validity) = 15 - move.Move(validity) 16 - 17 - pub type Legal = 18 - move.Legal 19 - 20 - pub type Valid = 21 - move.Valid 11 + /// A single legal move on the chess board. 12 + pub type Move = 13 + move.Move 22 14 23 15 /// The [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) 24 16 /// representing the initial position of a chess game. ··· 28 20 /// the input, meaning if a FEN string is partially incomplete (e.g. missing the 29 21 /// half-move and full-move counters at the end), it will fill it in with the 30 22 /// default values of the starting position. 31 - /// 23 + /// 32 24 /// For strict parsing, see [`try_from_fen`](#try_from_fen). 33 - /// 25 + /// 34 26 /// ## Examples 35 - /// 27 + /// 36 28 /// The following expressions are all equivalent: 37 - /// 29 + /// 38 30 /// ```gleam 39 31 /// starfish.new() 40 32 /// starfish.from_fen(starfish.starting_fen) ··· 76 68 /// Tries to parse a game from a FEN string, returning an error if it doesn't 77 69 /// follow standard FEN notation. For more lenient parsing, see [`from_fen`]( 78 70 /// #from_fen). 79 - /// 71 + /// 80 72 /// ## Examples 81 - /// 73 + /// 82 74 /// ```gleam 83 75 /// let assert Ok(start_pos) = starfish.try_from_fen(starfish.starting_fen) 84 76 /// assert start_pos == starfish.new() 85 - /// 77 + /// 86 78 /// let assert Error(starfish.ExpectedSpaceAfterSegment) = 87 79 /// starfish.try_from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR") 88 80 /// ``` ··· 112 104 } 113 105 114 106 /// Convert a game into its FEN string representation. 115 - /// 107 + /// 116 108 /// ## Examples 117 - /// 109 + /// 118 110 /// ```gleam 119 111 /// assert starfish.to_fen(starfish.new()) == starfish.starting_fen 120 112 /// ``` ··· 122 114 game.to_fen(game) 123 115 } 124 116 125 - pub fn legal_moves(game: Game) -> List(Move(Legal)) { 117 + pub fn legal_moves(game: Game) -> List(Move) { 126 118 move.legal(game) 127 119 } 128 120 129 - pub fn search(game: Game, to_depth depth: Int) -> Result(Move(Legal), Nil) { 121 + pub fn search(game: Game, to_depth depth: Int) -> Result(Move, Nil) { 130 122 search.best_move(game, depth) 131 123 } 132 124 133 - pub fn apply_move(game: Game, move: Move(Legal)) -> Game { 125 + pub fn apply_move(game: Game, move: Move) -> Game { 134 126 move.apply(game, move) 135 127 } 136 128 137 - pub fn to_standard_algebraic_notation(move: Move(a)) -> String { 129 + pub fn to_standard_algebraic_notation(move: Move) -> String { 138 130 todo 139 131 } 140 132 ··· 142 134 /// https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Long_algebraic_notation), 143 135 /// specifically the UCI format, containing the start and end positions. For 144 136 /// example, `e2e4` or `c7d8q`. 145 - pub fn to_long_algebraic_notation(move: Move(a)) -> String { 137 + pub fn to_long_algebraic_notation(move: Move) -> String { 146 138 move.to_long_algebraic_notation(move) 147 139 } 148 140 149 141 /// Parses a move from long algebraic notation, in the same format as 150 - /// [`to_long_algebraic_notation`](#to_long_algebraic_notation). 151 - pub fn parse_long_algebraic_notation(string: String) -> Result(Move(Valid), Nil) { 152 - move.from_long_algebraic_notation(string) 142 + /// [`to_long_algebraic_notation`](#to_long_algebraic_notation). Returns an error 143 + /// if the syntax is invalid or the move is not legal. 144 + pub fn parse_long_algebraic_notation( 145 + string: String, 146 + game: Game, 147 + ) -> Result(Move, Nil) { 148 + move.from_long_algebraic_notation(string, game) 153 149 } 154 150 155 - pub fn parse_move(move: String, game: Game) -> Result(Move(Valid), Nil) { 156 - todo 157 - } 158 - 159 - pub fn parse_legal_move(string: String, game: Game) -> Result(Move(Legal), Nil) { 160 - string |> parse_long_algebraic_notation |> result.try(validate_move(_, game)) 161 - } 162 - 163 - pub fn validate_move(move: Move(a), game: Game) -> Result(Move(Legal), Nil) { 151 + pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) { 164 152 todo 165 153 } 166 154
+1 -4
src/starfish/internal/evaluate.gleam
··· 7 7 8 8 /// Statically evaluates a position. Does not take into account checkmate or 9 9 /// stalemate, those must be accounted for beforehand. 10 - pub fn evaluate( 11 - game: game.Game, 12 - legal_moves: List(move.Move(move.Legal)), 13 - ) -> Int { 10 + pub fn evaluate(game: game.Game, legal_moves: List(move.Move)) -> Int { 14 11 evaluate_position(game) + list.length(legal_moves) 15 12 } 16 13
+53 -38
src/starfish/internal/move.gleam
··· 10 10 import starfish/internal/move/attack 11 11 import starfish/internal/move/direction.{type Direction} 12 12 13 - pub type Valid 14 - 15 - pub type Legal 16 - 17 - pub type Move(validity) { 13 + pub type Move { 18 14 Castle(from: Int, to: Int) 19 15 Move(from: Int, to: Int) 20 16 Capture(from: Int, to: Int) ··· 22 18 Promotion(from: Int, to: Int, piece: board.Piece) 23 19 } 24 20 25 - pub fn legal(game: Game) -> List(Move(Legal)) { 21 + pub fn legal(game: Game) -> List(Move) { 26 22 use moves, position, #(piece, colour) <- dict.fold(game.board, []) 27 23 28 24 case colour == game.to_move { ··· 82 78 game: Game, 83 79 position: Int, 84 80 piece: board.Piece, 85 - moves: List(Move(Legal)), 86 - ) -> List(Move(Legal)) { 81 + moves: List(Move), 82 + ) -> List(Move) { 87 83 case piece { 88 84 board.Bishop -> 89 85 sliding_moves(game, position, moves, direction.bishop_directions) ··· 98 94 } 99 95 } 100 96 101 - fn pawn_moves( 102 - game: Game, 103 - position: Int, 104 - moves: List(Move(Legal)), 105 - ) -> List(Move(Legal)) { 97 + fn pawn_moves(game: Game, position: Int, moves: List(Move)) -> List(Move) { 106 98 let #(forward, left, right, promotion_rank) = case game.to_move { 107 99 board.Black -> #( 108 100 direction.down, ··· 216 208 /// En passant needs to be checked slightly different to other moves. For example, 217 209 /// if a row of the board looks something like this, after the black pawn having 218 210 /// moved two squares: 219 - /// 211 + /// 220 212 /// ```txt 221 213 /// | K | | p | P | | | r | | 222 214 /// ``` 223 - /// 215 + /// 224 216 /// Here, the white pawn is not pinned by the black rook, so it can safely move 225 217 /// forwards. However, if it were to perform en passant and capture the black 226 218 /// pawn, the king would be in check. Here, we check for this case. 227 - /// 219 + /// 228 220 /// To perform the check, we cast out rays on either side of the en passant pair. 229 221 /// If we hit the king, as well as a rook or a queen of the opposite colour, then 230 222 /// en passant is not valid. 231 - /// 223 + /// 232 224 fn in_check_after_en_passant( 233 225 game: Game, 234 226 position: Int, ··· 296 288 fn add_promotions( 297 289 from: Int, 298 290 to: Int, 299 - moves: List(Move(Legal)), 291 + moves: List(Move), 300 292 pieces: List(board.Piece), 301 - ) -> List(Move(Legal)) { 293 + ) -> List(Move) { 302 294 case pieces { 303 295 [] -> moves 304 296 [piece, ..pieces] -> ··· 309 301 fn knight_moves( 310 302 game: Game, 311 303 position: Int, 312 - moves: List(Move(Legal)), 304 + moves: List(Move), 313 305 directions: List(Direction), 314 - ) -> List(Move(Legal)) { 306 + ) -> List(Move) { 315 307 case directions { 316 308 [] -> moves 317 309 [direction, ..directions] -> { ··· 338 330 fn king_moves( 339 331 game: Game, 340 332 position: Int, 341 - moves: List(Move(Legal)), 333 + moves: List(Move), 342 334 directions: List(Direction), 343 - ) -> List(Move(Legal)) { 335 + ) -> List(Move) { 344 336 let moves = regular_king_moves(game, position, moves, directions) 345 337 346 338 // If we're in check, castling is not valid. ··· 385 377 fn regular_king_moves( 386 378 game: Game, 387 379 position: Int, 388 - moves: List(Move(Legal)), 380 + moves: List(Move), 389 381 directions: List(Direction), 390 - ) -> List(Move(Legal)) { 382 + ) -> List(Move) { 391 383 case directions { 392 384 [] -> moves 393 385 [direction, ..directions] -> { ··· 414 406 fn sliding_moves( 415 407 game: Game, 416 408 position: Int, 417 - moves: List(Move(Legal)), 409 + moves: List(Move), 418 410 directions: List(Direction), 419 - ) -> List(Move(Legal)) { 411 + ) -> List(Move) { 420 412 case directions { 421 413 [] -> moves 422 414 [direction, ..directions] -> ··· 434 426 start_position: Int, 435 427 position: Int, 436 428 direction: Direction, 437 - moves: List(Move(Legal)), 438 - ) -> List(Move(Legal)) { 429 + moves: List(Move), 430 + ) -> List(Move) { 439 431 let new_position = direction.in_direction(position, direction) 440 432 case board.get(game.board, new_position) { 441 433 board.Empty -> ··· 458 450 } 459 451 } 460 452 461 - pub fn apply(game: Game, move: Move(Legal)) -> game.Game { 453 + pub fn apply(game: Game, move: Move) -> game.Game { 462 454 case move { 463 455 Capture(from:, to:) -> do_apply(game, from, to, False, None, True) 464 456 Castle(from:, to:) -> apply_castle(game, from, to, board.file(to) == 2) ··· 644 636 } 645 637 } 646 638 647 - pub fn to_long_algebraic_notation(move: Move(a)) -> String { 639 + pub fn to_long_algebraic_notation(move: Move) -> String { 648 640 let from = board.position_to_string(move.from) 649 641 let to = board.position_to_string(move.to) 650 642 let extra = case move { ··· 663 655 from <> to <> extra 664 656 } 665 657 666 - pub fn from_long_algebraic_notation(string: String) -> Result(Move(Valid), Nil) { 658 + // TODO: can we do this without calculating every legal move? We can probably just 659 + // calculate legal moves for the specific square we need. 660 + pub fn from_long_algebraic_notation( 661 + string: String, 662 + game: Game, 663 + ) -> Result(Move, Nil) { 667 664 use #(from, string) <- result.try(board.parse_position(string)) 668 665 use #(to, string) <- result.try(board.parse_position(string)) 669 - case string { 670 - "" -> Ok(Move(from:, to:)) 671 - "b" | "B" -> Ok(Promotion(from:, to:, piece: board.Bishop)) 672 - "n" | "N" -> Ok(Promotion(from:, to:, piece: board.Knight)) 673 - "q" | "Q" -> Ok(Promotion(from:, to:, piece: board.Queen)) 674 - "r" | "R" -> Ok(Promotion(from:, to:, piece: board.Rook)) 666 + use promotion_piece <- result.try(case string { 667 + "" -> Ok(None) 668 + "b" | "B" -> Ok(Some(board.Bishop)) 669 + "n" | "N" -> Ok(Some(board.Knight)) 670 + "q" | "Q" -> Ok(Some(board.Queen)) 671 + "r" | "R" -> Ok(Some(board.Rook)) 672 + _ -> Error(Nil) 673 + }) 674 + 675 + let legal_moves = legal(game) 676 + 677 + let valid_moves = 678 + list.filter(legal_moves, fn(move) { 679 + move.from == from 680 + && move.to == to 681 + && case move, promotion_piece { 682 + Promotion(..), None -> False 683 + Promotion(piece:, ..), Some(promotion) -> piece == promotion 684 + _, _ -> promotion_piece == None 685 + } 686 + }) 687 + 688 + case valid_moves { 689 + [move] -> Ok(move) 675 690 _ -> Error(Nil) 676 691 } 677 692 }
+3 -3
src/starfish/internal/move/attack.gleam
··· 390 390 /// When the king is in check, it cannot move into any squares that are attacked 391 391 /// by other pieces. However, there's another case too. Imagine one row of the 392 392 /// board looks like this: 393 - /// 393 + /// 394 394 /// ```txt 395 395 /// | R | | | | k | B | | | 396 396 /// ``` 397 - /// 397 + /// 398 398 /// Here, the white bishop isn't protected by the rook. However, if the king were 399 399 /// to capture the bishop, the rook now is attacking that square, so the king is 400 400 /// still in check. For this reason, we need to calculate all the squares just 401 - /// past the king in a check line, and prevent the king from moving there. 401 + /// past the king in a check line, and prevent the king from moving there. 402 402 fn get_check_attack_squares( 403 403 board: Board, 404 404 attacking: board.Colour,
+2 -5
src/starfish/internal/search.gleam
··· 3 3 import starfish/internal/evaluate 4 4 import starfish/internal/game.{type Game} 5 5 import starfish/internal/hash 6 - import starfish/internal/move 6 + import starfish/internal/move.{type Move} 7 7 8 - /// Not really infinity, but a high enough number that nothing but explicit 8 + /// Not really infinity, but a high enough number that nothing but explicit 9 9 /// references to it will reach it. 10 10 const infinity = 1_000_000_000 11 11 12 12 const checkmate = -1_000_000 13 - 14 - type Move = 15 - move.Move(move.Legal) 16 13 17 14 pub fn best_move(game: Game, depth: Int) -> Result(Move, Nil) { 18 15 use <- bool.guard(depth < 1, Error(Nil))
+89 -23
test/starfish_test.gleam
··· 80 80 } 81 81 82 82 pub fn parse_long_algebraic_notation_test() { 83 - let assert Ok(move) = starfish.parse_long_algebraic_notation("a2a4") 83 + let assert Ok(move) = 84 + starfish.parse_long_algebraic_notation("a2a4", starfish.new()) 84 85 assert move == move.Move(from: 8, to: 24) 85 - let assert Ok(move) = starfish.parse_long_algebraic_notation("g1f3") 86 + let assert Ok(move) = 87 + starfish.parse_long_algebraic_notation("g1f3", starfish.new()) 86 88 assert move == move.Move(from: 6, to: 21) 87 - let assert Ok(move) = starfish.parse_long_algebraic_notation("b8c6") 89 + let assert Ok(move) = 90 + starfish.parse_long_algebraic_notation( 91 + "b8c6", 92 + starfish.from_fen( 93 + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 94 + ), 95 + ) 88 96 assert move == move.Move(from: 57, to: 42) 89 - let assert Ok(move) = starfish.parse_long_algebraic_notation("B7b5") 97 + let assert Ok(move) = 98 + starfish.parse_long_algebraic_notation( 99 + "B7b5", 100 + starfish.from_fen( 101 + "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 102 + ), 103 + ) 90 104 assert move == move.Move(from: 49, to: 33) 91 - let assert Ok(move) = starfish.parse_long_algebraic_notation("a5b6") 92 - assert move == move.Move(from: 32, to: 41) 93 - let assert Ok(move) = starfish.parse_long_algebraic_notation("e1G1") 94 - assert move == move.Move(from: 4, to: 6) 95 - let assert Ok(move) = starfish.parse_long_algebraic_notation("e1c1") 96 - assert move == move.Move(from: 4, to: 2) 97 - let assert Ok(move) = starfish.parse_long_algebraic_notation("e8g8") 98 - assert move == move.Move(from: 60, to: 62) 99 - let assert Ok(move) = starfish.parse_long_algebraic_notation("E8C8") 100 - assert move == move.Move(from: 60, to: 58) 101 - let assert Ok(move) = starfish.parse_long_algebraic_notation("d7c8q") 105 + let assert Ok(move) = 106 + starfish.parse_long_algebraic_notation( 107 + "a5b6", 108 + starfish.from_fen( 109 + "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", 110 + ), 111 + ) 112 + assert move == move.EnPassant(from: 32, to: 41) 113 + let assert Ok(move) = 114 + starfish.parse_long_algebraic_notation( 115 + "e1G1", 116 + starfish.from_fen( 117 + "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", 118 + ), 119 + ) 120 + assert move == move.Castle(from: 4, to: 6) 121 + let assert Ok(move) = 122 + starfish.parse_long_algebraic_notation( 123 + "e1c1", 124 + starfish.from_fen( 125 + "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", 126 + ), 127 + ) 128 + assert move == move.Castle(from: 4, to: 2) 129 + let assert Ok(move) = 130 + starfish.parse_long_algebraic_notation( 131 + "e8g8", 132 + starfish.from_fen( 133 + "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", 134 + ), 135 + ) 136 + assert move == move.Castle(from: 60, to: 62) 137 + let assert Ok(move) = 138 + starfish.parse_long_algebraic_notation( 139 + "E8C8", 140 + starfish.from_fen( 141 + "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", 142 + ), 143 + ) 144 + assert move == move.Castle(from: 60, to: 58) 145 + let assert Ok(move) = 146 + starfish.parse_long_algebraic_notation( 147 + "d7c8q", 148 + starfish.from_fen( 149 + "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 150 + ), 151 + ) 102 152 assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 103 - let assert Ok(move) = starfish.parse_long_algebraic_notation("d2c1N") 153 + let assert Ok(move) = 154 + starfish.parse_long_algebraic_notation( 155 + "d2c1N", 156 + starfish.from_fen( 157 + "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 158 + ), 159 + ) 104 160 assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 105 - let assert Ok(move) = starfish.parse_long_algebraic_notation("b7h1") 106 - assert move == move.Move(from: 49, to: 7) 161 + let assert Ok(move) = 162 + starfish.parse_long_algebraic_notation( 163 + "b7h1", 164 + starfish.from_fen( 165 + "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 166 + ), 167 + ) 168 + assert move == move.Capture(from: 49, to: 7) 107 169 108 - let assert Error(Nil) = starfish.parse_long_algebraic_notation("abcd") 109 - let assert Error(Nil) = starfish.parse_long_algebraic_notation("e2e4extra") 110 - let assert Error(Nil) = starfish.parse_long_algebraic_notation("e2") 111 - let assert Error(Nil) = starfish.parse_long_algebraic_notation("Bxe4") 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()) 112 178 } 113 179 114 180 pub fn state_test() { ··· 376 442 377 443 fn test_apply_move( 378 444 starting_fen: String, 379 - moves: List(move.Move(move.Legal)), 445 + moves: List(move.Move), 380 446 expected_fen: String, 381 447 ) { 382 448 let final_fen =