Add depth to move calculations

This commit is contained in:
Alex Clink 2021-10-03 22:26:09 -04:00
parent 6aea3e95ca
commit fa4486569e
6 changed files with 149 additions and 33 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
/bin/ /bin/
/.shards/ /.shards/
*.dwarf *.dwarf
*.log

View file

@ -1,5 +1,7 @@
require "./lx_chess/*" require "./lx_chess/*"
require "option_parser" require "option_parser"
require "log"
options = {} of String => String | Nil options = {} of String => String | Nil
@ -28,20 +30,28 @@ OptionParser.parse do |parser|
options["pgn_path"] = path options["pgn_path"] = path
end end
parser.on("--player-white=PLAYER", "set the type of player") do |player_type| parser.on("--player-white=PLAYER", "set the type of player (human|computer)") do |player_type|
case player_type case player_type
when /c(omputer)?/i when /c(omputer)?/i
options["player_white"] = "computer" options["player_white"] = "computer"
end end
end end
parser.on("--player-black=PLAYER", "set the type of player") do |player_type| parser.on("--player-black=PLAYER", "set the type of player (human|computer)") do |player_type|
case player_type case player_type
when /c(omputer)?/i when /c(omputer)?/i
options["player_black"] = "computer" options["player_black"] = "computer"
end end
end end
parser.on("--log=FILE", "log to file") do |file_Path|
options["log_file"] = file_Path
end
parser.on("--log-level=LEVEL", "set log level, 0-7") do |level|
options["log_level"] = level
end
parser.invalid_option do |flag| parser.invalid_option do |flag|
STDERR.puts "ERROR: #{flag} is not a valid option." STDERR.puts "ERROR: #{flag} is not a valid option."
STDERR.puts parser STDERR.puts parser
@ -49,6 +59,12 @@ OptionParser.parse do |parser|
end end
end end
if log_file = options["log_file"]?
log_level = options["log_level"]? || "2"
log_level = Log::Severity.from_value(log_level.to_i8)
Log.setup(log_level, Log::IOBackend.new(File.new(log_file, "a+")))
end
players = [] of LxChess::Player players = [] of LxChess::Player
if player_type = options["player_white"]? if player_type = options["player_white"]?

View file

@ -8,6 +8,12 @@ module LxChess
LETTERS = ('a'..'z').to_a LETTERS = ('a'..'z').to_a
# Convert an *index* into a human coordinate (ex: `a1`)
def self.cord(index : Int, width = 8)
y, x = index.divmod(width)
"#{LETTERS[x]}#{y + 1}"
end
property :width, :height, :squares property :width, :height, :squares
def initialize(@width : Int16 = 8, @height : Int16 = 8) def initialize(@width : Int16 = 8, @height : Int16 = 8)

View file

@ -2,6 +2,7 @@ require "./board"
require "./game" require "./game"
require "./player" require "./player"
require "./piece" require "./piece"
require "./move_tree"
module LxChess module LxChess
class Computer < Player class Computer < Player
@ -10,19 +11,44 @@ module LxChess
end end
def get_move(game : Game, turn : Int8? = nil) def get_move(game : Game, turn : Int8? = nil)
moves = best_moves(game, turn) tree = best_moves(game, turn)
moves.any? ? moves.sample : nil
Log.debug do
"Best moves: #{tree.best_branches.map { |b| [game.board.cord(b[0]), game.board.cord(b[1])].join(" => ") + " : #{b[2].score}" }.join(", ")}"
end
move = tree.best_branches.sample
from, to, _ = move
raise "from is nil" unless from_piece = game.board[from]
promotion =
case game.turn
when 0
if from_piece.pawn?
game.board.rank(to) == game.board.height - 1 ? 'Q' : nil
end
when 1
if from_piece.pawn?
game.board.rank(to) == 0 ? 'Q' : nil
end
end
Log.debug { "Chose random best move: #{game.board.cord(from)} => #{game.board.cord(to)}" }
{from, to, promotion}
end end
# Evaluate the score of a given *board* # Evaluate the score of a given *board*
# TODO: more positional analysis # TODO: more positional analysis
def board_score(game : Game) def board_score(game : Game, turn : Int8? = nil)
turn = turn || game.turn
score = 0 score = 0
if game.checkmate? if game.checkmate?
score += game.turn == 0 ? 10_000 : -10_000 score += game.turn == 0 ? 10_000 : -10_000
end end
score + game.board.reduce(0) do |score, piece| score += game.board.reduce(0) do |score, piece|
next score unless piece next score unless piece
val = val =
case piece.id case piece.id
@ -36,11 +62,18 @@ module LxChess
end end
piece.black? ? score - val : score + val piece.black? ? score - val : score + val
end end
return score if game.players.none?
# If the player is black, a negative value is good
if game.players[game.turn] == self
score.abs
else
-score.abs
end
end end
# Generate an array of moves (from => to) which result in an even or best score def move_sets(game : Game, turn : Int8? = nil)
# TODO: promotion, depth, pruning
def best_moves(game : Game, turn : Int8? = nil) : Array(Array(Int16))
turn = turn || game.turn turn = turn || game.turn
move_sets = game.pieces_for(turn).map do |piece| move_sets = game.pieces_for(turn).map do |piece|
@ -48,29 +81,57 @@ module LxChess
game.remove_illegal_moves(set) game.remove_illegal_moves(set)
end end
end.compact end.compact
end
_best_moves = [] of Array(Int16) # Generate an array of moves (from => to) which result in an even or best score
best_score = 0 # TODO: promotion, pruning
def best_moves(game : Game, turn : Int8? = nil, depth = 0, root_tree : MoveTree? = nil) # : Array(Array(Int16))
turn = turn || game.turn
move_sets.each do |set| Log.debug { "Evaluating best moves for turn: #{turn}, depth: #{depth}..." }
if root_tree.nil?
root_tree = MoveTree.new score: board_score(game, turn), turn: turn
end
Log.debug { "Current Board score: #{root_tree.score}, turn: #{turn}" }
move_sets(game, turn).each do |set|
origin = set.piece.index origin = set.piece.index
set.moves.each do |move| set.moves.each do |move|
game.tmp_move(from: origin, to: move) do game.tmp_move(from: origin, to: move) do
current_score = board_score(game) current_score = board_score(game, turn)
if current_score > best_score tree = MoveTree.new(score: current_score, turn: game.next_turn(turn))
best_score = current_score root_tree << {origin, move, tree}
_best_moves = [] of Array(Int16) end
_best_moves << [origin, move] end
elsif current_score == best_score end
_best_moves << [origin, move]
Log.debug { "Considered #{root_tree.branches.size} moves at depth #{depth}" }
if depth < 2
root_tree.branches.each do |branch|
from, to, tree = branch
Log.debug { "Considering #{game.board.cord(from)} => #{game.board.cord(to)} ..." }
game.tmp_move(from: from, to: to) do
best_moves(game, turn, depth + 1, tree)
if tree.score > root_tree.score
root_tree.score = tree.score
root_tree.best_branches.truncate(0, 0)
end
if tree.score == root_tree.score
root_tree.best_branches << branch
end end
end end
end end
end end
_best_moves root_tree
end end
end end
end end

37
src/lx_chess/move_tree.cr Normal file
View file

@ -0,0 +1,37 @@
require "./board"
module LxChess
class MoveTree
alias Branch = Tuple(Int16, Int16, MoveTree)
property branches = [] of Branch
property score : Int32
property parent : MoveTree? = nil
getter best_branches = [] of Branch
getter turn : Int8
def initialize(@score, @turn)
end
def <<(other : Branch)
from, to, tree = other
tree.parent = self
# a position is only as good as its outcomes
@score = tree.score if @branches.empty?
if tree.score > @score
Log.debug { "Found better move: #{Board.cord(from)} => #{Board.cord(to)} : #{tree.score}, truncating best branches" }
@score = tree.score
@best_branches.truncate(0, 0)
end
if tree.score == @score
@best_branches << other
end
@branches << other
end
end
end

View file

@ -47,6 +47,7 @@ module LxChess
break if @game.checkmate? break if @game.checkmate?
update update
end end
draw
end end
def update def update
@ -54,14 +55,7 @@ module LxChess
if player.ai? if player.ai?
if move = player.get_move(@game) if move = player.get_move(@game)
from, to = move from, to, promotion = move
promotion =
case @game.turn
when 0
@game.board.rank(to) == @game.board.height - 1 ? 'Q' : nil
when 1
@game.board.rank(to) == 0 ? 'Q' : nil
end
san = @game.move_to_san(from, to, promotion) san = @game.move_to_san(from, to, promotion)
@changes << @game.make_move(from, to, promotion) @changes << @game.make_move(from, to, promotion)
@ -86,10 +80,11 @@ module LxChess
when /suggest/i when /suggest/i
@gb.clear @gb.clear
if suggestions = @evaluator.best_moves(@game) if suggestions = @evaluator.best_moves(@game)
suggestions.each do |suggestion| suggestions.best_branches.each do |branch|
@gb.highlight([suggestion[0]]) from, to, _ = branch
@gb.highlight([suggestion[1]], "red") @gb.highlight([from])
@log.unshift " #{suggestion.map { |s| @game.board.cord(s) }.join(" => ")}" @gb.highlight([to], "red")
@log.unshift " #{@game.board.cord(from)} => #{@game.board.cord(to)}"
end end
@log.unshift "suggestions:" @log.unshift "suggestions:"
end end
@ -146,7 +141,7 @@ module LxChess
end end
when nil when nil
else else
if input unless input.blank?
notation = Notation.new(input) notation = Notation.new(input)
from, to = @game.parse_san(notation) from, to = @game.parse_san(notation)
if from && to if from && to