···7788/// Statically evaluates a position. Does not take into account checkmate or
99/// stalemate, those must be accounted for beforehand.
1010-pub fn evaluate(game: game.Game, legal_moves: List(move.Move)) -> Int {
1010+pub fn evaluate(game: game.Game, legal_moves: List(#(move.Move, Int))) -> Int {
1111 evaluate_position(game) + list.length(legal_moves)
1212}
13131414fn evaluate_position(game: game.Game) -> Int {
1515 use eval, position, #(piece, colour) <- dict.fold(game.board, 0)
1616 let score =
1717- piece_score(piece) + piece_table.piece_score(piece, colour, position)
1717+ board.piece_value(piece) + piece_table.piece_score(piece, colour, position)
1818 case colour == game.to_move {
1919 True -> eval + score
2020 False -> eval - score
2121 }
2222}
2323-2424-fn piece_score(piece: board.Piece) -> Int {
2525- case piece {
2626- board.King -> 0
2727- board.Pawn -> 100
2828- board.Knight -> 300
2929- board.Bishop -> 300
3030- board.Rook -> 500
3131- board.Queen -> 900
3232- }
3333-}
+103-8
src/starfish/internal/search.gleam
···11import gleam/bool
22+import gleam/int
33+import gleam/list
24import gleam/option.{type Option, None, Some}
55+import starfish/internal/board
36import starfish/internal/evaluate
47import starfish/internal/game.{type Game}
58import starfish/internal/hash
69import starfish/internal/move.{type Move}
1010+import starfish/internal/piece_table
711812/// Not really infinity, but a high enough number that nothing but explicit
913/// references to it will reach it.
···16201721pub fn best_move(game: Game, until: Until) -> Result(Move, Nil) {
1822 use <- bool.guard(until(0), Error(Nil))
1919- let legal_moves = move.legal(game)
2323+ let legal_moves = order_moves(game)
2024 use <- bool.guard(legal_moves == [], Error(Nil))
2125 iterative_deepening(game, 1, None, legal_moves, hash.new_table(), until)
2226}
···2529 game: Game,
2630 depth: Int,
2731 best_move: Option(Move),
2828- legal_moves: List(Move),
3232+ legal_moves: List(#(Move, Int)),
2933 cached_positions: hash.Table,
3034 until: Until,
3135) -> Result(Move, Nil) {
···4953 game,
5054 depth + 1,
5155 Some(best_move),
5252- legal_moves,
5656+ // TODO: Instead of just sorting the best move to the front, maybe we
5757+ // can sort all the moves by evaluation?
5858+ reorder_moves(legal_moves, best_move),
5359 cached_positions,
5460 until,
5561 )
···7985 game: Game,
8086 cached_positions: hash.Table,
8187 depth: Int,
8282- legal_moves: List(Move),
8888+ legal_moves: List(#(Move, Int)),
8389 best_move: Option(Move),
8490 best_eval: Int,
8591 until: Until,
···9197 Some(best_move) ->
9298 Ok(TopLevelSearchResult(best_move:, cached_positions:))
9399 }
9494- [move, ..moves] -> {
100100+ [#(move, _), ..moves] -> {
95101 let SearchResult(eval:, cached_positions:, eval_kind: _, finished:) =
96102 search(
97103 move.apply(game, move),
···162168 Ok(#(eval, eval_kind)) ->
163169 SearchResult(eval:, cached_positions:, eval_kind:, finished: True)
164170 Error(_) ->
165165- case move.legal(game) {
171171+ case order_moves(game) {
166172 // If the game is in a checkmate or stalemate position, the game is over, so
167173 // we stop searching.
168174 [] -> {
···245251fn search_loop(
246252 game: Game,
247253 cached_positions: hash.Table,
248248- moves: List(Move),
254254+ moves: List(#(Move, Int)),
249255 depth: Int,
250256 // The best evaluation we've encountered so far.
251257 best_eval: Int,
···265271 eval_kind:,
266272 finished: True,
267273 )
268268- [move, ..moves] -> {
274274+ [#(move, _), ..moves] -> {
269275 // Evaluate the position for the opponent. The negative of the opponent's
270276 // eval is our eval.
271277 let SearchResult(
···317323 }
318324 }
319325}
326326+327327+/// Sort moves by their guessed evaluation. We return the guesses with the moves
328328+/// in order to save iterating the list a second time. The guesses are discarded
329329+/// after this point.
330330+fn order_moves(game: Game) -> List(#(Move, Int)) {
331331+ game
332332+ |> move.legal
333333+ |> collect_guessed_eval(game, [])
334334+ |> list.sort(fn(a, b) { int.compare(a.1, b.1) })
335335+}
336336+337337+/// Reorder already ordered moves to move the best move to the front of the list,
338338+/// so that it will be searched first on the next iteration.
339339+fn reorder_moves(
340340+ moves: List(#(Move, Int)),
341341+ best_move: Move,
342342+) -> List(#(Move, Int)) {
343343+ let moves_without_best = list.filter(moves, fn(pair) { pair.0 != best_move })
344344+ [#(best_move, 0), ..moves_without_best]
345345+}
346346+347347+fn collect_guessed_eval(
348348+ moves: List(Move),
349349+ game: Game,
350350+ acc: List(#(Move, Int)),
351351+) -> List(#(Move, Int)) {
352352+ case moves {
353353+ [] -> acc
354354+ [move, ..moves] ->
355355+ collect_guessed_eval(moves, game, [#(move, guess_eval(game, move)), ..acc])
356356+ }
357357+}
358358+359359+/// Rate captures and promotions higher than quiet moves
360360+const capture_promotion_bonus = 10_000
361361+362362+/// Guess the evaluation of a move so we can hopefully search moves in a better
363363+/// order than random. Searching better moves first improves alpha-beta pruning,
364364+/// allowing us to search more positions.
365365+fn guess_eval(game: Game, move: Move) -> Int {
366366+ let assert board.Occupied(piece:, colour:) = board.get(game.board, move.from)
367367+ as "Invalid move trying to move empty piece"
368368+369369+ let moving_piece = case move {
370370+ move.Promotion(piece:, ..) -> piece
371371+ move.Capture(..) | move.Castle(..) | move.EnPassant(..) | move.Move(..) ->
372372+ piece
373373+ }
374374+375375+ let from_score = piece_table.piece_score(moving_piece, colour, move.from)
376376+ let to_score = piece_table.piece_score(moving_piece, colour, move.to)
377377+378378+ let position_improvement = to_score - from_score
379379+ let move_specific_score = case move {
380380+ // TODO store information in moves so we don't have to retrieve it from the
381381+ // board every time.
382382+ move.Capture(..) -> {
383383+ let assert board.Occupied(piece: captured_piece, colour: _) =
384384+ board.get(game.board, move.to)
385385+ as "Invalid capture moving to empty square"
386386+387387+ capture_promotion_bonus
388388+ // Capturing a more valuable piece is better, and using a less valuable
389389+ // piece to capture is usually better. However, we prioritise the value of
390390+ // the captured piece.
391391+ + board.piece_value(captured_piece)
392392+ * 2
393393+ * -board.piece_value(moving_piece)
394394+ }
395395+ move.EnPassant(..) -> capture_promotion_bonus
396396+ move.Promotion(..) -> {
397397+ // Promotions can also be captures
398398+ let capture_value = case board.get(game.board, move.to) {
399399+ board.Empty | board.OffBoard -> 0
400400+ board.Occupied(piece: captured_piece, colour: _) ->
401401+ board.piece_value(captured_piece)
402402+ * 2
403403+ - board.piece_value(moving_piece)
404404+ }
405405+406406+ // Promoting to a more valuable piece is usually better
407407+ capture_promotion_bonus + capture_value + board.piece_value(move.piece)
408408+ }
409409+ // For castling and quite moves, we can't easily predict the score
410410+ move.Castle(..) | move.Move(..) -> 0
411411+ }
412412+413413+ position_improvement + move_specific_score
414414+}