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