A chess library for Gleam
2
fork

Configure Feed

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

Implement basic iterative deepening

+139 -25
+1
gleam.toml
··· 15 15 [dependencies] 16 16 gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 17 iv = ">= 1.3.2 and < 2.0.0" 18 + birl = ">= 1.8.0 and < 2.0.0" 18 19 19 20 [dev-dependencies] 20 21 gleeunit = ">= 1.0.0 and < 2.0.0"
+1
manifest.toml
··· 17 17 ] 18 18 19 19 [requirements] 20 + birl = { version = ">= 1.8.0 and < 2.0.0" } 20 21 gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 21 22 gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 22 23 iv = { version = ">= 1.3.2 and < 2.0.0" }
+25 -2
src/starfish.gleam
··· 1 + import birl 1 2 import gleam/bool 2 3 import gleam/result 3 4 import starfish/internal/board ··· 118 119 move.legal(game) 119 120 } 120 121 121 - pub fn search(game: Game, to_depth depth: Int) -> Result(Move, Nil) { 122 - search.best_move(game, depth) 122 + /// Used to determine how long to search positions 123 + pub type SearchCutoff { 124 + /// Search to a specific depth 125 + Depth(depth: Int) 126 + /// Search for a given number of milliseconds. 127 + /// 128 + /// NOTE: The process will usually take slightly longer than the specified time. 129 + /// It would be expensive to check the time every millisecond, so it is checked 130 + /// periodically. This is usually less than 10ms, but it can be higher than that. 131 + Time(milliseconds: Int) 132 + } 133 + 134 + /// Finds the best move for a given position, or returns an error if no moves are 135 + /// legal (If it's checkmate or stalemate) 136 + pub fn search(game: Game, until cutoff: SearchCutoff) -> Result(Move, Nil) { 137 + let until = case cutoff { 138 + Depth(depth:) -> fn(current_depth) { current_depth > depth } 139 + Time(milliseconds:) -> { 140 + let end_time = birl.monotonic_now() + milliseconds * 1000 141 + fn(_) { birl.monotonic_now() >= end_time } 142 + } 143 + } 144 + 145 + search.best_move(game, until) 123 146 } 124 147 125 148 pub fn apply_move(game: Game, move: Move) -> Game {
+111 -22
src/starfish/internal/search.gleam
··· 11 11 12 12 const checkmate = -1_000_000 13 13 14 - pub fn best_move(game: Game, depth: Int) -> Result(Move, Nil) { 15 - use <- bool.guard(depth < 1, Error(Nil)) 16 - search_top_level( 17 - game, 18 - hash.new_table(), 19 - depth, 20 - move.legal(game), 21 - None, 22 - -infinity, 23 - ) 14 + type Until = 15 + fn(Int) -> Bool 16 + 17 + pub fn best_move(game: Game, until: Until) -> Result(Move, Nil) { 18 + use <- bool.guard(until(0), Error(Nil)) 19 + let legal_moves = move.legal(game) 20 + use <- bool.guard(legal_moves == [], Error(Nil)) 21 + iterative_deepening(game, 1, None, legal_moves, hash.new_table(), until) 22 + } 23 + 24 + fn iterative_deepening( 25 + game: Game, 26 + depth: Int, 27 + best_move: Option(Move), 28 + legal_moves: List(Move), 29 + cached_positions: hash.Table, 30 + until: Until, 31 + ) -> Result(Move, Nil) { 32 + use <- bool.guard(until(depth), option.to_result(best_move, Nil)) 33 + 34 + let move_result = 35 + search_top_level( 36 + game, 37 + cached_positions, 38 + depth, 39 + legal_moves, 40 + None, 41 + -infinity, 42 + until, 43 + ) 44 + 45 + case move_result { 46 + Error(_) -> option.to_result(best_move, Nil) 47 + Ok(TopLevelSearchResult(best_move:, cached_positions:)) -> 48 + iterative_deepening( 49 + game, 50 + depth + 1, 51 + Some(best_move), 52 + legal_moves, 53 + cached_positions, 54 + until, 55 + ) 56 + } 57 + } 58 + 59 + type TopLevelSearchResult { 60 + TopLevelSearchResult(best_move: Move, cached_positions: hash.Table) 24 61 } 25 62 26 63 type SearchResult { ··· 28 65 eval: Int, 29 66 cached_positions: hash.Table, 30 67 eval_kind: hash.CacheKind, 68 + finished: Bool, 31 69 ) 32 70 } 33 71 ··· 44 82 legal_moves: List(Move), 45 83 best_move: Option(Move), 46 84 best_eval: Int, 47 - ) -> Result(Move, Nil) { 85 + until: Until, 86 + ) -> Result(TopLevelSearchResult, Nil) { 48 87 case legal_moves { 49 88 [] -> 50 89 case best_move { 51 90 None -> Error(Nil) 52 - Some(best_move) -> Ok(best_move) 91 + Some(best_move) -> 92 + Ok(TopLevelSearchResult(best_move:, cached_positions:)) 53 93 } 54 94 [move, ..moves] -> { 55 - let SearchResult(eval:, cached_positions:, ..) = 95 + let SearchResult(eval:, cached_positions:, eval_kind: _, finished:) = 56 96 search( 57 97 move.apply(game, move), 58 98 cached_positions, ··· 60 100 -infinity, 61 101 -best_eval, 62 102 0, 103 + until, 63 104 ) 64 105 106 + use <- bool.guard(!finished, case best_move { 107 + None -> Error(Nil) 108 + Some(best_move) -> 109 + Ok(TopLevelSearchResult(best_move:, cached_positions:)) 110 + }) 111 + 65 112 let eval = -eval 66 113 67 114 let #(best_move, best_eval) = case eval > best_eval { ··· 75 122 moves, 76 123 best_move, 77 124 best_eval, 125 + until, 78 126 ) 79 127 } 80 128 } ··· 87 135 best_eval: Int, 88 136 best_opponent_move: Int, 89 137 depth_searched: Int, 138 + until: Until, 90 139 ) -> SearchResult { 140 + use <- bool.guard( 141 + until(depth), 142 + SearchResult(0, cached_positions, hash.Exact, False), 143 + ) 144 + 91 145 // If we have reached fifty moves, the game is already a draw, so there's no 92 146 // point searching further. 93 147 use <- bool.guard( 94 148 game.half_moves >= 50, 95 - SearchResult(0, cached_positions, hash.Exact), 149 + SearchResult(0, cached_positions, hash.Exact, True), 96 150 ) 97 151 98 152 case ··· 105 159 best_opponent_move, 106 160 ) 107 161 { 108 - Ok(#(eval, eval_kind)) -> SearchResult(eval:, cached_positions:, eval_kind:) 162 + Ok(#(eval, eval_kind)) -> 163 + SearchResult(eval:, cached_positions:, eval_kind:, finished: True) 109 164 Error(_) -> 110 165 case move.legal(game) { 111 166 // If the game is in a checkmate or stalemate position, the game is over, so ··· 126 181 hash.Exact, 127 182 eval, 128 183 ) 129 - SearchResult(eval:, cached_positions:, eval_kind: hash.Exact) 184 + SearchResult( 185 + eval:, 186 + cached_positions:, 187 + eval_kind: hash.Exact, 188 + finished: True, 189 + ) 130 190 } 131 191 moves -> 132 192 case depth { ··· 142 202 hash.Exact, 143 203 eval, 144 204 ) 145 - SearchResult(eval:, cached_positions:, eval_kind: hash.Exact) 205 + SearchResult( 206 + eval:, 207 + cached_positions:, 208 + eval_kind: hash.Exact, 209 + finished: True, 210 + ) 146 211 } 147 212 _ -> { 148 - let SearchResult(eval:, cached_positions:, eval_kind:) = 213 + let SearchResult(eval:, cached_positions:, eval_kind:, finished:) as result = 149 214 search_loop( 150 215 game, 151 216 cached_positions, ··· 155 220 best_opponent_move, 156 221 depth_searched, 157 222 hash.Ceiling, 223 + until, 158 224 ) 159 225 226 + use <- bool.guard(!finished, result) 227 + 160 228 let cached_positions = 161 229 hash.cache( 162 230 cached_positions, ··· 167 235 eval, 168 236 ) 169 237 170 - SearchResult(eval:, cached_positions:, eval_kind:) 238 + SearchResult(eval:, cached_positions:, eval_kind:, finished:) 171 239 } 172 240 } 173 241 } ··· 187 255 best_opponent_move: Int, 188 256 depth_searched: Int, 189 257 eval_kind: hash.CacheKind, 258 + until: Until, 190 259 ) -> SearchResult { 191 260 case moves { 192 - [] -> SearchResult(eval: best_eval, cached_positions:, eval_kind:) 261 + [] -> 262 + SearchResult( 263 + eval: best_eval, 264 + cached_positions:, 265 + eval_kind:, 266 + finished: True, 267 + ) 193 268 [move, ..moves] -> { 194 269 // Evaluate the position for the opponent. The negative of the opponent's 195 270 // eval is our eval. 196 - let SearchResult(eval:, cached_positions:, eval_kind: search_kind) = 271 + let SearchResult( 272 + eval:, 273 + cached_positions:, 274 + eval_kind: search_kind, 275 + finished:, 276 + ) as result = 197 277 search( 198 278 move.apply(game, move), 199 279 cached_positions, ··· 201 281 -best_opponent_move, 202 282 -best_eval, 203 283 depth_searched + 1, 284 + until, 204 285 ) 286 + 287 + use <- bool.guard(!finished, result) 205 288 206 289 let eval = -eval 207 290 208 291 use <- bool.guard( 209 292 eval >= best_opponent_move, 210 - SearchResult(best_opponent_move, cached_positions, hash.Floor), 293 + SearchResult( 294 + best_opponent_move, 295 + cached_positions, 296 + hash.Floor, 297 + finished: True, 298 + ), 211 299 ) 212 300 213 301 let #(best_eval, eval_kind) = case eval > best_eval { ··· 224 312 best_opponent_move, 225 313 depth_searched, 226 314 eval_kind, 315 + until, 227 316 ) 228 317 } 229 318 }
+1 -1
test/starfish_test.gleam
··· 237 237 let assert Ok(move) = 238 238 starfish.search( 239 239 starfish.from_fen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1"), 240 - to_depth: 5, 240 + until: starfish.Depth(5), 241 241 ) 242 242 // b4f4 243 243 assert move == move.Capture(from: 25, to: 29)