mirror of
https://github.com/SleepingInsomniac/lx_chess_cr
synced 2024-12-26 09:58:57 +01:00
Add depth to move calculations
This commit is contained in:
parent
6aea3e95ca
commit
fa4486569e
6 changed files with 149 additions and 33 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@
|
||||||
/bin/
|
/bin/
|
||||||
/.shards/
|
/.shards/
|
||||||
*.dwarf
|
*.dwarf
|
||||||
|
*.log
|
||||||
|
|
|
@ -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"]?
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
37
src/lx_chess/move_tree.cr
Normal 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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue