git.mcksp
    1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
require "./board"
require "./piece"
require "./utils"

module Fork
  LOSE        = -90000i32
  INFINITY    = 100000i32
  LOOKUP_SIZE =   1048576

  struct Result
    getter move : Move?
    getter score : Int32

    def initialize(@move : Move?, @score : Int32)
    end

    def swap
      @score = -@score
    end
  end

  class Search
    getter depth : Int32
    getter board : Fork::Board
    getter already_searched : Hash(UInt64, Int32) = Hash(UInt64, Int32).new(LOOKUP_SIZE)
    getter move_table : Array(Array(Fork::Move | Nil)) =
      Array(Array(Fork::Move | Nil)).new(32) { Array(Fork::Move | Nil).new(32) { nil } }
    getter best_moves_yet : Array(Fork::Move | Nil) = [] of (Fork::Move | Nil)

    def initialize(@board : Fork::Board, @depth = 5)
    end

    def run
      1.upto(@depth) do |cur_depth|
        @already_searched = Hash(UInt64, Int32).new(LOOKUP_SIZE)
        @best_moves_yet = @move_table[0].first(cur_depth - 1)
        search(max_depth: cur_depth)
      end
      @move_table[0].first(@depth)
    end

    def search(board : Board = @board, alpha : Int32 = -INFINITY, beta : Int32 = INFINITY, ply : Int32 = 0, max_depth : Int = @depth) : Int32
      side = board.side
      return Eval.eval(board, side) if ply == max_depth

      best = LOSE
      moves = Moves.generate(board)
      best_move_from_prev = @best_moves_yet[ply]?
      if !best_move_from_prev.nil?
        if index = moves.index(best_move_from_prev)
          moves[index] = moves[0]
          moves[0] = best_move_from_prev
        end
      end

      moves.each do |m|
        copy = board.dup
        legal = copy.make_move(m)
        next unless legal
        next if @already_searched[copy.hash]?.try { |searched_ply| searched_ply <= ply }
        res = -search(copy, -beta, -alpha, ply + 1, max_depth)
        @already_searched[copy.hash] = ply
        if best < res
          best = res
        end
        if res > alpha
          alpha = res
          @move_table[ply][ply] = m
          (ply + 1).upto(max_depth) do |i|
            @move_table[ply][i] = @move_table[ply + 1][i]
          end
        end

        break if alpha >= beta
      end

      if best == LOSE
        if board.checked?(side)
          return LOSE
        else
          return 0i32
        end
      end
      best
    end
  end
end