feat: refactor AlphaBetaSearch and ClassicalBot for improved evaluation and organization
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.bot.bots.ClassicalBot
|
||||
|
||||
object BotController {
|
||||
|
||||
private var bots: Map[String, Bot] = Map.empty
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.nowchess.bot.ai
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
|
||||
trait Weights {
|
||||
|
||||
def CHECKMATE_SCORE: Int
|
||||
def DRAW_SCORE: Int
|
||||
|
||||
def evaluate(context: GameContext): Int
|
||||
|
||||
}
|
||||
+6
-2
@@ -1,7 +1,11 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.bots
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.bot.bots.classic.EvaluationClassic
|
||||
import de.nowchess.bot.logic.AlphaBetaSearch
|
||||
import de.nowchess.bot.util.PolyglotBook
|
||||
import de.nowchess.bot.{Bot, BotDifficulty}
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
|
||||
@@ -11,7 +15,7 @@ final class ClassicalBot(
|
||||
book: Option[PolyglotBook] = None
|
||||
) extends Bot:
|
||||
|
||||
private val search: AlphaBetaSearch = AlphaBetaSearch(rules)
|
||||
private val search: AlphaBetaSearch = AlphaBetaSearch(rules, weights = EvaluationClassic)
|
||||
private val TIME_BUDGET_MS = 1000L
|
||||
|
||||
override val name: String = s"ClassicalBot(${difficulty.toString})"
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.bots.classic
|
||||
|
||||
import de.nowchess.api.board.{Color, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.bot.ai.Weights
|
||||
|
||||
object Evaluation:
|
||||
object EvaluationClassic extends Weights:
|
||||
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
+14
-9
@@ -1,18 +1,23 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.logic
|
||||
|
||||
import de.nowchess.api.board.PieceType
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.ai.Weights
|
||||
import de.nowchess.bot.logic.{MoveOrdering, TTEntry, TTFlag, TranspositionTable}
|
||||
import de.nowchess.bot.util.ZobristHash
|
||||
import de.nowchess.rules.RuleSet
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
final class AlphaBetaSearch(
|
||||
rules: RuleSet = DefaultRules,
|
||||
tt: TranspositionTable = TranspositionTable(),
|
||||
weights: Weights,
|
||||
numThreads: Int = Runtime.getRuntime().availableProcessors()
|
||||
):
|
||||
|
||||
@@ -135,7 +140,7 @@ final class AlphaBetaSearch(
|
||||
// Periodic time check
|
||||
nodeCount += 1
|
||||
if nodeCount % TIME_CHECK_FREQUENCY == 0 && isOutOfTime() then
|
||||
return (Evaluation.evaluate(context), None)
|
||||
return (weights.evaluate(context), None)
|
||||
|
||||
val hash = ZobristHash.hash(context)
|
||||
|
||||
@@ -159,13 +164,13 @@ final class AlphaBetaSearch(
|
||||
val legalMoves = rules.allLegalMoves(context)
|
||||
if legalMoves.isEmpty then
|
||||
val score = if rules.isCheckmate(context) then
|
||||
-(Evaluation.CHECKMATE_SCORE - ply)
|
||||
-(weights.CHECKMATE_SCORE - ply)
|
||||
else
|
||||
Evaluation.DRAW_SCORE
|
||||
weights.DRAW_SCORE
|
||||
return (score, None)
|
||||
|
||||
if rules.isInsufficientMaterial(context) || rules.isFiftyMoveRule(context) then
|
||||
return (Evaluation.DRAW_SCORE, None)
|
||||
return (weights.DRAW_SCORE, None)
|
||||
|
||||
// Leaf node: call quiescence
|
||||
if depth == 0 then
|
||||
@@ -211,7 +216,7 @@ final class AlphaBetaSearch(
|
||||
|
||||
// Futility pruning at frontier nodes: if static eval + margin is still below alpha, skip quiet moves
|
||||
if depth == 1 && isQuiet && moveNumber > 2 then
|
||||
val staticEval = Evaluation.evaluate(context)
|
||||
val staticEval = weights.evaluate(context)
|
||||
if staticEval + FUTILITY_MARGIN < alpha then
|
||||
moveNumber += 1
|
||||
|
||||
@@ -328,7 +333,7 @@ final class AlphaBetaSearch(
|
||||
beta: Int
|
||||
): Int =
|
||||
// Stand-pat: evaluate current position
|
||||
val standPat = Evaluation.evaluate(context)
|
||||
val standPat = weights.evaluate(context)
|
||||
if standPat >= beta then
|
||||
return beta
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.logic
|
||||
|
||||
import de.nowchess.api.board.PieceType
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
|
||||
import scala.collection.mutable
|
||||
|
||||
object MoveOrdering:
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.logic
|
||||
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.util
|
||||
|
||||
import de.nowchess.api.board.{Color, File, PieceType, Rank, Square}
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
|
||||
import java.io.{DataInputStream, FileInputStream}
|
||||
import scala.collection.mutable
|
||||
import scala.util.Random
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.util
|
||||
|
||||
import de.nowchess.api.board.{Color, Piece, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
package de.nowchess.bot
|
||||
package de.nowchess.bot.util
|
||||
|
||||
import de.nowchess.api.board.{Color, File, PieceType, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
object ZobristHash:
|
||||
@@ -3,6 +3,7 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.logic.AlphaBetaSearch
|
||||
import de.nowchess.rules.RuleSet
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.bot.bots.ClassicalBot
|
||||
import de.nowchess.rules.RuleSet
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -3,13 +3,14 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.Board
|
||||
import de.nowchess.bot.bots.classic.EvaluationClassic
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("initial position evaluates to tempo bonus"):
|
||||
val eval = Evaluation.evaluate(GameContext.initial)
|
||||
val eval = EvaluationClassic.evaluate(GameContext.initial)
|
||||
eval should equal(10) // TEMPO_BONUS only
|
||||
|
||||
test("remove white queen gives negative evaluation"):
|
||||
@@ -18,7 +19,7 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
val emptySquare = Square(File.D, Rank.R1)
|
||||
val boardWithoutQueen = board.pieces.filter((sq, _) => sq != emptySquare)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = Evaluation.evaluate(newContext)
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
eval should be < 0
|
||||
|
||||
test("remove black queen gives positive evaluation"):
|
||||
@@ -27,7 +28,7 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
val emptySquare = Square(File.D, Rank.R8)
|
||||
val boardWithoutQueen = board.pieces.filter((sq, _) => sq != emptySquare)
|
||||
val newContext = initial.withBoard(Board(boardWithoutQueen))
|
||||
val eval = Evaluation.evaluate(newContext)
|
||||
val eval = EvaluationClassic.evaluate(newContext)
|
||||
eval should be > 0
|
||||
|
||||
test("different piece-square bonuses are applied"):
|
||||
@@ -37,27 +38,27 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
val knightD4 = GameContext.initial.withBoard(knightD4Board)
|
||||
val knightA1 = GameContext.initial.withBoard(knightA1Board)
|
||||
|
||||
val eval1 = Evaluation.evaluate(knightD4)
|
||||
val eval2 = Evaluation.evaluate(knightA1)
|
||||
val eval1 = EvaluationClassic.evaluate(knightD4)
|
||||
val eval2 = EvaluationClassic.evaluate(knightA1)
|
||||
eval1 should be > eval2 // d4 (center) is better than a1 (corner) for knight
|
||||
|
||||
test("all piece types are in material map"):
|
||||
PieceType.values.length should be > 0
|
||||
// Just verify evaluate works with all piece types
|
||||
val eval = Evaluation.evaluate(GameContext.initial)
|
||||
eval should not be (Evaluation.CHECKMATE_SCORE)
|
||||
val eval = EvaluationClassic.evaluate(GameContext.initial)
|
||||
eval should not be (EvaluationClassic.CHECKMATE_SCORE)
|
||||
|
||||
test("CHECKMATE_SCORE and DRAW_SCORE are accessible"):
|
||||
Evaluation.CHECKMATE_SCORE should equal(10_000_000)
|
||||
Evaluation.DRAW_SCORE should equal(0)
|
||||
EvaluationClassic.CHECKMATE_SCORE should equal(10_000_000)
|
||||
EvaluationClassic.DRAW_SCORE should equal(0)
|
||||
|
||||
test("active knight (center) scores higher than passive knight (corner)"):
|
||||
val knightD4Board = Board(Map(Square(File.D, Rank.R4) -> Piece.WhiteKnight))
|
||||
val knightA1Board = Board(Map(Square(File.A, Rank.R1) -> Piece.WhiteKnight))
|
||||
val knightD4Context = GameContext.initial.withBoard(knightD4Board)
|
||||
val knightA1Context = GameContext.initial.withBoard(knightA1Board)
|
||||
val evalD4 = Evaluation.evaluate(knightD4Context)
|
||||
val evalA1 = Evaluation.evaluate(knightA1Context)
|
||||
val evalD4 = EvaluationClassic.evaluate(knightD4Context)
|
||||
val evalA1 = EvaluationClassic.evaluate(knightA1Context)
|
||||
evalD4 should be > evalA1 // Knight on d4 (center, more mobility) should score higher
|
||||
|
||||
test("bishop pair scores higher than bishop + knight"):
|
||||
@@ -71,8 +72,8 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
))
|
||||
val pairContext = GameContext.initial.withBoard(bishopPairBoard)
|
||||
val knightContext = GameContext.initial.withBoard(bishopKnightBoard)
|
||||
val evalPair = Evaluation.evaluate(pairContext)
|
||||
val evalKnight = Evaluation.evaluate(knightContext)
|
||||
val evalPair = EvaluationClassic.evaluate(pairContext)
|
||||
val evalKnight = EvaluationClassic.evaluate(knightContext)
|
||||
evalPair should be > evalKnight // Bishop pair should score higher
|
||||
|
||||
test("rook on 7th rank scores higher than rook on 4th rank"):
|
||||
@@ -80,6 +81,6 @@ class EvaluationTest extends AnyFunSuite with Matchers:
|
||||
val rook4thBoard = Board(Map(Square(File.A, Rank.R4) -> Piece.WhiteRook))
|
||||
val rook7thContext = GameContext.initial.withBoard(rook7thBoard)
|
||||
val rook4thContext = GameContext.initial.withBoard(rook4thBoard)
|
||||
val eval7th = Evaluation.evaluate(rook7thContext)
|
||||
val eval4th = Evaluation.evaluate(rook4thContext)
|
||||
val eval7th = EvaluationClassic.evaluate(rook7thContext)
|
||||
val eval4th = EvaluationClassic.evaluate(rook4thContext)
|
||||
eval7th should be > eval4th // Rook on 7th rank should score higher
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.bot.logic.MoveOrdering
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ package de.nowchess.bot
|
||||
import de.nowchess.api.board.{Color, File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.bot.bots.ClassicalBot
|
||||
import de.nowchess.bot.util.{PolyglotBook, PolyglotHash}
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
import java.io.{DataOutputStream, FileOutputStream}
|
||||
import java.nio.file.Files
|
||||
import scala.util.Using
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{Color, File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.bot.util.PolyglotHash
|
||||
import de.nowchess.io.fen.FenParser
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.logic.{TTEntry, TTFlag, TranspositionTable}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, CastlingRights, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.util.ZobristHash
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user