A chess library for Gleam

Ensure earliest checkmate is detected

+44 -48
+6 -22
src/starfish/internal/hash.gleam
··· 80 80 table: Table, 81 81 hash: Int, 82 82 depth: Int, 83 - depth_searched: Int, 84 83 kind: CacheKind, 85 84 eval: Int, 86 85 ) -> Table { 87 86 let key = hash % table_size 88 87 89 - let eval = correct_mate_score(eval, -depth_searched) 90 - 91 88 let position = Entry(depth:, kind:, eval:, hash:) 92 89 93 90 iv.try_set(table, key, position) 94 91 } 95 92 96 - fn correct_mate_score(eval: Int, depth_searched: Int) -> Int { 97 - case eval < 0 { 98 - True -> 99 - case eval - max_depth <= -checkmate { 100 - True -> eval + depth_searched 101 - False -> eval 102 - } 103 - False -> 104 - case eval + max_depth >= checkmate { 105 - True -> eval - depth_searched 106 - False -> eval 107 - } 108 - } 109 - } 110 - 111 93 pub fn get( 112 94 table: Table, 113 95 hash: Int, 114 96 depth: Int, 115 - depth_searched: Int, 116 97 best_eval: Int, 117 98 best_opponent_move: Int, 118 99 ) -> Result(#(Int, CacheKind), Nil) { ··· 120 101 let entry = iv.get_or_default(table, key, missing_entry) 121 102 use <- bool.guard(entry.hash != hash || entry.depth < depth, Error(Nil)) 122 103 123 - let eval = correct_mate_score(entry.eval, depth_searched) 104 + let eval = entry.eval 105 + 106 + use <- bool.guard( 107 + eval == checkmate || eval == -checkmate, 108 + Ok(#(eval, entry.kind)), 109 + ) 124 110 125 111 case entry.kind { 126 112 Exact -> Ok(#(eval, Exact)) ··· 131 117 } 132 118 133 119 const checkmate = 1_000_000 134 - 135 - const max_depth = 1000
+31 -26
src/starfish/internal/search.gleam
··· 13 13 /// references to it will reach it. 14 14 const infinity = 1_000_000_000 15 15 16 - const checkmate = -1_000_000 16 + const checkmate = 1_000_000 17 17 18 18 type Until = 19 19 fn(Int) -> Bool ··· 48 48 49 49 case move_result { 50 50 Error(_) -> option.to_result(best_move, Nil) 51 - Ok(TopLevelSearchResult(best_move:, cached_positions:)) -> 52 - iterative_deepening( 53 - game, 54 - depth + 1, 55 - Some(best_move), 56 - reorder_moves(legal_moves, best_move), 57 - cached_positions, 58 - until, 59 - ) 51 + Ok(TopLevelSearchResult(best_move:, best_eval:, cached_positions:)) -> 52 + // If we found checkmate, there's no need to continue searching. Deeper 53 + // searches cannot find a sooner mate. Similarly if the best move we have 54 + // found is forced checkmate, that means we can't get out of checkmate so 55 + // searching further is also pointless. 56 + case best_eval == checkmate || best_eval == -checkmate { 57 + True -> Ok(best_move) 58 + False -> 59 + iterative_deepening( 60 + game, 61 + depth + 1, 62 + Some(best_move), 63 + reorder_moves(legal_moves, best_move), 64 + cached_positions, 65 + until, 66 + ) 67 + } 60 68 } 61 69 } 62 70 63 71 type TopLevelSearchResult { 64 - TopLevelSearchResult(best_move: Move, cached_positions: hash.Table) 72 + TopLevelSearchResult( 73 + best_move: Move, 74 + best_eval: Int, 75 + cached_positions: hash.Table, 76 + ) 65 77 } 66 78 67 79 type SearchResult { ··· 93 105 case best_move { 94 106 None -> Error(Nil) 95 107 Some(best_move) -> 96 - Ok(TopLevelSearchResult(best_move:, cached_positions:)) 108 + Ok(TopLevelSearchResult(best_move:, best_eval:, cached_positions:)) 97 109 } 98 110 [#(move, _), ..moves] -> { 99 111 let SearchResult(eval:, cached_positions:, eval_kind: _, finished:) = ··· 104 116 -infinity, 105 117 -best_eval, 106 118 0, 107 - 0, 108 119 until, 109 120 ) 110 121 ··· 122 133 // which case we've found a better move to play 123 134 // Either way, it's advantageous to use the best move from incomplete searches. 124 135 Some(best_move) -> 125 - Ok(TopLevelSearchResult(best_move:, cached_positions:)) 136 + Ok(TopLevelSearchResult(best_move:, best_eval:, cached_positions:)) 126 137 }) 127 138 128 139 let eval = -eval ··· 152 163 depth: Int, 153 164 best_eval: Int, 154 165 best_opponent_move: Int, 155 - depth_searched: Int, 156 166 extensions: Int, 157 167 until: Until, 158 168 ) -> SearchResult { ··· 170 180 SearchResult(0, cached_positions, hash.Exact, True), 171 181 ) 172 182 183 + use <- bool.guard( 184 + best_eval == checkmate, 185 + SearchResult(best_eval, cached_positions, hash.Exact, True), 186 + ) 187 + 173 188 case 174 189 hash.get( 175 190 cached_positions, 176 191 game.zobrist_hash, 177 192 depth, 178 - depth_searched, 179 193 best_eval, 180 194 best_opponent_move, 181 195 ) ··· 188 202 // we stop searching. 189 203 [] -> { 190 204 let eval = case game.attack_information.in_check { 191 - // Sooner checkmate is better. Or from the perspective of the side being 192 - // mated, later checkmate is better. 193 - True -> checkmate + depth_searched 205 + True -> -checkmate 194 206 False -> 0 195 207 } 196 208 let cached_positions = ··· 198 210 cached_positions, 199 211 game.zobrist_hash, 200 212 depth, 201 - depth_searched, 202 213 hash.Exact, 203 214 eval, 204 215 ) ··· 221 232 cached_positions, 222 233 game.zobrist_hash, 223 234 depth, 224 - depth_searched, 225 235 hash.Exact, 226 236 eval, 227 237 ) ··· 241 251 depth, 242 252 best_eval, 243 253 best_opponent_move, 244 - depth_searched, 245 254 hash.Ceiling, 246 255 extensions, 247 256 until, ··· 254 263 cached_positions, 255 264 game.zobrist_hash, 256 265 depth, 257 - depth_searched, 258 266 eval_kind, 259 267 eval, 260 268 ) ··· 277 285 // higher than this, we can discard it. That position is too good and we can 278 286 // assume that our opponent would never let us get there. 279 287 best_opponent_move: Int, 280 - depth_searched: Int, 281 288 eval_kind: hash.CacheKind, 282 289 extensions: Int, 283 290 until: Until, ··· 312 319 depth - 1 + extension, 313 320 -best_opponent_move, 314 321 -best_eval, 315 - depth_searched + 1, 316 322 extensions + extension, 317 323 until, 318 324 ) ··· 343 349 depth, 344 350 best_eval, 345 351 best_opponent_move, 346 - depth_searched, 347 352 eval_kind, 348 353 extensions, 349 354 until,
+7
test/starfish_test.gleam
··· 502 502 ) 503 503 // b4f4 504 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) 505 512 } 506 513 507 514 pub fn perft_initial_position_test_() {