A chess library for Gleam

Compare changes

Choose any two refs to compare.

+152
gen.js
··· 1 + const tables = { 2 + "pawn": `#( 3 + #(000, 000, 000, 000, 000, 000, 000, 000), 4 + #(-007, 007, -003, -013, 005, -016, 010, -008), 5 + #(005, -012, -007, 022, -008, -005, -015, -008), 6 + #(013, 000, -013, 001, 011, -002, -013, 005), 7 + #(-004, -023, 006, 020, 040, 017, 004, -008), 8 + #(-009, -015, 011, 015, 032, 022, 005, -022), 9 + #(003, 003, 010, 019, 016, 019, 007, -005), 10 + #(000, 000, 000, 000, 000, 000, 000, 000), 11 + )`, 12 + 13 + "knight": `#( 14 + #(-201, -083, -056, -026, -026, -056, -083, -201), 15 + #(-067, -027, 004, 037, 037, 004, -027, -067), 16 + #(-009, 022, 058, 053, 053, 058, 022, -009), 17 + #(-034, 013, 044, 051, 051, 044, 013, -034), 18 + #(-035, 008, 040, 049, 049, 040, 008, -035), 19 + #(-061, -017, 006, 012, 012, 006, -017, -061), 20 + #(-077, -041, -027, -015, -015, -027, -041, -077), 21 + #(-175, -092, -074, -073, -073, -074, -092, -175), 22 + )`, 23 + 24 + "bishop": `#( 25 + #(-048, 001, -014, -023, -023, -014, 001, -048), 26 + #(-017, -014, 005, 000, 000, 005, -014, -017), 27 + #(-016, 006, 001, 011, 011, 001, 006, -016), 28 + #(-012, 029, 022, 031, 031, 022, 029, -012), 29 + #(-005, 011, 025, 039, 039, 025, 011, -005), 30 + #(-007, 021, -005, 017, 017, -005, 021, -007), 31 + #(-015, 008, 019, 004, 004, 019, 008, -015), 32 + #(-053, -005, -008, -023, -023, -008, -005, -053), 33 + )`, 34 + 35 + "rook": `#( 36 + #(-017, -019, -001, 009, 009, -001, -019, -017), 37 + #(-002, 012, 016, 018, 018, 016, 012, -002), 38 + #(-022, -002, 006, 012, 012, 006, -002, -022), 39 + #(-027, -015, -004, 003, 003, -004, -015, -027), 40 + #(-013, -005, -004, -006, -006, -004, -005, -013), 41 + #(-025, -011, -001, 003, 003, -001, -011, -025), 42 + #(-021, -013, -008, 006, 006, -008, -013, -021), 43 + #(-031, -020, -014, -005, -005, -014, -020, -031), 44 + )`, 45 + 46 + "queen": `#( 47 + #(-002, -002, 001, -002, -002, 001, -002, -002), 48 + #(-005, 006, 010, 008, 008, 010, 006, -005), 49 + #(-004, 010, 006, 008, 008, 006, 010, -004), 50 + #(000, 014, 012, 005, 005, 012, 014, 000), 51 + #(004, 005, 009, 008, 008, 009, 005, 004), 52 + #(-003, 006, 013, 007, 007, 013, 006, -003), 53 + #(-003, 005, 008, 012, 012, 008, 005, -003), 54 + #(003, -005, -005, 004, 004, -005, -005, 003), 55 + )`, 56 + 57 + "king": `#( 58 + #(059, 089, 045, -001, -001, 045, 089, 059), 59 + #(088, 120, 065, 033, 033, 065, 120, 088), 60 + #(123, 145, 081, 031, 031, 081, 145, 123), 61 + #(154, 179, 105, 070, 070, 105, 179, 154), 62 + #(164, 190, 138, 098, 098, 138, 190, 164), 63 + #(195, 258, 169, 120, 120, 169, 258, 195), 64 + #(278, 303, 234, 179, 179, 234, 303, 278), 65 + #(271, 327, 271, 198, 198, 271, 327, 271), 66 + )`, 67 + 68 + "pawn_endgame": `#( 69 + #(000, 000, 000, 000, 000, 000, 000, 000), 70 + #(000, -011, 012, 021, 025, 019, 004, 007), 71 + #(028, 020, 021, 028, 030, 007, 006, 013), 72 + #(010, 005, 004, -005, -005, -005, 014, 009), 73 + #(006, -002, -008, -004, -013, -012, -010, -009), 74 + #(-010, -010, -010, 004, 004, 003, -006, -004), 75 + #(-010, -006, 010, 000, 014, 007, -005, -019), 76 + #(000, 000, 000, 000, 000, 000, 000, 000), 77 + )`, 78 + 79 + "knight_endgame": `#( 80 + #(-100, -088, -056, -017, -017, -056, -088, -100), 81 + #(-069, -050, -051, 012, 012, -051, -050, -069), 82 + #(-051, -044, -016, 017, 017, -016, -044, -051), 83 + #(-045, -016, 009, 039, 039, 009, -016, -045), 84 + #(-035, -002, 013, 028, 028, 013, -002, -035), 85 + #(-040, -027, -008, 029, 029, -008, -027, -040), 86 + #(-067, -054, -018, 008, 008, -018, -054, -067), 87 + #(-096, -065, -049, -021, -021, -049, -065, -096), 88 + )`, 89 + 90 + "bishop_endgame": `#( 91 + #(-046, -042, -037, -024, -024, -037, -042, -046), 92 + #(-031, -020, -001, 001, 001, -001, -020, -031), 93 + #(-030, 006, 004, 006, 006, 004, 006, -030), 94 + #(-017, -001, -014, 015, 015, -014, -001, -017), 95 + #(-020, -006, 000, 017, 017, 000, -006, -020), 96 + #(-016, -001, -002, 010, 010, -002, -001, -016), 97 + #(-037, -013, -017, 001, 001, -017, -013, -037), 98 + #(-057, -030, -037, -012, -012, -037, -030, -057), 99 + )`, 100 + 101 + "rook_endgame": `#( 102 + #(018, 000, 019, 013, 013, 019, 000, 018), 103 + #(004, 005, 020, -005, -005, 020, 005, 004), 104 + #(006, 001, -007, 010, 010, -007, 001, 006), 105 + #(-005, 008, 007, -006, -006, 007, 008, -005), 106 + #(-006, 001, -009, 007, 007, -009, 001, -006), 107 + #(006, -008, -002, -006, -006, -002, -008, 006), 108 + #(-012, -009, -001, -002, -002, -001, -009, -012), 109 + #(-009, -013, -010, -009, -009, -010, -013, -009), 110 + )`, 111 + 112 + "queen_endgame": `#( 113 + #(-075, -052, -043, -036, -036, -043, -052, -075), 114 + #(-050, -027, -024, -008, -008, -024, -027, -050), 115 + #(-038, -018, -012, 001, 001, -012, -018, -038), 116 + #(-029, -006, 009, 021, 021, 009, -006, -029), 117 + #(-023, -003, 013, 024, 024, 013, -003, -023), 118 + #(-039, -018, -009, 003, 003, -009, -018, -039), 119 + #(-055, -031, -022, -004, -004, -022, -031, -055), 120 + #(-069, -057, -047, -026, -026, -047, -057, -069), 121 + )`, 122 + 123 + "king_endgame": `#( 124 + #(011, 059, 073, 078, 078, 073, 059, 011), 125 + #(047, 121, 116, 131, 131, 116, 121, 047), 126 + #(092, 172, 184, 191, 191, 184, 172, 092), 127 + #(096, 166, 199, 199, 199, 199, 166, 096), 128 + #(103, 156, 172, 172, 172, 172, 156, 103), 129 + #(088, 130, 169, 175, 175, 169, 130, 088), 130 + #(053, 100, 133, 135, 135, 133, 100, 053), 131 + #(001, 045, 085, 076, 076, 085, 045, 001), 132 + )`, 133 + } 134 + 135 + let out = ""; 136 + 137 + for (let [name, table] of Object.entries(tables)) { 138 + if (!table.startsWith("#(\n")) throw undefined; 139 + if (!table.endsWith("\n)")) throw undefined; 140 + 141 + table = table.slice(3, table.length - 2); 142 + 143 + const rows = table.split("\n").reverse(); 144 + 145 + const reverseTable = `#( 146 + ${rows.join("\n")} 147 + )`; 148 + 149 + out += `const ${name} = ${reverseTable}\n\n`; 150 + } 151 + 152 + console.log(out);
-1
gleam.toml
··· 15 15 [dependencies] 16 16 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 17 iv = ">= 1.3.2 and < 2.0.0" 18 - birl = ">= 1.8.0 and < 2.0.0" 19 18 20 19 [dev-dependencies] 21 20 gleeunit = ">= 1.0.0 and < 2.0.0"
-1
manifest.toml
··· 17 17 ] 18 18 19 19 [requirements] 20 - birl = { version = ">= 1.8.0 and < 2.0.0" } 21 20 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 22 21 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 23 22 iv = { version = ">= 1.3.2 and < 2.0.0" }
+8 -6
src/starfish/internal/board.gleam
··· 34 34 King 35 35 } 36 36 37 - pub const pawn_value = 100 37 + // Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/ 38 38 39 - pub const knight_value = 300 39 + pub const pawn_value = 124 40 40 41 - pub const bishop_value = 300 41 + pub const knight_value = 781 42 42 43 - pub const rook_value = 500 43 + pub const bishop_value = 825 44 44 45 - pub const queen_value = 900 45 + pub const rook_value = 1276 46 + 47 + pub const queen_value = 2538 46 48 47 - pub const king_value = 1000 49 + pub const king_value = 10_000 48 50 49 51 pub fn piece_value(piece: Piece) -> Int { 50 52 case piece {
-27
src/starfish/internal/evaluate.gleam
··· 1 - import gleam/list 2 - import starfish/internal/board 3 - import starfish/internal/game 4 - 5 - /// Statically evaluates a position. Does not take into account checkmate or 6 - /// stalemate, those must be accounted for beforehand. 7 - pub fn evaluate(game: game.Game, legal_moves: List(move)) -> Int { 8 - let black_position_score = position_score(game.black_pieces) 9 - let white_position_score = position_score(game.white_pieces) 10 - 11 - let position_score = case game.to_move { 12 - board.Black -> black_position_score - white_position_score 13 - board.White -> white_position_score - black_position_score 14 - } 15 - 16 - position_score + list.length(legal_moves) 17 - } 18 - 19 - fn position_score(pieces: game.PieceInfo) -> Int { 20 - let game.PieceInfo( 21 - king_position: _, 22 - non_pawn_material:, 23 - pawn_material:, 24 - piece_square_score:, 25 - ) = pieces 26 - non_pawn_material + pawn_material + piece_square_score 27 - }
+109 -39
src/starfish/internal/game.gleam
··· 1 1 import gleam/bool 2 2 import gleam/dict 3 3 import gleam/int 4 + import gleam/list 4 5 import gleam/option.{type Option, None, Some} 5 6 import gleam/result 6 7 import gleam/string ··· 41 42 king_position: Int, 42 43 non_pawn_material: Int, 43 44 pawn_material: Int, 44 - piece_square_score: Int, 45 + piece_square_score_midgame: Int, 46 + piece_square_score_endgame: Int, 45 47 ) 46 48 } 47 49 ··· 68 70 69 71 let attack_information = attack.calculate(board, white_king_position, to_move) 70 72 71 - let phase = phase(non_pawn_material, non_pawn_material) 72 - let #(white_piece_scores, black_piece_scores) = 73 - piece_square_scores(board, phase) 73 + let #( 74 + white_piece_scores_mid, 75 + white_piece_scores_end, 76 + black_piece_scores_mid, 77 + black_piece_scores_end, 78 + ) = piece_square_scores(board) 74 79 75 80 Game( 76 81 board:, ··· 86 91 king_position: white_king_position, 87 92 non_pawn_material:, 88 93 pawn_material:, 89 - piece_square_score: white_piece_scores, 94 + piece_square_score_midgame: white_piece_scores_mid, 95 + piece_square_score_endgame: white_piece_scores_end, 90 96 ), 91 97 black_pieces: PieceInfo( 92 98 king_position: black_king_position, 93 99 non_pawn_material:, 94 100 pawn_material:, 95 - piece_square_score: black_piece_scores, 101 + piece_square_score_midgame: black_piece_scores_mid, 102 + piece_square_score_endgame: black_piece_scores_end, 96 103 ), 97 104 ) 98 105 } 99 106 100 - fn piece_square_scores(board: board.Board, phase: Int) -> #(Int, Int) { 101 - use #(white_score, black_score), position, #(piece, colour) <- dict.fold( 107 + fn piece_square_scores(board: board.Board) -> #(Int, Int, Int, Int) { 108 + use #(white_mid, white_end, black_mid, black_end), position, #(piece, colour) <- dict.fold( 102 109 board, 103 - #(0, 0), 110 + #(0, 0, 0, 0), 104 111 ) 105 - let score = piece_table.piece_score(piece, colour, position, phase) 112 + let mid_score = piece_table.piece_score_midgame(piece, colour, position) 113 + let end_score = piece_table.piece_score_endgame(piece, colour, position) 106 114 case colour { 107 - Black -> #(white_score, black_score + score) 108 - White -> #(white_score + score, black_score) 115 + Black -> #( 116 + white_mid, 117 + white_end, 118 + black_mid + mid_score, 119 + black_end + end_score, 120 + ) 121 + White -> #( 122 + white_mid + mid_score, 123 + white_end + end_score, 124 + black_mid, 125 + black_end, 126 + ) 109 127 } 110 128 } 111 129 ··· 168 186 } 169 187 let attack_information = attack.calculate(board, king_position, to_move) 170 188 171 - let phase = phase(white_non_pawn_material, black_non_pawn_material) 172 - let #(white_piece_scores, black_piece_scores) = 173 - piece_square_scores(board, phase) 189 + let #( 190 + white_piece_scores_mid, 191 + white_piece_scores_end, 192 + black_piece_scores_mid, 193 + black_piece_scores_end, 194 + ) = piece_square_scores(board) 174 195 175 196 Game( 176 197 board:, ··· 186 207 king_position: white_king_position, 187 208 non_pawn_material: white_non_pawn_material, 188 209 pawn_material: white_pawn_material, 189 - piece_square_score: white_piece_scores, 210 + piece_square_score_midgame: white_piece_scores_mid, 211 + piece_square_score_endgame: white_piece_scores_end, 190 212 ), 191 213 black_pieces: PieceInfo( 192 214 king_position: black_king_position, 193 215 non_pawn_material: black_non_pawn_material, 194 216 pawn_material: black_pawn_material, 195 - piece_square_score: black_piece_scores, 217 + piece_square_score_midgame: black_piece_scores_mid, 218 + piece_square_score_endgame: black_piece_scores_end, 196 219 ), 197 220 ) 198 221 } ··· 352 375 353 376 let attack_information = attack.calculate(board, king_position, to_move) 354 377 355 - let phase = phase(white_non_pawn_material, black_non_pawn_material) 356 - let #(white_piece_scores, black_piece_scores) = 357 - piece_square_scores(board, phase) 378 + let #( 379 + white_piece_scores_mid, 380 + white_piece_scores_end, 381 + black_piece_scores_mid, 382 + black_piece_scores_end, 383 + ) = piece_square_scores(board) 358 384 359 385 Ok(Game( 360 386 board:, ··· 370 396 king_position: white_king_position, 371 397 non_pawn_material: white_non_pawn_material, 372 398 pawn_material: white_pawn_material, 373 - piece_square_score: white_piece_scores, 399 + piece_square_score_midgame: white_piece_scores_mid, 400 + piece_square_score_endgame: white_piece_scores_end, 374 401 ), 375 402 black_pieces: PieceInfo( 376 403 king_position: black_king_position, 377 404 non_pawn_material: black_non_pawn_material, 378 405 pawn_material: black_pawn_material, 379 - piece_square_score: black_piece_scores, 406 + piece_square_score_midgame: black_piece_scores_mid, 407 + piece_square_score_endgame: black_piece_scores_end, 380 408 ), 381 409 )) 382 410 } ··· 466 494 pub fn is_insufficient_material(game: Game) -> Bool { 467 495 game.black_pieces.pawn_material == 0 468 496 && game.white_pieces.pawn_material == 0 469 - && { 470 - game.black_pieces.non_pawn_material == board.bishop_value 471 - || game.black_pieces.non_pawn_material == board.knight_value 472 - || game.black_pieces.non_pawn_material == 0 473 - } 474 - && { 475 - game.white_pieces.non_pawn_material == board.bishop_value 476 - || game.white_pieces.non_pawn_material == board.knight_value 477 - || game.white_pieces.non_pawn_material == 0 478 - } 497 + && game.black_pieces.non_pawn_material <= board.bishop_value 498 + && game.white_pieces.non_pawn_material <= board.bishop_value 479 499 } 480 500 481 501 pub fn is_threefold_repetition(game: Game) -> Bool { ··· 504 524 505 525 const phase_multiplier = 128 506 526 507 - /// About queen + rook, so one major piece per side 508 - const endgame_material = 1400 527 + // Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/ 528 + 529 + /// About queen + rook, so one major piece per side. If total material is less 530 + /// than this, then we are completely in the endgame. 531 + const endgame_material = 3915 509 532 510 - /// Below this material limit, the endgame weight is zero. this is about enough 511 - /// for three minor pieces to be captured. 512 - const middlegame_material = 3000 533 + /// Above this material limit, the endgame weight is zero. this is about enough 534 + /// for three minor pieces to be captured . 535 + const middlegame_material = 15_258 513 536 514 - pub fn phase(white_material: Int, black_material: Int) -> Int { 515 - let non_pawn_material = white_material + black_material 537 + pub fn phase(game: Game) -> Int { 538 + let non_pawn_material = 539 + game.white_pieces.non_pawn_material + game.black_pieces.non_pawn_material 516 540 517 541 let clamped_material = case non_pawn_material > middlegame_material { 518 542 True -> middlegame_material ··· 527 551 * phase_multiplier 528 552 / { middlegame_material - endgame_material } 529 553 } 554 + 555 + pub fn interpolate_phase( 556 + middlegame_value: Int, 557 + endgame_value: Int, 558 + phase: Int, 559 + ) -> Int { 560 + { middlegame_value * { phase_multiplier - phase } + endgame_value * phase } 561 + / phase_multiplier 562 + } 563 + 564 + /// Statically evaluates a position. Does not take into account checkmate or 565 + /// stalemate, those must be accounted for beforehand. 566 + pub fn evaluation(game: Game, legal_moves: List(move)) -> Int { 567 + let phase = phase(game) 568 + 569 + let black_position_score = position_score(game.black_pieces, phase) 570 + let white_position_score = position_score(game.white_pieces, phase) 571 + 572 + let position_score = case game.to_move { 573 + board.Black -> black_position_score - white_position_score 574 + board.White -> white_position_score - black_position_score 575 + } 576 + 577 + position_score + list.length(legal_moves) 578 + } 579 + 580 + fn position_score(pieces: PieceInfo, phase: Int) -> Int { 581 + let PieceInfo( 582 + king_position: _, 583 + non_pawn_material:, 584 + pawn_material:, 585 + piece_square_score_midgame:, 586 + piece_square_score_endgame:, 587 + ) = pieces 588 + 589 + // Pawns become much more valuable in the endgame, about 1.5x 590 + let pawn_material_endgame = pawn_material * 15 / 10 591 + 592 + non_pawn_material 593 + + interpolate_phase(pawn_material, pawn_material_endgame, phase) 594 + + interpolate_phase( 595 + piece_square_score_midgame, 596 + piece_square_score_endgame, 597 + phase, 598 + ) 599 + }
-3
src/starfish/internal/move/attack.gleam
··· 345 345 board, 346 346 position, 347 347 king_position, 348 - False, 349 348 direction, 350 349 [position], 351 350 ) ··· 361 360 board: Board, 362 361 position: Int, 363 362 king_position: Int, 364 - found_king: Bool, 365 363 direction: Direction, 366 364 line: List(Int), 367 365 ) -> List(Int) { ··· 374 372 board, 375 373 new_position, 376 374 king_position, 377 - found_king, 378 375 direction, 379 376 [new_position, ..line], 380 377 )
+285 -139
src/starfish/internal/move.gleam
··· 13 13 14 14 pub type Move { 15 15 Castle(from: Int, to: Int) 16 - Move(from: Int, to: Int) 17 - Capture(from: Int, to: Int) 16 + Move(from: Int, to: Int, piece: board.Piece) 17 + Capture(from: Int, to: Int, piece: board.Piece, captured_piece: board.Piece) 18 18 EnPassant(from: Int, to: Int) 19 - Promotion(from: Int, to: Int, piece: board.Piece) 19 + Promotion( 20 + from: Int, 21 + to: Int, 22 + piece: board.Piece, 23 + captured_piece: Option(board.Piece), 24 + ) 25 + } 26 + 27 + pub fn moving_piece(move: Move) -> board.Piece { 28 + case move { 29 + Capture(piece:, ..) | Move(piece:, ..) -> piece 30 + Castle(..) -> board.King 31 + EnPassant(..) | Promotion(..) -> board.Pawn 32 + } 20 33 } 21 34 22 35 pub fn legal(game: Game) -> List(Move) { ··· 83 96 ) -> List(Move) { 84 97 case piece { 85 98 board.Bishop -> 86 - sliding_moves(game, position, moves, direction.bishop_directions) 99 + sliding_moves(game, piece, position, moves, direction.bishop_directions) 87 100 board.Rook -> 88 - sliding_moves(game, position, moves, direction.rook_directions) 101 + sliding_moves(game, piece, position, moves, direction.rook_directions) 89 102 board.Queen -> 90 - sliding_moves(game, position, moves, direction.queen_directions) 103 + sliding_moves(game, piece, position, moves, direction.queen_directions) 91 104 board.King -> king_moves(game, position, moves, direction.queen_directions) 92 105 board.Knight -> 93 106 knight_moves(game, position, moves, direction.knight_directions) ··· 120 133 { 121 134 False -> moves 122 135 True if is_promotion -> 123 - add_promotions(position, forward_one, moves, board.pawn_promotions) 124 - True -> [Move(from: position, to: forward_one), ..moves] 136 + add_promotions( 137 + position, 138 + forward_one, 139 + None, 140 + moves, 141 + board.pawn_promotions, 142 + ) 143 + True -> [Move(board.Pawn, from: position, to: forward_one), ..moves] 125 144 } 126 145 127 146 let can_double_move = case game.to_move, position / 8 { ··· 136 155 board.Empty -> 137 156 case can_move(position, forward_two, game.attack_information) { 138 157 False -> moves 139 - True -> [Move(from: position, to: forward_two), ..moves] 158 + True -> [Move(board.Pawn, from: position, to: forward_two), ..moves] 140 159 } 141 160 board.Occupied(_, _) | board.OffBoard -> moves 142 161 } ··· 146 165 147 166 let new_position = direction.in_direction(position, left) 148 167 let moves = case board.get(game.board, new_position) { 149 - board.Occupied(colour:, ..) if colour != game.to_move -> 168 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 150 169 case can_move(position, new_position, game.attack_information) { 151 170 False -> moves 152 171 True if is_promotion -> 153 - add_promotions(position, new_position, moves, board.pawn_promotions) 154 - True -> [Capture(from: position, to: new_position), ..moves] 172 + add_promotions( 173 + position, 174 + new_position, 175 + Some(captured_piece), 176 + moves, 177 + board.pawn_promotions, 178 + ) 179 + True -> [ 180 + Capture(board.Pawn, from: position, to: new_position, captured_piece:), 181 + ..moves 182 + ] 155 183 } 156 184 board.Empty if game.en_passant_square == Some(new_position) -> 157 185 case en_passant_is_valid(game, position, new_position) { ··· 163 191 164 192 let new_position = direction.in_direction(position, right) 165 193 case board.get(game.board, new_position) { 166 - board.Occupied(colour:, ..) if colour != game.to_move -> 194 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 167 195 case can_move(position, new_position, game.attack_information) { 168 196 False -> moves 169 197 True if is_promotion -> 170 - add_promotions(position, new_position, moves, board.pawn_promotions) 171 - True -> [Capture(from: position, to: new_position), ..moves] 198 + add_promotions( 199 + position, 200 + new_position, 201 + Some(captured_piece), 202 + moves, 203 + board.pawn_promotions, 204 + ) 205 + True -> [ 206 + Capture(board.Pawn, from: position, to: new_position, captured_piece:), 207 + ..moves 208 + ] 172 209 } 173 210 board.Empty if game.en_passant_square == Some(new_position) -> 174 211 case en_passant_is_valid(game, position, new_position) { ··· 288 325 fn add_promotions( 289 326 from: Int, 290 327 to: Int, 328 + captured_piece: Option(board.Piece), 291 329 moves: List(Move), 292 330 pieces: List(board.Piece), 293 331 ) -> List(Move) { 294 332 case pieces { 295 333 [] -> moves 296 334 [piece, ..pieces] -> 297 - add_promotions(from, to, [Promotion(from:, to:, piece:), ..moves], pieces) 335 + add_promotions( 336 + from, 337 + to, 338 + captured_piece, 339 + [Promotion(from:, to:, piece:, captured_piece:), ..moves], 340 + pieces, 341 + ) 298 342 } 299 343 } 300 344 ··· 312 356 board.Empty -> 313 357 case can_move(position, new_position, game.attack_information) { 314 358 False -> moves 315 - True -> [Move(from: position, to: new_position), ..moves] 359 + True -> [ 360 + Move(board.Knight, from: position, to: new_position), 361 + ..moves 362 + ] 316 363 } 317 - board.Occupied(colour:, ..) if colour != game.to_move -> 364 + board.Occupied(colour:, piece: captured_piece) 365 + if colour != game.to_move 366 + -> 318 367 case can_move(position, new_position, game.attack_information) { 319 368 False -> moves 320 - True -> [Capture(from: position, to: new_position), ..moves] 369 + True -> [ 370 + Capture( 371 + board.Knight, 372 + from: position, 373 + to: new_position, 374 + captured_piece:, 375 + ), 376 + ..moves 377 + ] 321 378 } 322 379 board.Occupied(_, _) | board.OffBoard -> moves 323 380 } ··· 388 445 board.Empty -> 389 446 case king_can_move(new_position, game.attack_information) { 390 447 False -> moves 391 - True -> [Move(from: position, to: new_position), ..moves] 448 + True -> [ 449 + Move(board.King, from: position, to: new_position), 450 + ..moves 451 + ] 392 452 } 393 - board.Occupied(colour:, ..) if colour != game.to_move -> 453 + board.Occupied(colour:, piece: captured_piece) 454 + if colour != game.to_move 455 + -> 394 456 case king_can_move(new_position, game.attack_information) { 395 457 False -> moves 396 - True -> [Capture(from: position, to: new_position), ..moves] 458 + True -> [ 459 + Capture( 460 + board.King, 461 + from: position, 462 + to: new_position, 463 + captured_piece:, 464 + ), 465 + ..moves 466 + ] 397 467 } 398 468 board.Occupied(_, _) | board.OffBoard -> moves 399 469 } ··· 405 475 406 476 fn sliding_moves( 407 477 game: Game, 478 + piece: board.Piece, 408 479 position: Int, 409 480 moves: List(Move), 410 481 directions: List(Direction), ··· 414 485 [direction, ..directions] -> 415 486 sliding_moves( 416 487 game, 488 + piece, 417 489 position, 418 - sliding_moves_in_direction(game, position, position, direction, moves), 490 + sliding_moves_in_direction( 491 + game, 492 + piece, 493 + position, 494 + position, 495 + direction, 496 + moves, 497 + ), 419 498 directions, 420 499 ) 421 500 } ··· 423 502 424 503 fn sliding_moves_in_direction( 425 504 game: Game, 505 + piece: board.Piece, 426 506 start_position: Int, 427 507 position: Int, 428 508 direction: Direction, ··· 433 513 board.Empty -> 434 514 sliding_moves_in_direction( 435 515 game, 516 + piece, 436 517 start_position, 437 518 new_position, 438 519 direction, 439 520 case can_move(start_position, new_position, game.attack_information) { 440 521 False -> moves 441 - True -> [Move(from: start_position, to: new_position), ..moves] 522 + True -> [Move(piece, from: start_position, to: new_position), ..moves] 442 523 }, 443 524 ) 444 - board.Occupied(colour:, ..) if colour != game.to_move -> 525 + board.Occupied(colour:, piece: captured_piece) if colour != game.to_move -> 445 526 case can_move(start_position, new_position, game.attack_information) { 446 527 False -> moves 447 - True -> [Capture(from: start_position, to: new_position), ..moves] 528 + True -> [ 529 + Capture( 530 + piece, 531 + from: start_position, 532 + to: new_position, 533 + captured_piece:, 534 + ), 535 + ..moves 536 + ] 448 537 } 449 538 board.Occupied(_, _) | board.OffBoard -> moves 450 539 } ··· 452 541 453 542 pub fn apply(game: Game, move: Move) -> game.Game { 454 543 case move { 455 - Capture(from:, to:) -> do_apply(game, from, to, False, None, True) 544 + Capture(from:, to:, piece:, captured_piece:) -> 545 + do_apply(game, piece, from, to, False, None, Some(captured_piece)) 456 546 Castle(from:, to:) -> apply_castle(game, from, to, to % 8 == 2) 457 - EnPassant(from:, to:) -> do_apply(game, from, to, True, None, True) 458 - Move(from:, to:) -> do_apply(game, from, to, False, None, False) 459 - Promotion(from:, to:, piece:) -> 460 - do_apply(game, from, to, False, Some(piece), False) 547 + EnPassant(from:, to:) -> 548 + do_apply(game, board.Pawn, from, to, True, None, None) 549 + Move(from:, to:, piece:) -> 550 + do_apply(game, piece, from, to, False, None, None) 551 + Promotion(from:, to:, piece:, captured_piece:) -> 552 + do_apply(game, board.Pawn, from, to, False, Some(piece), captured_piece) 461 553 } 462 554 } 463 555 ··· 476 568 king_position: white_king_position, 477 569 non_pawn_material: white_non_pawn_material, 478 570 pawn_material: white_pawn_material, 479 - piece_square_score: white_piece_square_score, 571 + piece_square_score_midgame: white_piece_square_score_midgame, 572 + piece_square_score_endgame: white_piece_square_score_endgame, 480 573 ), 481 574 black_pieces: game.PieceInfo( 482 575 king_position: black_king_position, 483 576 non_pawn_material: black_non_pawn_material, 484 577 pawn_material: black_pawn_material, 485 - piece_square_score: black_piece_square_score, 578 + piece_square_score_midgame: black_piece_square_score_midgame, 579 + piece_square_score_endgame: black_piece_square_score_endgame, 486 580 ), 487 581 ) = game 488 582 ··· 517 611 |> hash.toggle_piece(rook_from, board.Rook, to_move) 518 612 |> hash.toggle_piece(rook_to, board.Rook, to_move) 519 613 520 - let phase = game.phase(white_non_pawn_material, black_non_pawn_material) 521 - 522 - let #(white_piece_square_score, black_piece_square_score) = case to_move { 614 + let #( 615 + white_piece_square_score_midgame, 616 + white_piece_square_score_endgame, 617 + black_piece_square_score_midgame, 618 + black_piece_square_score_endgame, 619 + ) = case to_move { 523 620 board.Black -> #( 524 - white_piece_square_score, 525 - black_piece_square_score 526 - - piece_table.piece_score(board.King, to_move, from, phase) 527 - - piece_table.piece_score(board.Rook, to_move, rook_from, phase) 528 - + piece_table.piece_score(board.King, to_move, to, phase) 529 - + piece_table.piece_score(board.Rook, to_move, rook_to, phase), 621 + white_piece_square_score_midgame, 622 + white_piece_square_score_endgame, 623 + black_piece_square_score_midgame 624 + - piece_table.piece_score_midgame(board.King, to_move, from) 625 + - piece_table.piece_score_midgame(board.Rook, to_move, rook_from) 626 + + piece_table.piece_score_midgame(board.King, to_move, to) 627 + + piece_table.piece_score_midgame(board.Rook, to_move, rook_to), 628 + black_piece_square_score_endgame 629 + - piece_table.piece_score_endgame(board.King, to_move, from) 630 + - piece_table.piece_score_endgame(board.Rook, to_move, rook_from) 631 + + piece_table.piece_score_endgame(board.King, to_move, to) 632 + + piece_table.piece_score_endgame(board.Rook, to_move, rook_to), 530 633 ) 531 634 board.White -> #( 532 - white_piece_square_score 533 - - piece_table.piece_score(board.King, to_move, from, phase) 534 - - piece_table.piece_score(board.Rook, to_move, rook_from, phase) 535 - + piece_table.piece_score(board.King, to_move, to, phase) 536 - + piece_table.piece_score(board.Rook, to_move, rook_to, phase), 537 - black_piece_square_score, 635 + white_piece_square_score_midgame 636 + - piece_table.piece_score_midgame(board.King, to_move, from) 637 + - piece_table.piece_score_midgame(board.Rook, to_move, rook_from) 638 + + piece_table.piece_score_midgame(board.King, to_move, to) 639 + + piece_table.piece_score_midgame(board.Rook, to_move, rook_to), 640 + white_piece_square_score_endgame 641 + - piece_table.piece_score_endgame(board.King, to_move, from) 642 + - piece_table.piece_score_endgame(board.Rook, to_move, rook_from) 643 + + piece_table.piece_score_endgame(board.King, to_move, to) 644 + + piece_table.piece_score_endgame(board.Rook, to_move, rook_to), 645 + black_piece_square_score_midgame, 646 + black_piece_square_score_endgame, 538 647 ) 539 648 } 540 649 ··· 584 693 king_position: white_king_position, 585 694 non_pawn_material: white_non_pawn_material, 586 695 pawn_material: white_pawn_material, 587 - piece_square_score: white_piece_square_score, 696 + piece_square_score_midgame: white_piece_square_score_midgame, 697 + piece_square_score_endgame: white_piece_square_score_endgame, 588 698 ), 589 699 black_pieces: game.PieceInfo( 590 700 king_position: black_king_position, 591 701 non_pawn_material: black_non_pawn_material, 592 702 pawn_material: black_pawn_material, 593 - piece_square_score: black_piece_square_score, 703 + piece_square_score_midgame: black_piece_square_score_midgame, 704 + piece_square_score_endgame: black_piece_square_score_endgame, 594 705 ), 595 706 ) 596 707 } 597 708 598 709 fn do_apply( 599 710 game: Game, 711 + piece: board.Piece, 600 712 from: Int, 601 713 to: Int, 602 714 en_passant: Bool, 603 715 promotion: Option(board.Piece), 604 - capture: Bool, 716 + captured_piece: Option(board.Piece), 605 717 ) -> Game { 606 718 let Game( 607 719 board:, ··· 622 734 king_position: our_king_position, 623 735 non_pawn_material: our_non_pawn_material, 624 736 pawn_material: our_pawn_material, 625 - piece_square_score: our_piece_square_score, 737 + piece_square_score_midgame: our_piece_square_score_midgame, 738 + piece_square_score_endgame: our_piece_square_score_endgame, 626 739 ), 627 740 game.PieceInfo( 628 741 king_position: opposing_king_position, 629 742 non_pawn_material: opposing_non_pawn_material, 630 743 pawn_material: opposing_pawn_material, 631 - piece_square_score: opposing_piece_square_score, 744 + piece_square_score_midgame: opposing_piece_square_score_midgame, 745 + piece_square_score_endgame: opposing_piece_square_score_endgame, 632 746 ), 633 747 ) = case to_move { 634 748 board.Black -> #(black_pieces, white_pieces) 635 749 board.White -> #(white_pieces, black_pieces) 636 750 } 637 751 638 - let assert board.Occupied(piece:, colour:) = board.get(board, from) 639 - as "Tried to apply move from invalid position" 752 + let our_colour = to_move 753 + let enemy_colour = case to_move { 754 + board.Black -> board.White 755 + board.White -> board.Black 756 + } 640 757 641 758 let castling = 642 759 castling 643 760 |> remove_castling(from) 644 761 |> remove_castling(to) 645 762 646 - let one_way_move = capture || piece == board.Pawn 763 + let one_way_move = captured_piece != None || piece == board.Pawn 647 764 648 765 let zobrist_hash = 649 766 previous_hash 650 767 |> hash.toggle_to_move 651 - |> hash.toggle_piece(from, piece, colour) 768 + |> hash.toggle_piece(from, piece, our_colour) 652 769 653 - let phase = 654 - game.phase(white_pieces.non_pawn_material, black_pieces.non_pawn_material) 655 - 656 - let our_piece_square_score = 657 - our_piece_square_score - piece_table.piece_score(piece, colour, from, phase) 770 + let our_piece_square_score_midgame = 771 + our_piece_square_score_midgame 772 + - piece_table.piece_score_midgame(piece, our_colour, from) 773 + let our_piece_square_score_endgame = 774 + our_piece_square_score_endgame 775 + - piece_table.piece_score_endgame(piece, our_colour, from) 658 776 659 777 let #(piece, our_pawn_material, our_non_pawn_material) = case promotion { 660 778 None -> #(piece, our_pawn_material, our_non_pawn_material) ··· 665 783 ) 666 784 } 667 785 668 - let our_piece_square_score = 669 - our_piece_square_score + piece_table.piece_score(piece, colour, to, phase) 786 + let our_piece_square_score_midgame = 787 + our_piece_square_score_midgame 788 + + piece_table.piece_score_midgame(piece, our_colour, to) 789 + let our_piece_square_score_endgame = 790 + our_piece_square_score_endgame 791 + + piece_table.piece_score_endgame(piece, our_colour, to) 670 792 671 - let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, colour) 793 + let zobrist_hash = hash.toggle_piece(zobrist_hash, to, piece, our_colour) 672 794 673 795 let #( 674 796 zobrist_hash, 675 797 opposing_pawn_material, 676 798 opposing_non_pawn_material, 677 - opposing_piece_square_score, 678 - ) = case board.get(board, to) { 679 - board.Occupied(piece: board.Pawn, colour:) -> #( 680 - hash.toggle_piece(zobrist_hash, to, board.Pawn, colour), 799 + opposing_piece_square_score_midgame, 800 + opposing_piece_square_score_endgame, 801 + ) = case captured_piece { 802 + Some(board.Pawn) -> #( 803 + hash.toggle_piece(zobrist_hash, to, board.Pawn, enemy_colour), 681 804 opposing_pawn_material - board.pawn_value, 682 805 opposing_non_pawn_material, 683 - opposing_piece_square_score 684 - - piece_table.piece_score(board.Pawn, colour, to, phase), 806 + opposing_piece_square_score_midgame 807 + - piece_table.piece_score_midgame(board.Pawn, enemy_colour, to), 808 + opposing_piece_square_score_endgame 809 + - piece_table.piece_score_endgame(board.Pawn, enemy_colour, to), 685 810 ) 686 - board.Occupied(piece:, colour:) -> #( 687 - hash.toggle_piece(zobrist_hash, to, piece, colour), 811 + Some(piece) -> #( 812 + hash.toggle_piece(zobrist_hash, to, piece, enemy_colour), 688 813 opposing_pawn_material, 689 814 opposing_non_pawn_material - board.piece_value(piece), 690 - opposing_piece_square_score 691 - - piece_table.piece_score(piece, colour, to, phase), 815 + opposing_piece_square_score_midgame 816 + - piece_table.piece_score_midgame(piece, enemy_colour, to), 817 + opposing_piece_square_score_endgame 818 + - piece_table.piece_score_endgame(piece, enemy_colour, to), 692 819 ) 693 - board.Empty | board.OffBoard -> #( 820 + None -> #( 694 821 zobrist_hash, 695 822 opposing_pawn_material, 696 823 opposing_non_pawn_material, 697 - opposing_piece_square_score, 824 + opposing_piece_square_score_midgame, 825 + opposing_piece_square_score_endgame, 698 826 ) 699 827 } 700 828 701 829 let board = 702 830 board 703 831 |> dict.delete(from) 704 - |> dict.insert(to, #(piece, colour)) 832 + |> dict.insert(to, #(piece, our_colour)) 705 833 706 - let #(board, zobrist_hash) = case en_passant, en_passant_square, colour { 834 + let #( 835 + board, 836 + zobrist_hash, 837 + opposing_pawn_material, 838 + opposing_piece_square_score_midgame, 839 + opposing_piece_square_score_endgame, 840 + ) = case en_passant, en_passant_square, our_colour { 707 841 True, Some(square), board.White -> { 708 842 let ep_square = square - 8 709 843 #( 710 844 dict.delete(board, ep_square), 711 845 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.Black), 846 + opposing_pawn_material - board.pawn_value, 847 + opposing_piece_square_score_midgame 848 + - piece_table.piece_score_midgame(board.Pawn, board.Black, ep_square), 849 + opposing_piece_square_score_endgame 850 + - piece_table.piece_score_endgame(board.Pawn, board.Black, ep_square), 712 851 ) 713 852 } 714 853 True, Some(square), board.Black -> { ··· 716 855 #( 717 856 dict.delete(board, ep_square), 718 857 hash.toggle_piece(zobrist_hash, ep_square, board.Pawn, board.White), 858 + opposing_pawn_material - board.pawn_value, 859 + opposing_piece_square_score_midgame 860 + - piece_table.piece_score_midgame(board.Pawn, board.White, ep_square), 861 + opposing_piece_square_score_endgame 862 + - piece_table.piece_score_endgame(board.Pawn, board.White, ep_square), 719 863 ) 720 864 } 721 - _, _, _ -> #(board, zobrist_hash) 865 + _, _, _ -> #( 866 + board, 867 + zobrist_hash, 868 + opposing_pawn_material, 869 + opposing_piece_square_score_midgame, 870 + opposing_piece_square_score_endgame, 871 + ) 722 872 } 723 873 724 874 let en_passant_square = case piece, to - from { ··· 742 892 king_position: our_king_position, 743 893 non_pawn_material: our_non_pawn_material, 744 894 pawn_material: our_pawn_material, 745 - piece_square_score: our_piece_square_score, 895 + piece_square_score_midgame: our_piece_square_score_midgame, 896 + piece_square_score_endgame: our_piece_square_score_endgame, 746 897 ) 747 898 748 899 let opposing_pieces = ··· 750 901 king_position: opposing_king_position, 751 902 non_pawn_material: opposing_non_pawn_material, 752 903 pawn_material: opposing_pawn_material, 753 - piece_square_score: opposing_piece_square_score, 904 + piece_square_score_midgame: opposing_piece_square_score_midgame, 905 + piece_square_score_endgame: opposing_piece_square_score_endgame, 754 906 ) 755 907 756 908 let #(white_pieces, black_pieces) = case to_move { ··· 758 910 board.Black -> #(opposing_pieces, our_pieces) 759 911 } 760 912 761 - let to_move = case to_move { 762 - board.Black -> board.White 763 - board.White -> board.Black 764 - } 913 + let to_move = enemy_colour 765 914 766 915 let #(half_moves, previous_positions) = case one_way_move { 767 916 True -> #(0, []) ··· 823 972 } 824 973 825 974 pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String { 826 - let assert board.Occupied(piece:, colour: _) = 827 - board.get(game.board, move.from) 828 - as "Legal moves should only move valid pieces" 975 + let piece = moving_piece(move) 829 976 830 977 case move { 831 978 Castle(from: _, to:) -> { ··· 835 982 True -> "O-O-O" 836 983 } 837 984 } 838 - Capture(from:, to:) if piece == board.Pawn -> 985 + Capture(from:, to:, ..) if piece == board.Pawn -> 839 986 pawn_move_to_san(from, to, True, None) 840 987 EnPassant(from:, to:) -> pawn_move_to_san(from, to, True, None) 841 - Promotion(from:, to:, piece:) -> { 842 - let is_capture = case board.get(game.board, move.to) { 843 - board.Occupied(..) -> True 844 - board.Empty | board.OffBoard -> False 845 - } 846 - pawn_move_to_san(from, to, is_capture, Some(piece)) 847 - } 848 - Move(from:, to:) -> move_to_san(game, piece, from, to, False) 849 - Capture(from:, to:) -> move_to_san(game, piece, from, to, True) 988 + Promotion(from:, to:, piece:, captured_piece: None) -> 989 + pawn_move_to_san(from, to, False, Some(piece)) 990 + Promotion(from:, to:, piece:, captured_piece: Some(_)) -> 991 + pawn_move_to_san(from, to, True, Some(piece)) 992 + Move(from:, to:, ..) -> move_to_san(game, piece, from, to, False) 993 + Capture(from:, to:, ..) -> move_to_san(game, piece, from, to, True) 850 994 } 851 995 } 852 996 ··· 886 1030 use <- bool.guard(move.to != to, disambiguation) 887 1031 use <- bool.guard(move.from == from, disambiguation) 888 1032 889 - let assert board.Occupied(piece: moving_piece, colour: _) = 890 - board.get(game.board, move.from) 891 - as "Legal moves should only move valid pieces" 1033 + let moving_piece = moving_piece(move) 892 1034 893 1035 use <- bool.guard(moving_piece != piece, disambiguation) 894 1036 ··· 1019 1161 use #(first, move) <- result.try(parse_move_part(move)) 1020 1162 use #(second, move) <- result.try(parse_move_part(move)) 1021 1163 1022 - use #(from_file, from_rank, capture, to_file, to_rank, move) <- result.try( 1164 + use #(from_file, from_rank, to_file, to_rank, move) <- result.try( 1023 1165 case first, second { 1024 1166 // `xx` is not an allowed move 1025 1167 CaptureSpecifier, CaptureSpecifier -> Error(Nil) ··· 1027 1169 File(file), CaptureSpecifier -> { 1028 1170 let from_file = Some(file) 1029 1171 let from_rank = None 1030 - let capture = True 1031 1172 use #(to_file, move) <- result.try(parse_file(move)) 1032 1173 use #(to_rank, move) <- result.try(parse_rank(move)) 1033 1174 1034 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1175 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1035 1176 } 1036 1177 // We disambiguate the rank and it's a capture (e.g. `R5xc4`) 1037 1178 Rank(rank), CaptureSpecifier -> { 1038 1179 let from_file = None 1039 1180 let from_rank = Some(rank) 1040 - let capture = True 1041 1181 use #(to_file, move) <- result.try(parse_file(move)) 1042 1182 use #(to_rank, move) <- result.try(parse_rank(move)) 1043 1183 1044 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1184 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1045 1185 } 1046 1186 // It's a capture, and we've parsed the file of the destination (e.g. 1047 1187 // `Bxa5`) 1048 1188 CaptureSpecifier, File(to_file) -> { 1049 1189 let from_file = None 1050 1190 let from_rank = None 1051 - let capture = True 1052 1191 use #(to_rank, move) <- result.try(parse_rank(move)) 1053 1192 1054 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1193 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1055 1194 } 1056 1195 // We disambiguate the file and we've parsed the file of the destination 1057 1196 // (e.g. `Qhd4`) 1058 1197 File(from_file), File(to_file) -> { 1059 1198 let from_file = Some(from_file) 1060 1199 let from_rank = None 1061 - let capture = False 1062 1200 use #(to_rank, move) <- result.try(parse_rank(move)) 1063 1201 1064 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1202 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1065 1203 } 1066 1204 // We disambiguate the rank and we've parsed the file of the destination 1067 1205 // (e.g. `R7d2`) 1068 1206 Rank(rank), File(to_file) -> { 1069 1207 let from_file = None 1070 1208 let from_rank = Some(rank) 1071 - let capture = False 1072 1209 use #(to_rank, move) <- result.try(parse_rank(move)) 1073 1210 1074 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1211 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1075 1212 } 1076 1213 // Capture followed by a rank is not allowed, e.g. `Rx1` 1077 1214 CaptureSpecifier, Rank(_) -> Error(Nil) 1078 1215 // We've parsed the file and rank, and there's no more move to parse, 1079 1216 // so we're done. (e.g. `Nf3`) 1080 1217 File(file), Rank(rank) if move == "" -> 1081 - Ok(#(None, None, False, file, rank, move)) 1218 + Ok(#(None, None, file, rank, move)) 1082 1219 // We've disambiguated the rank and file, and we still need to parse 1083 1220 // the rest of the move. (e.g. `Qh4xe1`) 1084 1221 File(from_file), Rank(from_rank) -> ··· 1086 1223 Ok(#(CaptureSpecifier, move)) -> { 1087 1224 let from_file = Some(from_file) 1088 1225 let from_rank = Some(from_rank) 1089 - let capture = True 1090 1226 use #(to_file, move) <- result.try(parse_file(move)) 1091 1227 use #(to_rank, move) <- result.try(parse_rank(move)) 1092 1228 1093 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1229 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1094 1230 } 1095 1231 Ok(#(File(to_file), _)) -> { 1096 1232 let from_file = Some(from_file) 1097 1233 let from_rank = Some(from_rank) 1098 - let capture = False 1099 1234 use #(to_rank, move) <- result.try(parse_rank(move)) 1100 1235 1101 - Ok(#(from_file, from_rank, capture, to_file, to_rank, move)) 1236 + Ok(#(from_file, from_rank, to_file, to_rank, move)) 1102 1237 } 1103 1238 Ok(#(Rank(_), _)) | Error(_) -> Error(Nil) 1104 1239 } ··· 1111 1246 1112 1247 let to = to_rank * 8 + to_file 1113 1248 1114 - case get_pieces(game, piece_kind, legal_moves, from_file, from_rank, to) { 1115 - [from] if capture -> Ok(Capture(from:, to:)) 1116 - [from] -> Ok(Move(from:, to:)) 1249 + case 1250 + get_moves(game, piece_kind, legal_moves, from_file, from_rank, to, None) 1251 + { 1252 + [move] -> Ok(move) 1117 1253 // If there is more than one valid move, the notation is ambiguous, and 1118 1254 // so we error. If there are no valid moves, we also error. 1119 1255 _ -> Error(Nil) ··· 1178 1314 ) -> Result(Move, Nil) { 1179 1315 use #(file, move) <- result.try(parse_file(move)) 1180 1316 1181 - use #(from_file, is_capture, to_file, move) <- result.try(case move { 1317 + use #(from_file, to_file, move) <- result.try(case move { 1182 1318 "x" <> move -> 1183 1319 parse_file(move) 1184 1320 |> result.map(fn(pair) { 1185 1321 let #(to_file, move) = pair 1186 - #(Some(file), True, to_file, move) 1322 + #(Some(file), to_file, move) 1187 1323 }) 1188 - _ -> Ok(#(None, False, file, move)) 1324 + _ -> Ok(#(None, file, move)) 1189 1325 }) 1190 1326 1191 1327 use #(rank, move) <- result.try(parse_rank(move)) ··· 1202 1338 let to = rank * 8 + to_file 1203 1339 1204 1340 case 1205 - get_pieces(game, board.Pawn, legal_moves, from_file, None, to), 1206 - promotion 1341 + get_moves(game, board.Pawn, legal_moves, from_file, None, to, promotion) 1207 1342 { 1208 - [from], Some(piece) -> Ok(Promotion(from:, to:, piece:)) 1209 - [from], _ if game.en_passant_square == Some(to) -> Ok(EnPassant(from:, to:)) 1210 - [from], _ if is_capture -> Ok(Capture(from:, to:)) 1211 - [from], _ -> Ok(Move(from:, to:)) 1212 - _, _ -> Error(Nil) 1343 + [move] -> Ok(move) 1344 + _ -> Error(Nil) 1213 1345 } 1214 1346 } 1215 1347 1216 - /// Gets the possible destination squares for a move, based on the information 1217 - /// we know. 1218 - fn get_pieces( 1348 + /// Gets the possible moves for a piece, based on the information we know from 1349 + /// SAN. 1350 + fn get_moves( 1219 1351 game: Game, 1220 1352 find_piece: board.Piece, 1221 1353 legal_moves: List(Move), 1222 1354 from_file: option.Option(Int), 1223 1355 from_rank: option.Option(Int), 1224 1356 to: Int, 1225 - ) -> List(Int) { 1226 - use pieces, position, #(piece, colour) <- dict.fold(game.board, []) 1357 + promotion: Option(board.Piece), 1358 + ) -> List(Move) { 1359 + use moves, position, #(piece, colour) <- dict.fold(game.board, []) 1227 1360 let is_valid = 1228 1361 colour == game.to_move 1229 1362 && piece == find_piece ··· 1235 1368 None -> True 1236 1369 Some(rank) -> rank == position / 8 1237 1370 } 1238 - && list.any(legal_moves, fn(move) { move.to == to && move.from == position }) 1239 1371 1240 1372 case is_valid { 1241 - False -> pieces 1242 - True -> [position, ..pieces] 1373 + False -> moves 1374 + True -> 1375 + case 1376 + list.find(legal_moves, fn(move) { 1377 + let valid = move.to == to && move.from == position 1378 + case move, promotion { 1379 + Promotion(piece:, ..), Some(promotion) if piece == promotion -> 1380 + valid 1381 + Promotion(..), _ -> False 1382 + _, _ -> valid 1383 + } 1384 + }) 1385 + { 1386 + Error(_) -> moves 1387 + Ok(move) -> [move, ..moves] 1388 + } 1243 1389 } 1244 1390 } 1245 1391
+133 -86
src/starfish/internal/piece_table.gleam
··· 6 6 7 7 import starfish/internal/board 8 8 9 - // Values taken from https://www.chessprogramming.org/Simplified_Evaluation_Function 9 + // Values taken from https://hxim.github.io/Stockfish-Evaluation-Guide/ 10 10 11 11 // We use tuples for constant time accessing. The values are constant so we don't 12 12 // need to worry about the cost of updating. 13 13 14 14 const pawn = #( 15 - #(0, 0, 0, 0, 0, 0, 0, 0), 16 - #(30, 30, 30, 30, 30, 30, 30, 30), 17 - #(10, 10, 20, 30, 30, 20, 10, 10), 18 - #(5, 5, 10, 25, 25, 10, 5, 5), 19 - #(0, 0, 0, 20, 20, 0, 0, 0), 20 - #(5, -5, -10, 0, 0, -10, -5, 5), 21 - #(5, 10, 10, -20, -20, 10, 10, 5), 22 - #(0, 0, 0, 0, 0, 0, 0, 0), 15 + #(000, 000, 000, 000, 000, 000, 000, 000), 16 + #(003, 003, 010, 019, 016, 019, 007, -005), 17 + #(-009, -015, 011, 015, 032, 022, 005, -022), 18 + #(-004, -023, 006, 020, 040, 017, 004, -008), 19 + #(013, 000, -013, 001, 011, -002, -013, 005), 20 + #(005, -012, -007, 022, -008, -005, -015, -008), 21 + #(-007, 007, -003, -013, 005, -016, 010, -008), 22 + #(000, 000, 000, 000, 000, 000, 000, 000), 23 23 ) 24 24 25 25 const knight = #( 26 - #(-50, -40, -30, -30, -30, -30, -40, -50), 27 - #(-40, -20, 0, 0, 0, 0, -20, -40), 28 - #(-30, 0, 10, 15, 15, 10, 0, -30), 29 - #(-30, 5, 15, 20, 20, 15, 5, -30), 30 - #(-30, 0, 15, 20, 20, 15, 0, -30), 31 - #(-30, 5, 10, 15, 15, 10, 5, -30), 32 - #(-40, -20, 0, 5, 5, 0, -20, -40), 33 - #(-50, -40, -30, -30, -30, -30, -40, -50), 26 + #(-175, -092, -074, -073, -073, -074, -092, -175), 27 + #(-077, -041, -027, -015, -015, -027, -041, -077), 28 + #(-061, -017, 006, 012, 012, 006, -017, -061), 29 + #(-035, 008, 040, 049, 049, 040, 008, -035), 30 + #(-034, 013, 044, 051, 051, 044, 013, -034), 31 + #(-009, 022, 058, 053, 053, 058, 022, -009), 32 + #(-067, -027, 004, 037, 037, 004, -027, -067), 33 + #(-201, -083, -056, -026, -026, -056, -083, -201), 34 34 ) 35 35 36 36 const bishop = #( 37 - #(-20, -10, -10, -10, -10, -10, -10, -20), 38 - #(-10, 0, 0, 0, 0, 0, 0, -10), 39 - #(-10, 0, 5, 10, 10, 5, 0, -10), 40 - #(-10, 5, 5, 10, 10, 5, 5, -10), 41 - #(-10, 0, 10, 10, 10, 10, 0, -10), 42 - #(-10, 10, 10, 10, 10, 10, 10, -10), 43 - #(-10, 5, 0, 0, 0, 0, 5, -10), 44 - #(-20, -10, -10, -10, -10, -10, -10, -20), 37 + #(-053, -005, -008, -023, -023, -008, -005, -053), 38 + #(-015, 008, 019, 004, 004, 019, 008, -015), 39 + #(-007, 021, -005, 017, 017, -005, 021, -007), 40 + #(-005, 011, 025, 039, 039, 025, 011, -005), 41 + #(-012, 029, 022, 031, 031, 022, 029, -012), 42 + #(-016, 006, 001, 011, 011, 001, 006, -016), 43 + #(-017, -014, 005, 000, 000, 005, -014, -017), 44 + #(-048, 001, -014, -023, -023, -014, 001, -048), 45 45 ) 46 46 47 47 const rook = #( 48 - #(0, 0, 0, 0, 0, 0, 0, 0), 49 - #(5, 10, 10, 10, 10, 10, 10, 5), 50 - #(-5, 0, 0, 0, 0, 0, 0, -5), 51 - #(-5, 0, 0, 0, 0, 0, 0, -5), 52 - #(-5, 0, 0, 0, 0, 0, 0, -5), 53 - #(-5, 0, 0, 0, 0, 0, 0, -5), 54 - #(-5, 0, 0, 0, 0, 0, 0, -5), 55 - #(0, 0, 0, 5, 5, 0, 0, 0), 48 + #(-031, -020, -014, -005, -005, -014, -020, -031), 49 + #(-021, -013, -008, 006, 006, -008, -013, -021), 50 + #(-025, -011, -001, 003, 003, -001, -011, -025), 51 + #(-013, -005, -004, -006, -006, -004, -005, -013), 52 + #(-027, -015, -004, 003, 003, -004, -015, -027), 53 + #(-022, -002, 006, 012, 012, 006, -002, -022), 54 + #(-002, 012, 016, 018, 018, 016, 012, -002), 55 + #(-017, -019, -001, 009, 009, -001, -019, -017), 56 56 ) 57 57 58 58 const queen = #( 59 - #(-20, -10, -10, -5, -5, -10, -10, -20), 60 - #(-10, 0, 0, 0, 0, 0, 0, -10), 61 - #(-10, 0, 5, 5, 5, 5, 0, -10), 62 - #(-5, 0, 5, 5, 5, 5, 0, -5), 63 - #(0, 0, 5, 5, 5, 5, 0, -5), 64 - #(-10, 5, 5, 5, 5, 5, 0, -10), 65 - #(-10, 0, 5, 0, 0, 0, 0, -10), 66 - #(-20, -10, -10, -5, -5, -10, -10, -20), 59 + #(003, -005, -005, 004, 004, -005, -005, 003), 60 + #(-003, 005, 008, 012, 012, 008, 005, -003), 61 + #(-003, 006, 013, 007, 007, 013, 006, -003), 62 + #(004, 005, 009, 008, 008, 009, 005, 004), 63 + #(000, 014, 012, 005, 005, 012, 014, 000), 64 + #(-004, 010, 006, 008, 008, 006, 010, -004), 65 + #(-005, 006, 010, 008, 008, 010, 006, -005), 66 + #(-002, -002, 001, -002, -002, 001, -002, -002), 67 67 ) 68 68 69 69 const king = #( 70 - #(-30, -40, -40, -50, -50, -40, -40, -30), 71 - #(-30, -40, -40, -50, -50, -40, -40, -30), 72 - #(-30, -40, -40, -50, -50, -40, -40, -30), 73 - #(-30, -40, -40, -50, -50, -40, -40, -30), 74 - #(-20, -30, -30, -40, -40, -30, -30, -20), 75 - #(-10, -20, -20, -20, -20, -20, -20, -10), 76 - #(20, 20, 0, 0, 0, 0, 20, 20), 77 - #(20, 30, 10, 0, 0, 10, 30, 20), 70 + #(271, 327, 271, 198, 198, 271, 327, 271), 71 + #(278, 303, 234, 179, 179, 234, 303, 278), 72 + #(195, 258, 169, 120, 120, 169, 258, 195), 73 + #(164, 190, 138, 098, 098, 138, 190, 164), 74 + #(154, 179, 105, 070, 070, 105, 179, 154), 75 + #(123, 145, 081, 031, 031, 081, 145, 123), 76 + #(088, 120, 065, 033, 033, 065, 120, 088), 77 + #(059, 089, 045, -001, -001, 045, 089, 059), 78 + ) 79 + 80 + /// In the middlegame, pawns are encouraged to protect the king's castling 81 + /// squares. In the endgame though, they no longer need to protect the king and 82 + /// instead should promote. Therefore, we use a different table to encourage this. 83 + const pawn_endgame = #( 84 + #(000, 000, 000, 000, 000, 000, 000, 000), 85 + #(-010, -006, 010, 000, 014, 007, -005, -019), 86 + #(-010, -010, -010, 004, 004, 003, -006, -004), 87 + #(006, -002, -008, -004, -013, -012, -010, -009), 88 + #(010, 005, 004, -005, -005, -005, 014, 009), 89 + #(028, 020, 021, 028, 030, 007, 006, 013), 90 + #(000, -011, 012, 021, 025, 019, 004, 007), 91 + #(000, 000, 000, 000, 000, 000, 000, 000), 92 + ) 93 + 94 + const knight_endgame = #( 95 + #(-096, -065, -049, -021, -021, -049, -065, -096), 96 + #(-067, -054, -018, 008, 008, -018, -054, -067), 97 + #(-040, -027, -008, 029, 029, -008, -027, -040), 98 + #(-035, -002, 013, 028, 028, 013, -002, -035), 99 + #(-045, -016, 009, 039, 039, 009, -016, -045), 100 + #(-051, -044, -016, 017, 017, -016, -044, -051), 101 + #(-069, -050, -051, 012, 012, -051, -050, -069), 102 + #(-100, -088, -056, -017, -017, -056, -088, -100), 103 + ) 104 + 105 + const bishop_endgame = #( 106 + #(-057, -030, -037, -012, -012, -037, -030, -057), 107 + #(-037, -013, -017, 001, 001, -017, -013, -037), 108 + #(-016, -001, -002, 010, 010, -002, -001, -016), 109 + #(-020, -006, 000, 017, 017, 000, -006, -020), 110 + #(-017, -001, -014, 015, 015, -014, -001, -017), 111 + #(-030, 006, 004, 006, 006, 004, 006, -030), 112 + #(-031, -020, -001, 001, 001, -001, -020, -031), 113 + #(-046, -042, -037, -024, -024, -037, -042, -046), 114 + ) 115 + 116 + const rook_endgame = #( 117 + #(-009, -013, -010, -009, -009, -010, -013, -009), 118 + #(-012, -009, -001, -002, -002, -001, -009, -012), 119 + #(006, -008, -002, -006, -006, -002, -008, 006), 120 + #(-006, 001, -009, 007, 007, -009, 001, -006), 121 + #(-005, 008, 007, -006, -006, 007, 008, -005), 122 + #(006, 001, -007, 010, 010, -007, 001, 006), 123 + #(004, 005, 020, -005, -005, 020, 005, 004), 124 + #(018, 000, 019, 013, 013, 019, 000, 018), 125 + ) 126 + 127 + const queen_endgame = #( 128 + #(-069, -057, -047, -026, -026, -047, -057, -069), 129 + #(-055, -031, -022, -004, -004, -022, -031, -055), 130 + #(-039, -018, -009, 003, 003, -009, -018, -039), 131 + #(-023, -003, 013, 024, 024, 013, -003, -023), 132 + #(-029, -006, 009, 021, 021, 009, -006, -029), 133 + #(-038, -018, -012, 001, 001, -012, -018, -038), 134 + #(-050, -027, -024, -008, -008, -024, -027, -050), 135 + #(-075, -052, -043, -036, -036, -043, -052, -075), 78 136 ) 79 137 80 138 /// In the beginning and middle of the game, the king must be kept safe. However 81 139 /// as the game progresses towards the end, the king should become more aggressive 82 140 /// so we use a different set of scores for kings in the endgame. 83 141 const king_endgame = #( 84 - #(-50, -40, -30, -20, -20, -30, -40, -50), 85 - #(-30, -20, -10, 0, 0, -10, -20, -30), 86 - #(-30, -10, 20, 30, 30, 20, -10, -30), 87 - #(-30, -10, 30, 40, 40, 30, -10, -30), 88 - #(-30, -10, 30, 40, 40, 30, -10, -30), 89 - #(-30, -10, 20, 30, 30, 20, -10, -30), 90 - #(-30, -30, 0, 0, 0, 0, -30, -30), 91 - #(-50, -30, -30, -30, -30, -30, -30, -50), 92 - ) 93 - 94 - /// In the middlegame, pawns are encouraged to protect the king's castling 95 - /// squares. In the endgame though, they no longer need to protect the king and 96 - /// instead should promote. Therefore, we use a different table to encourage this. 97 - const pawn_endgame = #( 98 - #(100, 100, 100, 100, 100, 100, 100, 100), 99 - #(80, 80, 80, 80, 80, 80, 80, 80), 100 - #(50, 50, 50, 50, 50, 50, 50, 50), 101 - #(30, 30, 30, 30, 30, 30, 30, 30), 102 - #(10, 10, 10, 10, 10, 10, 10, 10), 103 - // Since pawns can double-move, the first two ranks are equivalent from the 104 - // pawn's perspective. 105 - #(-10, -10, -10, -10, -10, -10, -10, -10), 106 - #(-10, -10, -10, -10, -10, -10, -10, -10), 107 - #(-10, -10, -10, -10, -10, -10, -10, -10), 142 + #(001, 045, 085, 076, 076, 085, 045, 001), 143 + #(053, 100, 133, 135, 135, 133, 100, 053), 144 + #(088, 130, 169, 175, 175, 169, 130, 088), 145 + #(103, 156, 172, 172, 172, 172, 156, 103), 146 + #(096, 166, 199, 199, 199, 199, 166, 096), 147 + #(092, 172, 184, 191, 191, 184, 172, 092), 148 + #(047, 121, 116, 131, 131, 116, 121, 047), 149 + #(011, 059, 073, 078, 078, 073, 059, 011), 108 150 ) 109 151 110 152 type Table = ··· 151 193 } 152 194 } 153 195 154 - /// Calculate the score for a given piece at a position at some point in the game 155 - pub fn piece_score( 196 + /// Calculate the score for a given piece at a position during the middlegame 197 + pub fn piece_score_midgame( 156 198 piece: board.Piece, 157 199 colour: board.Colour, 158 200 position: Int, 159 - phase: Int, 160 201 ) -> Int { 161 202 let table = case piece { 162 203 board.Pawn -> pawn ··· 167 208 board.King -> king 168 209 } 169 210 170 - let middlegame_value = get(table, position, colour) 211 + get(table, position, colour) 212 + } 171 213 172 - case piece { 173 - board.King if phase > 0 -> 174 - interpolate(middlegame_value, get(king_endgame, position, colour), phase) 175 - board.Pawn if phase > 0 -> 176 - interpolate(middlegame_value, get(pawn_endgame, position, colour), phase) 177 - _ -> middlegame_value 214 + /// Calculate the score for a given piece at a position in the endgame 215 + pub fn piece_score_endgame( 216 + piece: board.Piece, 217 + colour: board.Colour, 218 + position: Int, 219 + ) -> Int { 220 + let table = case piece { 221 + board.Pawn -> pawn_endgame 222 + board.King -> king_endgame 223 + board.Bishop -> bishop_endgame 224 + board.Knight -> knight_endgame 225 + board.Queen -> queen_endgame 226 + board.Rook -> rook_endgame 178 227 } 179 - } 180 228 181 - fn interpolate(middlegame_value: Int, endgame_value: Int, phase: Int) -> Int { 182 - { middlegame_value * { 128 - phase } + endgame_value * phase } / 128 229 + get(table, position, colour) 183 230 }
+17 -18
src/starfish/internal/search.gleam
··· 3 3 import gleam/list 4 4 import gleam/option.{type Option, None, Some} 5 5 import starfish/internal/board 6 - import starfish/internal/evaluate 7 6 import starfish/internal/game.{type Game} 8 7 import starfish/internal/hash 9 8 import starfish/internal/move.{type Move} ··· 361 360 /// in order to save iterating the list a second time. The guesses are discarded 362 361 /// after this point. 363 362 fn order_moves(game: Game) -> List(#(Move, Int)) { 364 - let phase = 365 - game.phase( 366 - game.white_pieces.non_pawn_material, 367 - game.black_pieces.non_pawn_material, 368 - ) 363 + let phase = game.phase(game) 369 364 game 370 365 |> move.legal 371 366 |> collect_guessed_eval(game, phase, []) ··· 408 403 /// order than random. Searching better moves first improves alpha-beta pruning, 409 404 /// allowing us to search more positions. 410 405 fn guess_eval(game: Game, move: Move, phase: Int) -> Int { 411 - let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from) 412 - as "Invalid move trying to move empty piece" 406 + let piece = move.moving_piece(move) 407 + let colour = game.to_move 413 408 414 409 let moving_piece = case move { 415 410 move.Promotion(piece:, ..) -> piece ··· 417 412 piece 418 413 } 419 414 420 - let from_score = piece_table.piece_score(piece, colour, move.from, phase) 421 - let to_score = piece_table.piece_score(moving_piece, colour, move.to, phase) 415 + let from_score_midgame = 416 + piece_table.piece_score_midgame(piece, colour, move.from) 417 + let from_score_endgame = 418 + piece_table.piece_score_endgame(piece, colour, move.from) 419 + let from_score = 420 + game.interpolate_phase(from_score_midgame, from_score_endgame, phase) 421 + 422 + let to_score_midgame = piece_table.piece_score_midgame(piece, colour, move.to) 423 + let to_score_endgame = piece_table.piece_score_endgame(piece, colour, move.to) 424 + let to_score = 425 + game.interpolate_phase(to_score_midgame, to_score_endgame, phase) 426 + 422 427 let position_improvement = to_score - from_score 423 428 424 429 let move_specific_score = case move { 425 - // TODO store information in moves so we don't have to retrieve it from the 426 - // board every time. 427 - move.Capture(..) -> { 428 - let assert board.Occupied(piece: captured_piece, colour: _) = 429 - board.get(game.board, move.to) 430 - as "Invalid capture moving to empty square" 431 - 430 + move.Capture(captured_piece:, ..) -> { 432 431 capture_promotion_bonus 433 432 // Capturing a more valuable piece is better, and using a less valuable 434 433 // piece to capture is usually better. However, we prioritise the value of ··· 467 466 best_eval: Int, 468 467 best_opponent_move: Int, 469 468 ) -> Int { 470 - let evaluation = evaluate.evaluate(game, moves) 469 + let evaluation = game.evaluation(game, moves) 471 470 472 471 use <- bool.guard(evaluation >= best_opponent_move, evaluation) 473 472
+35 -19
src/starfish.gleam
··· 1 - import birl 2 1 import gleam/bool 2 + import gleam/list 3 3 import gleam/result 4 4 import starfish/internal/board 5 5 import starfish/internal/game 6 6 import starfish/internal/move 7 7 import starfish/internal/search 8 8 9 - pub type Game = 10 - game.Game 9 + pub opaque type Game { 10 + Game(game: game.Game) 11 + } 11 12 12 13 /// A single legal move on the chess board. 13 - pub type Move = 14 - move.Move 14 + pub opaque type Move { 15 + Move(move: move.Move) 16 + } 17 + 18 + @internal 19 + pub fn get_move(move: Move) -> move.Move { 20 + move.move 21 + } 15 22 16 23 /// The [FEN string](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) 17 24 /// representing the initial position of a chess game. ··· 36 43 /// starfish.from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR") 37 44 /// ``` 38 45 pub fn from_fen(fen: String) -> Game { 39 - game.from_fen(fen) 46 + Game(game.from_fen(fen)) 40 47 } 41 48 42 49 pub type FenParseError { ··· 84 91 /// starfish.try_from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR") 85 92 /// ``` 86 93 pub fn try_from_fen(fen: String) -> Result(Game, FenParseError) { 87 - result.map_error(game.try_from_fen(fen), convert_fen_parse_error) 94 + game.try_from_fen(fen) 95 + |> result.map_error(convert_fen_parse_error) 96 + |> result.map(Game) 88 97 } 89 98 90 99 // Since circular imports are not allowed, but we want the user to be able to ··· 107 116 108 117 /// Returns a game representing the initial position. 109 118 pub fn new() -> Game { 110 - game.initial_position() 119 + Game(game.initial_position()) 111 120 } 112 121 113 122 /// Convert a game into its FEN string representation. ··· 118 127 /// assert starfish.to_fen(starfish.new()) == starfish.starting_fen 119 128 /// ``` 120 129 pub fn to_fen(game: Game) -> String { 121 - game.to_fen(game) 130 + game.to_fen(game.game) 122 131 } 123 132 124 133 pub fn legal_moves(game: Game) -> List(Move) { 125 - move.legal(game) 134 + list.map(move.legal(game.game), Move) 126 135 } 127 136 128 137 /// Used to determine how long to search positions ··· 143 152 let until = case cutoff { 144 153 Depth(depth:) -> fn(current_depth) { current_depth > depth } 145 154 Time(milliseconds:) -> { 146 - let end_time = birl.monotonic_now() + milliseconds * 1000 147 - fn(_) { birl.monotonic_now() >= end_time } 155 + let end_time = monotonic_time() + milliseconds 156 + fn(_) { monotonic_time() >= end_time } 148 157 } 149 158 } 150 159 151 - search.best_move(game, until) 160 + result.map(search.best_move(game.game, until), Move) 152 161 } 153 162 163 + @external(erlang, "starfish_ffi", "monotonic_time") 164 + @external(javascript, "./starfish_ffi.mjs", "monotonic_time") 165 + fn monotonic_time() -> Int 166 + 154 167 pub fn apply_move(game: Game, move: Move) -> Game { 155 - move.apply(game, move) 168 + Game(move.apply(game.game, move.move)) 156 169 } 157 170 158 171 pub fn to_standard_algebraic_notation(move: Move, game: Game) -> String { 159 - move.to_standard_algebraic_notation(move, game) 172 + move.to_standard_algebraic_notation(move.move, game.game) 160 173 } 161 174 162 175 /// Convert a move to [long algebraic notation]( ··· 164 177 /// specifically the UCI format, containing the start and end positions. For 165 178 /// example, `e2e4` or `c7d8q`. 166 179 pub fn to_long_algebraic_notation(move: Move) -> String { 167 - move.to_long_algebraic_notation(move) 180 + move.to_long_algebraic_notation(move.move) 168 181 } 169 182 170 183 /// Parses a move from either long algebraic notation, in the same format as ··· 173 186 /// Returns an error if the syntax is invalid or the move is not legal on the 174 187 /// board. 175 188 pub fn parse_move(move: String, game: Game) -> Result(Move, Nil) { 176 - let legal_moves = legal_moves(game) 189 + let legal_moves = move.legal(game.game) 177 190 case move.from_long_algebraic_notation(move, legal_moves) { 178 - Ok(move) -> Ok(move) 179 - Error(_) -> move.from_standard_algebraic_notation(move, game, legal_moves) 191 + Ok(move) -> Ok(Move(move)) 192 + Error(_) -> 193 + move.from_standard_algebraic_notation(move, game.game, legal_moves) 194 + |> result.map(Move) 180 195 } 181 196 } 182 197 ··· 196 211 197 212 /// Returns the current game state: A win, draw or neither. 198 213 pub fn state(game: Game) -> GameState { 214 + let game = game.game 199 215 use <- bool.guard(game.half_moves >= 50, Draw(FiftyMoves)) 200 216 use <- bool.guard( 201 217 game.is_insufficient_material(game),
+9
src/starfish_ffi.erl
··· 1 + -module(starfish_ffi). 2 + 3 + -export([monotonic_time/0]). 4 + 5 + monotonic_time() -> 6 + StartTime = erlang:system_info(start_time), 7 + CurrentTime = erlang:monotonic_time(), 8 + Difference = (CurrentTime - StartTime), 9 + erlang:convert_time_unit(Difference, native, millisecond).
+3
src/starfish_ffi.mjs
··· 1 + export function monotonic_time() { 2 + return performance.now(); 3 + }
+133 -127
test/starfish_test.gleam
··· 1 1 import gleam/int 2 2 import gleam/io 3 3 import gleam/list 4 + import gleam/option.{None, Some} 4 5 import gleeunit 5 6 import pocket_watch 6 7 import starfish ··· 12 13 gleeunit.main() 13 14 } 14 15 15 - /// Compare the state of two games, ignoring additional fields 16 - fn game_equal(a: game.Game, b: game.Game) -> Bool { 17 - a.board == b.board 18 - && a.to_move == b.to_move 19 - && a.castling == b.castling 20 - && a.en_passant_square == b.en_passant_square 21 - && a.half_moves == b.half_moves 22 - && a.full_moves == b.full_moves 23 - } 24 - 25 16 pub fn from_fen_test() { 26 17 let initial = starfish.new() 27 18 let parsed = starfish.from_fen(starfish.starting_fen) 28 - assert game_equal(initial, parsed) 19 + assert initial == parsed 29 20 30 21 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 31 22 let parsed = starfish.from_fen(initial_with_only_position) 32 - assert game_equal(initial, parsed) 23 + assert initial == parsed 33 24 } 34 25 35 26 pub fn try_from_fen_test() { 36 27 let initial = starfish.new() 37 28 let assert Ok(parsed) = starfish.try_from_fen(starfish.starting_fen) 38 - assert game_equal(parsed, initial) 29 + assert parsed == initial 39 30 40 31 let initial_with_only_position = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 41 32 let assert Error(error) = starfish.try_from_fen(initial_with_only_position) ··· 43 34 } 44 35 45 36 pub fn to_fen_test() { 46 - let fen = game.to_fen(starfish.new()) 37 + let fen = game.to_fen(game.initial_position()) 47 38 assert fen == starfish.starting_fen 48 39 49 40 let fen = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" ··· 51 42 } 52 43 53 44 pub fn to_long_algebraic_notation_test() { 54 - assert move.Move(from: 8, to: 24) |> starfish.to_long_algebraic_notation 45 + assert move.Move(board.Pawn, from: 8, to: 24) 46 + |> move.to_long_algebraic_notation 55 47 == "a2a4" 56 - assert move.Move(from: 6, to: 21) |> starfish.to_long_algebraic_notation 48 + assert move.Move(board.Pawn, from: 6, to: 21) 49 + |> move.to_long_algebraic_notation 57 50 == "g1f3" 58 - assert move.Move(from: 57, to: 42) |> starfish.to_long_algebraic_notation 51 + assert move.Move(board.Pawn, from: 57, to: 42) 52 + |> move.to_long_algebraic_notation 59 53 == "b8c6" 60 - assert move.Move(from: 49, to: 33) |> starfish.to_long_algebraic_notation 54 + assert move.Move(board.Pawn, from: 49, to: 33) 55 + |> move.to_long_algebraic_notation 61 56 == "b7b5" 62 - assert move.EnPassant(from: 32, to: 41) |> starfish.to_long_algebraic_notation 57 + assert move.EnPassant(from: 32, to: 41) |> move.to_long_algebraic_notation 63 58 == "a5b6" 64 - assert move.Castle(from: 4, to: 6) |> starfish.to_long_algebraic_notation 59 + assert move.Castle(from: 4, to: 6) |> move.to_long_algebraic_notation 65 60 == "e1g1" 66 - assert move.Castle(from: 4, to: 2) |> starfish.to_long_algebraic_notation 61 + assert move.Castle(from: 4, to: 2) |> move.to_long_algebraic_notation 67 62 == "e1c1" 68 - assert move.Castle(from: 60, to: 62) |> starfish.to_long_algebraic_notation 63 + assert move.Castle(from: 60, to: 62) |> move.to_long_algebraic_notation 69 64 == "e8g8" 70 - assert move.Castle(from: 60, to: 58) |> starfish.to_long_algebraic_notation 65 + assert move.Castle(from: 60, to: 58) |> move.to_long_algebraic_notation 71 66 == "e8c8" 72 - assert move.Promotion(from: 51, to: 58, piece: board.Queen) 73 - |> starfish.to_long_algebraic_notation 67 + assert move.Promotion( 68 + from: 51, 69 + to: 58, 70 + piece: board.Queen, 71 + captured_piece: None, 72 + ) 73 + |> move.to_long_algebraic_notation 74 74 == "d7c8q" 75 - assert move.Promotion(from: 11, to: 2, piece: board.Knight) 76 - |> starfish.to_long_algebraic_notation 75 + assert move.Promotion( 76 + from: 11, 77 + to: 2, 78 + piece: board.Knight, 79 + captured_piece: Some(board.Rook), 80 + ) 81 + |> move.to_long_algebraic_notation 77 82 == "d2c1n" 78 - assert move.Capture(from: 49, to: 7) |> starfish.to_long_algebraic_notation 83 + assert move.Capture(board.Bishop, from: 49, to: 7, captured_piece: board.Pawn) 84 + |> move.to_long_algebraic_notation 79 85 == "b7h1" 80 86 } 81 87 82 88 pub fn parse_long_algebraic_notation_test() { 83 89 let assert Ok(move) = starfish.parse_move("a2a4", starfish.new()) 84 - assert move == move.Move(from: 8, to: 24) 90 + assert starfish.get_move(move) == move.Move(board.Pawn, from: 8, to: 24) 85 91 let assert Ok(move) = starfish.parse_move("g1f3", starfish.new()) 86 - assert move == move.Move(from: 6, to: 21) 92 + assert starfish.get_move(move) == move.Move(board.Knight, from: 6, to: 21) 87 93 let assert Ok(move) = 88 94 starfish.parse_move( 89 95 "b8c6", ··· 91 97 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 92 98 ), 93 99 ) 94 - assert move == move.Move(from: 57, to: 42) 100 + assert starfish.get_move(move) == move.Move(board.Knight, from: 57, to: 42) 95 101 let assert Ok(move) = 96 102 starfish.parse_move( 97 103 "B7b5", ··· 99 105 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 100 106 ), 101 107 ) 102 - assert move == move.Move(from: 49, to: 33) 108 + assert starfish.get_move(move) == move.Move(board.Pawn, from: 49, to: 33) 103 109 let assert Ok(move) = 104 110 starfish.parse_move( 105 111 "a5b6", ··· 107 113 "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", 108 114 ), 109 115 ) 110 - assert move == move.EnPassant(from: 32, to: 41) 116 + assert starfish.get_move(move) == move.EnPassant(from: 32, to: 41) 111 117 let assert Ok(move) = 112 118 starfish.parse_move( 113 119 "e1G1", ··· 115 121 "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", 116 122 ), 117 123 ) 118 - assert move == move.Castle(from: 4, to: 6) 124 + assert starfish.get_move(move) == move.Castle(from: 4, to: 6) 119 125 let assert Ok(move) = 120 126 starfish.parse_move( 121 127 "e1c1", ··· 123 129 "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", 124 130 ), 125 131 ) 126 - assert move == move.Castle(from: 4, to: 2) 132 + assert starfish.get_move(move) == move.Castle(from: 4, to: 2) 127 133 let assert Ok(move) = 128 134 starfish.parse_move( 129 135 "e8g8", ··· 131 137 "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", 132 138 ), 133 139 ) 134 - assert move == move.Castle(from: 60, to: 62) 140 + assert starfish.get_move(move) == move.Castle(from: 60, to: 62) 135 141 let assert Ok(move) = 136 142 starfish.parse_move( 137 143 "E8C8", ··· 139 145 "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", 140 146 ), 141 147 ) 142 - assert move == move.Castle(from: 60, to: 58) 148 + assert starfish.get_move(move) == move.Castle(from: 60, to: 58) 143 149 let assert Ok(move) = 144 150 starfish.parse_move( 145 151 "d7c8q", ··· 147 153 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 148 154 ), 149 155 ) 150 - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 156 + assert starfish.get_move(move) 157 + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) 151 158 let assert Ok(move) = 152 159 starfish.parse_move( 153 160 "d2c1N", ··· 155 162 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 156 163 ), 157 164 ) 158 - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 165 + assert starfish.get_move(move) 166 + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) 159 167 let assert Ok(move) = 160 168 starfish.parse_move( 161 169 "b7h1", ··· 163 171 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 164 172 ), 165 173 ) 166 - assert move == move.Capture(from: 49, to: 7) 174 + assert starfish.get_move(move) 175 + == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) 167 176 168 177 let assert Error(Nil) = starfish.parse_move("abcd", starfish.new()) 169 178 let assert Error(Nil) = starfish.parse_move("e2e4extra", starfish.new()) ··· 172 181 173 182 pub fn parse_standard_algebraic_notation_test() { 174 183 let assert Ok(move) = starfish.parse_move("a4", starfish.new()) 175 - assert move == move.Move(from: 8, to: 24) 184 + assert starfish.get_move(move) == move.Move(board.Pawn, from: 8, to: 24) 176 185 let assert Ok(move) = starfish.parse_move("Nf3", starfish.new()) 177 - assert move == move.Move(from: 6, to: 21) 186 + assert starfish.get_move(move) == move.Move(board.Knight, from: 6, to: 21) 178 187 let assert Ok(move) = 179 188 starfish.parse_move( 180 189 "Nc6", ··· 182 191 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 183 192 ), 184 193 ) 185 - assert move == move.Move(from: 57, to: 42) 194 + assert starfish.get_move(move) == move.Move(board.Knight, from: 57, to: 42) 186 195 let assert Ok(move) = 187 196 starfish.parse_move( 188 197 "b5", ··· 190 199 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 191 200 ), 192 201 ) 193 - assert move == move.Move(from: 49, to: 33) 202 + assert starfish.get_move(move) == move.Move(board.Pawn, from: 49, to: 33) 194 203 let assert Ok(move) = 195 204 starfish.parse_move( 196 205 "axb6", ··· 198 207 "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", 199 208 ), 200 209 ) 201 - assert move == move.EnPassant(from: 32, to: 41) 210 + assert starfish.get_move(move) == move.EnPassant(from: 32, to: 41) 202 211 let assert Ok(move) = 203 212 starfish.parse_move( 204 213 "O-O", ··· 206 215 "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", 207 216 ), 208 217 ) 209 - assert move == move.Castle(from: 4, to: 6) 218 + assert starfish.get_move(move) == move.Castle(from: 4, to: 6) 210 219 let assert Ok(move) = 211 220 starfish.parse_move( 212 221 "O-O-O", ··· 214 223 "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", 215 224 ), 216 225 ) 217 - assert move == move.Castle(from: 4, to: 2) 226 + assert starfish.get_move(move) == move.Castle(from: 4, to: 2) 218 227 let assert Ok(move) = 219 228 starfish.parse_move( 220 229 "O-O", ··· 222 231 "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", 223 232 ), 224 233 ) 225 - assert move == move.Castle(from: 60, to: 62) 234 + assert starfish.get_move(move) == move.Castle(from: 60, to: 62) 226 235 let assert Ok(move) = 227 236 starfish.parse_move( 228 237 "0-0-0", ··· 230 239 "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", 231 240 ), 232 241 ) 233 - assert move == move.Castle(from: 60, to: 58) 242 + assert starfish.get_move(move) == move.Castle(from: 60, to: 58) 234 243 let assert Ok(move) = 235 244 starfish.parse_move( 236 245 "c8q", ··· 238 247 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 239 248 ), 240 249 ) 241 - assert move == move.Promotion(from: 51, to: 58, piece: board.Queen) 250 + assert starfish.get_move(move) 251 + == move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen) 242 252 let assert Ok(move) = 243 253 starfish.parse_move( 244 254 "c1=N", ··· 246 256 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 247 257 ), 248 258 ) 249 - assert move == move.Promotion(from: 11, to: 2, piece: board.Knight) 259 + assert starfish.get_move(move) 260 + == move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight) 250 261 let assert Ok(move) = 251 262 starfish.parse_move( 252 263 "Bxh1", ··· 254 265 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 255 266 ), 256 267 ) 257 - assert move == move.Capture(from: 49, to: 7) 268 + assert starfish.get_move(move) 269 + == move.Capture(board.Bishop, board.Rook, from: 49, to: 7) 258 270 let assert Ok(move) = 259 271 starfish.parse_move( 260 272 "Rac4", 261 273 starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 262 274 ) 263 - assert move == move.Move(from: 24, to: 26) 275 + assert starfish.get_move(move) == move.Move(board.Rook, from: 24, to: 26) 264 276 265 277 let assert Ok(move) = 266 278 starfish.parse_move( 267 279 "R7c6", 268 280 starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 269 281 ) 270 - assert move == move.Move(from: 50, to: 42) 282 + assert starfish.get_move(move) == move.Move(board.Rook, from: 50, to: 42) 271 283 272 284 let assert Error(Nil) = starfish.parse_move("e2", starfish.new()) 273 285 let assert Error(Nil) = starfish.parse_move("Bxe4", starfish.new()) ··· 275 287 } 276 288 277 289 pub fn to_standard_algebraic_notation_test() { 278 - assert starfish.to_standard_algebraic_notation( 279 - move.Move(from: 8, to: 24), 280 - starfish.new(), 290 + assert move.to_standard_algebraic_notation( 291 + move.Move(board.Pawn, from: 8, to: 24), 292 + game.initial_position(), 281 293 ) 282 294 == "a4" 283 - assert starfish.to_standard_algebraic_notation( 284 - move.Move(from: 6, to: 21), 285 - starfish.new(), 295 + assert move.to_standard_algebraic_notation( 296 + move.Move(board.Knight, from: 6, to: 21), 297 + game.initial_position(), 286 298 ) 287 299 == "Nf3" 288 - assert starfish.to_standard_algebraic_notation( 289 - move.Move(from: 57, to: 42), 290 - starfish.from_fen( 300 + assert move.to_standard_algebraic_notation( 301 + move.Move(board.Knight, from: 57, to: 42), 302 + game.from_fen( 291 303 "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", 292 304 ), 293 305 ) 294 306 == "Nc6" 295 307 296 - assert starfish.to_standard_algebraic_notation( 297 - move.Move(from: 49, to: 33), 298 - starfish.from_fen( 299 - "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1", 300 - ), 308 + assert move.to_standard_algebraic_notation( 309 + move.Move(board.Pawn, from: 49, to: 33), 310 + game.from_fen("rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq - 0 1"), 301 311 ) 302 312 == "b5" 303 313 304 - assert starfish.to_standard_algebraic_notation( 314 + assert move.to_standard_algebraic_notation( 305 315 move.EnPassant(from: 32, to: 41), 306 - starfish.from_fen( 316 + game.from_fen( 307 317 "rnbqkbnr/p1p1pppp/8/Pp1p4/8/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 3", 308 318 ), 309 319 ) 310 320 == "axb6" 311 321 312 - assert starfish.to_standard_algebraic_notation( 322 + assert move.to_standard_algebraic_notation( 313 323 move.Castle(from: 4, to: 6), 314 - starfish.from_fen( 324 + game.from_fen( 315 325 "rnbqkbnr/pp3ppp/8/2ppp3/4P3/5N2/PPPPBPPP/RNBQK2R w KQkq - 0 4", 316 326 ), 317 327 ) 318 328 == "O-O" 319 329 320 - assert starfish.to_standard_algebraic_notation( 330 + assert move.to_standard_algebraic_notation( 321 331 move.Castle(from: 4, to: 2), 322 - starfish.from_fen( 332 + game.from_fen( 323 333 "rnbqkbnr/ppp2ppp/8/3pp3/3P4/2N1B3/PPPQPPPP/R3KBNR w KQkq - 0 5", 324 334 ), 325 335 ) 326 336 == "O-O-O" 327 337 328 - assert starfish.to_standard_algebraic_notation( 338 + assert move.to_standard_algebraic_notation( 329 339 move.Castle(from: 60, to: 62), 330 - starfish.from_fen( 340 + game.from_fen( 331 341 "rnbqk2r/ppppbppp/5n2/4p3/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 4", 332 342 ), 333 343 ) 334 344 == "O-O" 335 345 336 - assert starfish.to_standard_algebraic_notation( 346 + assert move.to_standard_algebraic_notation( 337 347 move.Castle(from: 60, to: 58), 338 - starfish.from_fen( 348 + game.from_fen( 339 349 "r3kbnr/pppqpppp/2n1b3/3p4/3PP3/8/PPP2PPP/RNBQKBNR b KQkq - 0 5", 340 350 ), 341 351 ) 342 352 == "O-O-O" 343 353 344 - assert starfish.to_standard_algebraic_notation( 345 - move.Promotion(from: 51, to: 58, piece: board.Queen), 346 - starfish.from_fen( 347 - "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 348 - ), 354 + assert move.to_standard_algebraic_notation( 355 + move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen), 356 + game.from_fen("rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5"), 349 357 ) 350 358 == "dxc8=Q" 351 359 352 - assert starfish.to_standard_algebraic_notation( 353 - move.Promotion(from: 11, to: 2, piece: board.Knight), 354 - starfish.from_fen( 355 - "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 356 - ), 360 + assert move.to_standard_algebraic_notation( 361 + move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight), 362 + game.from_fen("rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5"), 357 363 ) 358 364 == "dxc1=N" 359 365 360 - assert starfish.to_standard_algebraic_notation( 361 - move.Capture(from: 49, to: 7), 362 - starfish.from_fen( 366 + assert move.to_standard_algebraic_notation( 367 + move.Capture(board.Bishop, board.Rook, from: 49, to: 7), 368 + game.from_fen( 363 369 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 364 370 ), 365 371 ) 366 372 == "Bxh1" 367 373 368 - assert starfish.to_standard_algebraic_notation( 369 - move.Move(from: 24, to: 26), 370 - starfish.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 374 + assert move.to_standard_algebraic_notation( 375 + move.Move(board.Rook, from: 24, to: 26), 376 + game.from_fen("k7/8/8/8/R4R2/8/8/7K w - - 0 1"), 371 377 ) 372 378 == "Rac4" 373 379 374 - assert starfish.to_standard_algebraic_notation( 375 - move.Move(from: 50, to: 42), 376 - starfish.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 380 + assert move.to_standard_algebraic_notation( 381 + move.Move(board.Rook, from: 50, to: 42), 382 + game.from_fen("k7/2r5/8/8/2r5/8/8/7K b - - 0 1"), 377 383 ) 378 384 == "R7c6" 379 385 380 - assert starfish.to_standard_algebraic_notation( 381 - move.Capture(from: 31, to: 13), 382 - starfish.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"), 386 + assert move.to_standard_algebraic_notation( 387 + move.Capture(board.Queen, board.Bishop, from: 31, to: 13), 388 + game.from_fen("k7/8/8/8/5Q1Q/8/5b1Q/3K4 w - - 0 1"), 383 389 ) 384 390 == "Qh4xf2" 385 391 ··· 389 395 } 390 396 391 397 pub fn phase_test() { 392 - let game = starfish.new() 393 - assert game.phase( 394 - game.white_pieces.non_pawn_material, 395 - game.black_pieces.non_pawn_material, 396 - ) 397 - == 0 398 - let game = starfish.from_fen("k7/8/8/8/8/8/8/K7") 399 - assert game.phase( 400 - game.white_pieces.non_pawn_material, 401 - game.black_pieces.non_pawn_material, 402 - ) 403 - == 128 398 + assert game.phase(game.initial_position()) == 0 399 + assert game.phase(game.from_fen("k7/8/8/8/8/8/8/K7")) == 128 404 400 } 405 401 406 402 fn apply_move(game: starfish.Game, move: String) -> starfish.Game { ··· 467 463 } 468 464 469 465 fn perft(fen: String, depth: Int, expected_moves: Int) { 470 - assert do_perft(game.from_fen(fen), depth - 1) == expected_moves 466 + assert do_perft(starfish.from_fen(fen), depth - 1) == expected_moves 471 467 } 472 468 473 - fn do_perft(game: game.Game, depth: Int) -> Int { 469 + fn do_perft(game: starfish.Game, depth: Int) -> Int { 474 470 let legal_moves = starfish.legal_moves(game) 475 471 case depth { 476 472 0 -> list.length(legal_moves) ··· 501 497 until: starfish.Depth(5), 502 498 ) 503 499 // b4f4 504 - assert move == move.Capture(from: 25, to: 29) 500 + assert starfish.get_move(move) 501 + == move.Capture(board.Rook, board.Pawn, from: 25, to: 29) 505 502 506 503 let assert Ok(move) = 507 504 starfish.search( 508 505 starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"), 509 506 until: starfish.Depth(10), 510 507 ) 511 - assert move == move.Move(from: 29, to: 31) 508 + assert starfish.get_move(move) == move.Move(board.Rook, from: 29, to: 31) 512 509 } 513 510 514 511 pub fn perft_initial_position_test_() { ··· 713 710 moves: List(move.Move), 714 711 expected_fen: String, 715 712 ) { 716 - let final_fen = 713 + let game = 717 714 starting_fen 718 715 |> game.from_fen 719 - |> list.fold(moves, _, starfish.apply_move) 720 - |> game.to_fen 716 + |> list.fold(moves, _, move.apply) 721 717 722 - assert final_fen == expected_fen 718 + let game = game.Game(..game, previous_positions: []) 719 + 720 + let expected_game = game.from_fen(expected_fen) 721 + 722 + assert game == expected_game 723 723 } 724 724 725 725 pub fn apply_move_test() { 726 726 test_apply_move( 727 727 starfish.starting_fen, 728 728 // a2a4 729 - [move.Move(from: 8, to: 24)], 729 + [move.Move(board.Pawn, from: 8, to: 24)], 730 730 "rnbqkbnr/pppppppp/8/8/P7/8/1PPPPPPP/RNBQKBNR b KQkq a3 0 1", 731 731 ) 732 732 733 733 test_apply_move( 734 734 starfish.starting_fen, 735 735 // g1f3 736 - [move.Move(from: 6, to: 21)], 736 + [move.Move(board.Knight, from: 6, to: 21)], 737 737 "rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1", 738 738 ) 739 739 740 740 test_apply_move( 741 741 starfish.starting_fen, 742 742 // a2a4, b8c6 743 - [move.Move(from: 8, to: 24), move.Move(from: 57, to: 42)], 743 + [ 744 + move.Move(board.Pawn, from: 8, to: 24), 745 + move.Move(board.Knight, from: 57, to: 42), 746 + ], 744 747 "r1bqkbnr/pppppppp/2n5/8/P7/8/1PPPPPPP/RNBQKBNR w KQkq - 1 2", 745 748 ) 746 749 747 750 test_apply_move( 748 751 starfish.starting_fen, 749 752 // a2a4, b7b5 750 - [move.Move(from: 8, to: 24), move.Move(from: 49, to: 33)], 753 + [ 754 + move.Move(board.Pawn, from: 8, to: 24), 755 + move.Move(board.Pawn, from: 49, to: 33), 756 + ], 751 757 "rnbqkbnr/p1pppppp/8/1p6/P7/8/1PPPPPPP/RNBQKBNR w KQkq b6 0 2", 752 758 ) 753 759 ··· 789 795 test_apply_move( 790 796 "rnbq1bnr/pppPkpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR w KQ - 1 5", 791 797 // d7c8q 792 - [move.Promotion(from: 51, to: 58, piece: board.Queen)], 798 + [move.Promotion(Some(board.Bishop), from: 51, to: 58, piece: board.Queen)], 793 799 "rnQq1bnr/ppp1kpp1/4p2p/8/8/8/PPPP1PPP/RNBQKBNR b KQ - 0 5", 794 800 ) 795 801 796 802 test_apply_move( 797 803 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPPpKPP1/RNBQ1BNR b kq - 1 5", 798 804 // d2c1n 799 - [move.Promotion(from: 11, to: 2, piece: board.Knight)], 805 + [move.Promotion(Some(board.Bishop), from: 11, to: 2, piece: board.Knight)], 800 806 "rnbqkbnr/pppp1ppp/8/8/8/4P2P/PPP1KPP1/RNnQ1BNR w kq - 0 6", 801 807 ) 802 808 ··· 804 810 test_apply_move( 805 811 "rn1qkbnr/pbpppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNR b KQkq - 0 3", 806 812 // b7h1 807 - [move.Capture(from: 49, to: 7)], 813 + [move.Capture(board.Bishop, board.Rook, from: 49, to: 7)], 808 814 "rn1qkbnr/p1pppppp/1p6/6P1/8/8/PPPPPP1P/RNBQKBNb w Qkq - 0 4", 809 815 ) 810 816 }