diff --git a/modules/bot/src/main/scala/de/nowchess/bot/BotController.scala b/modules/bot/src/main/scala/de/nowchess/bot/BotController.scala index e770717..7c2891e 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/BotController.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/BotController.scala @@ -1,5 +1,7 @@ package de.nowchess.bot +import de.nowchess.bot.bots.ClassicalBot + object BotController { private var bots: Map[String, Bot] = Map.empty diff --git a/modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala b/modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala new file mode 100644 index 0000000..f48525c --- /dev/null +++ b/modules/bot/src/main/scala/de/nowchess/bot/ai/Weights.scala @@ -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 + +} diff --git a/modules/bot/src/main/scala/de/nowchess/bot/ClassicalBot.scala b/modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala similarity index 64% rename from modules/bot/src/main/scala/de/nowchess/bot/ClassicalBot.scala rename to modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala index e16ffda..4097e6b 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/ClassicalBot.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/bots/ClassicalBot.scala @@ -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})" diff --git a/modules/bot/src/main/scala/de/nowchess/bot/Evaluation.scala b/modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala similarity index 99% rename from modules/bot/src/main/scala/de/nowchess/bot/Evaluation.scala rename to modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala index f94cfb1..62c5c36 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/Evaluation.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/bots/classic/EvaluationClassic.scala @@ -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 diff --git a/modules/bot/src/main/scala/de/nowchess/bot/AlphaBetaSearch.scala b/modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala similarity index 96% rename from modules/bot/src/main/scala/de/nowchess/bot/AlphaBetaSearch.scala rename to modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala index d6f5601..6a9025a 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/AlphaBetaSearch.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/logic/AlphaBetaSearch.scala @@ -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 diff --git a/modules/bot/src/main/scala/de/nowchess/bot/MoveOrdering.scala b/modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala similarity index 99% rename from modules/bot/src/main/scala/de/nowchess/bot/MoveOrdering.scala rename to modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala index fd2ede1..876c7d4 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/MoveOrdering.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/logic/MoveOrdering.scala @@ -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: diff --git a/modules/bot/src/main/scala/de/nowchess/bot/TranspositionTable.scala b/modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala similarity index 97% rename from modules/bot/src/main/scala/de/nowchess/bot/TranspositionTable.scala rename to modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala index 147c457..84ad270 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/TranspositionTable.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/logic/TranspositionTable.scala @@ -1,4 +1,4 @@ -package de.nowchess.bot +package de.nowchess.bot.logic import de.nowchess.api.move.Move diff --git a/modules/bot/src/main/scala/de/nowchess/bot/PolyglotBook.scala b/modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala similarity index 98% rename from modules/bot/src/main/scala/de/nowchess/bot/PolyglotBook.scala rename to modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala index 97f15c2..4b83b95 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/PolyglotBook.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotBook.scala @@ -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 diff --git a/modules/bot/src/main/scala/de/nowchess/bot/PolyglotHash.scala b/modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala similarity index 99% rename from modules/bot/src/main/scala/de/nowchess/bot/PolyglotHash.scala rename to modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala index 9e2c46b..fb98d1c 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/PolyglotHash.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/util/PolyglotHash.scala @@ -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 diff --git a/modules/bot/src/main/scala/de/nowchess/bot/ZobristHash.scala b/modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala similarity index 98% rename from modules/bot/src/main/scala/de/nowchess/bot/ZobristHash.scala rename to modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala index bcb58d2..3685f08 100644 --- a/modules/bot/src/main/scala/de/nowchess/bot/ZobristHash.scala +++ b/modules/bot/src/main/scala/de/nowchess/bot/util/ZobristHash.scala @@ -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: diff --git a/modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala index 599d22d..c1ae49f 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/AlphaBetaSearchTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala index 68df90a..91c0337 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/ClassicalBotTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala index 7b0cda3..85435ed 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/EvaluationTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/MoveOrderingTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/MoveOrderingTest.scala index b90aabb..488f116 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/MoveOrderingTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/MoveOrderingTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala index 485caf1..e0e20a3 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/PolyglotBookTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala index e7304d3..68ac1f7 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/PolyglotHashTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/TranspositionTableTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/TranspositionTableTest.scala index 293dbb6..7cf48f9 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/TranspositionTableTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/TranspositionTableTest.scala @@ -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 diff --git a/modules/bot/src/test/scala/de/nowchess/bot/ZobristHashTest.scala b/modules/bot/src/test/scala/de/nowchess/bot/ZobristHashTest.scala index e764066..a68406d 100644 --- a/modules/bot/src/test/scala/de/nowchess/bot/ZobristHashTest.scala +++ b/modules/bot/src/test/scala/de/nowchess/bot/ZobristHashTest.scala @@ -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 diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineWithBotTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineWithBotTest.scala index e6908af..524a69d 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineWithBotTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineWithBotTest.scala @@ -2,7 +2,8 @@ package de.nowchess.chess.engine import de.nowchess.api.board.Color import de.nowchess.api.game.GameContext -import de.nowchess.bot.{BotDifficulty, ClassicalBot, BotController} +import de.nowchess.bot.bots.ClassicalBot +import de.nowchess.bot.{BotDifficulty, BotController} import de.nowchess.chess.observer.* import de.nowchess.rules.sets.DefaultRules import org.scalatest.funsuite.AnyFunSuite diff --git a/modules/ui/src/main/scala/de/nowchess/ui/Main.scala b/modules/ui/src/main/scala/de/nowchess/ui/Main.scala index 881477c..11b7613 100644 --- a/modules/ui/src/main/scala/de/nowchess/ui/Main.scala +++ b/modules/ui/src/main/scala/de/nowchess/ui/Main.scala @@ -1,7 +1,9 @@ package de.nowchess.ui import de.nowchess.api.board.Color.Black -import de.nowchess.bot.{BotDifficulty, ClassicalBot, PolyglotBook} +import de.nowchess.bot.util.PolyglotBook +import de.nowchess.bot.BotDifficulty +import de.nowchess.bot.bots.ClassicalBot import de.nowchess.chess.engine.GameEngine import de.nowchess.ui.terminal.TerminalUI import de.nowchess.ui.gui.ChessGUILauncher