tangled
alpha
login
or
join now
gearsco.de
/
starfish
2
fork
atom
A chess library for Gleam
2
fork
atom
overview
issues
pulls
pipelines
Ensure earliest checkmate is detected
gearsco.de
5 months ago
7d543702
209d916a
+44
-48
3 changed files
expand all
collapse all
unified
split
src
starfish
internal
hash.gleam
search.gleam
test
starfish_test.gleam
+6
-22
src/starfish/internal/hash.gleam
···
80
80
table: Table,
81
81
hash: Int,
82
82
depth: Int,
83
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
89
-
let eval = correct_mate_score(eval, -depth_searched)
90
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
96
-
fn correct_mate_score(eval: Int, depth_searched: Int) -> Int {
97
97
-
case eval < 0 {
98
98
-
True ->
99
99
-
case eval - max_depth <= -checkmate {
100
100
-
True -> eval + depth_searched
101
101
-
False -> eval
102
102
-
}
103
103
-
False ->
104
104
-
case eval + max_depth >= checkmate {
105
105
-
True -> eval - depth_searched
106
106
-
False -> eval
107
107
-
}
108
108
-
}
109
109
-
}
110
110
-
111
93
pub fn get(
112
94
table: Table,
113
95
hash: Int,
114
96
depth: Int,
115
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
123
-
let eval = correct_mate_score(entry.eval, depth_searched)
104
104
+
let eval = entry.eval
105
105
+
106
106
+
use <- bool.guard(
107
107
+
eval == checkmate || eval == -checkmate,
108
108
+
Ok(#(eval, entry.kind)),
109
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
134
-
135
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
16
-
const checkmate = -1_000_000
16
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
51
-
Ok(TopLevelSearchResult(best_move:, cached_positions:)) ->
52
52
-
iterative_deepening(
53
53
-
game,
54
54
-
depth + 1,
55
55
-
Some(best_move),
56
56
-
reorder_moves(legal_moves, best_move),
57
57
-
cached_positions,
58
58
-
until,
59
59
-
)
51
51
+
Ok(TopLevelSearchResult(best_move:, best_eval:, cached_positions:)) ->
52
52
+
// If we found checkmate, there's no need to continue searching. Deeper
53
53
+
// searches cannot find a sooner mate. Similarly if the best move we have
54
54
+
// found is forced checkmate, that means we can't get out of checkmate so
55
55
+
// searching further is also pointless.
56
56
+
case best_eval == checkmate || best_eval == -checkmate {
57
57
+
True -> Ok(best_move)
58
58
+
False ->
59
59
+
iterative_deepening(
60
60
+
game,
61
61
+
depth + 1,
62
62
+
Some(best_move),
63
63
+
reorder_moves(legal_moves, best_move),
64
64
+
cached_positions,
65
65
+
until,
66
66
+
)
67
67
+
}
60
68
}
61
69
}
62
70
63
71
type TopLevelSearchResult {
64
64
-
TopLevelSearchResult(best_move: Move, cached_positions: hash.Table)
72
72
+
TopLevelSearchResult(
73
73
+
best_move: Move,
74
74
+
best_eval: Int,
75
75
+
cached_positions: hash.Table,
76
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
96
-
Ok(TopLevelSearchResult(best_move:, cached_positions:))
108
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
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
125
-
Ok(TopLevelSearchResult(best_move:, cached_positions:))
136
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
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
183
+
use <- bool.guard(
184
184
+
best_eval == checkmate,
185
185
+
SearchResult(best_eval, cached_positions, hash.Exact, True),
186
186
+
)
187
187
+
173
188
case
174
189
hash.get(
175
190
cached_positions,
176
191
game.zobrist_hash,
177
192
depth,
178
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
191
-
// Sooner checkmate is better. Or from the perspective of the side being
192
192
-
// mated, later checkmate is better.
193
193
-
True -> checkmate + depth_searched
205
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
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
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
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
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
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
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
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
505
+
506
506
+
let assert Ok(move) =
507
507
+
starfish.search(
508
508
+
starfish.from_fen("8/8/5k1K/8/5r2/8/8/8 b - - 34 18"),
509
509
+
until: starfish.Depth(10),
510
510
+
)
511
511
+
assert move == move.Move(from: 29, to: 31)
505
512
}
506
513
507
514
pub fn perft_initial_position_test_() {