mirror of
https://github.com/SleepingInsomniac/lx_chess_cr
synced 2024-12-25 09:58:58 +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/
|
||||
/.shards/
|
||||
*.dwarf
|
||||
*.log
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
require "./lx_chess/*"
|
||||
|
||||
require "option_parser"
|
||||
require "log"
|
||||
|
||||
options = {} of String => String | Nil
|
||||
|
||||
|
@ -28,20 +30,28 @@ OptionParser.parse do |parser|
|
|||
options["pgn_path"] = path
|
||||
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
|
||||
when /c(omputer)?/i
|
||||
options["player_white"] = "computer"
|
||||
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
|
||||
when /c(omputer)?/i
|
||||
options["player_black"] = "computer"
|
||||
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|
|
||||
STDERR.puts "ERROR: #{flag} is not a valid option."
|
||||
STDERR.puts parser
|
||||
|
@ -49,6 +59,12 @@ OptionParser.parse do |parser|
|
|||
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
|
||||
|
||||
if player_type = options["player_white"]?
|
||||
|
|
|
@ -8,6 +8,12 @@ module LxChess
|
|||
|
||||
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
|
||||
|
||||
def initialize(@width : Int16 = 8, @height : Int16 = 8)
|
||||
|
|
|
@ -2,6 +2,7 @@ require "./board"
|
|||
require "./game"
|
||||
require "./player"
|
||||
require "./piece"
|
||||
require "./move_tree"
|
||||
|
||||
module LxChess
|
||||
class Computer < Player
|
||||
|
@ -10,19 +11,44 @@ module LxChess
|
|||
end
|
||||
|
||||
def get_move(game : Game, turn : Int8? = nil)
|
||||
moves = best_moves(game, turn)
|
||||
moves.any? ? moves.sample : nil
|
||||
tree = best_moves(game, turn)
|
||||
|
||||
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
|
||||
|
||||
# Evaluate the score of a given *board*
|
||||
# TODO: more positional analysis
|
||||
def board_score(game : Game)
|
||||
def board_score(game : Game, turn : Int8? = nil)
|
||||
turn = turn || game.turn
|
||||
|
||||
score = 0
|
||||
|
||||
if game.checkmate?
|
||||
score += game.turn == 0 ? 10_000 : -10_000
|
||||
end
|
||||
|
||||
score + game.board.reduce(0) do |score, piece|
|
||||
score += game.board.reduce(0) do |score, piece|
|
||||
next score unless piece
|
||||
val =
|
||||
case piece.id
|
||||
|
@ -36,11 +62,18 @@ module LxChess
|
|||
end
|
||||
piece.black? ? score - val : score + val
|
||||
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
|
||||
|
||||
# Generate an array of moves (from => to) which result in an even or best score
|
||||
# TODO: promotion, depth, pruning
|
||||
def best_moves(game : Game, turn : Int8? = nil) : Array(Array(Int16))
|
||||
def move_sets(game : Game, turn : Int8? = nil)
|
||||
turn = turn || game.turn
|
||||
|
||||
move_sets = game.pieces_for(turn).map do |piece|
|
||||
|
@ -48,29 +81,57 @@ module LxChess
|
|||
game.remove_illegal_moves(set)
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
_best_moves = [] of Array(Int16)
|
||||
best_score = 0
|
||||
# Generate an array of moves (from => to) which result in an even or best score
|
||||
# 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
|
||||
|
||||
set.moves.each do |move|
|
||||
game.tmp_move(from: origin, to: move) do
|
||||
current_score = board_score(game)
|
||||
current_score = board_score(game, turn)
|
||||
|
||||
if current_score > best_score
|
||||
best_score = current_score
|
||||
_best_moves = [] of Array(Int16)
|
||||
_best_moves << [origin, move]
|
||||
elsif current_score == best_score
|
||||
_best_moves << [origin, move]
|
||||
tree = MoveTree.new(score: current_score, turn: game.next_turn(turn))
|
||||
root_tree << {origin, move, tree}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
_best_moves
|
||||
root_tree
|
||||
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?
|
||||
update
|
||||
end
|
||||
draw
|
||||
end
|
||||
|
||||
def update
|
||||
|
@ -54,14 +55,7 @@ module LxChess
|
|||
|
||||
if player.ai?
|
||||
if move = player.get_move(@game)
|
||||
from, to = 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
|
||||
from, to, promotion = move
|
||||
|
||||
san = @game.move_to_san(from, to, promotion)
|
||||
@changes << @game.make_move(from, to, promotion)
|
||||
|
@ -86,10 +80,11 @@ module LxChess
|
|||
when /suggest/i
|
||||
@gb.clear
|
||||
if suggestions = @evaluator.best_moves(@game)
|
||||
suggestions.each do |suggestion|
|
||||
@gb.highlight([suggestion[0]])
|
||||
@gb.highlight([suggestion[1]], "red")
|
||||
@log.unshift " #{suggestion.map { |s| @game.board.cord(s) }.join(" => ")}"
|
||||
suggestions.best_branches.each do |branch|
|
||||
from, to, _ = branch
|
||||
@gb.highlight([from])
|
||||
@gb.highlight([to], "red")
|
||||
@log.unshift " #{@game.board.cord(from)} => #{@game.board.cord(to)}"
|
||||
end
|
||||
@log.unshift "suggestions:"
|
||||
end
|
||||
|
@ -146,7 +141,7 @@ module LxChess
|
|||
end
|
||||
when nil
|
||||
else
|
||||
if input
|
||||
unless input.blank?
|
||||
notation = Notation.new(input)
|
||||
from, to = @game.parse_san(notation)
|
||||
if from && to
|
||||
|
|
Loading…
Reference in a new issue