@@ -0,0 +1,155 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{File, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.bot.ai.Evaluation
|
||||
import de.nowchess.bot.bots.HybridBot
|
||||
import de.nowchess.bot.util.{PolyglotBook, PolyglotHash}
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
import java.io.{DataOutputStream, FileOutputStream}
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import scala.util.Using
|
||||
|
||||
class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("HybridBot apply returns a move on the initial position"):
|
||||
val bot = HybridBot(BotDifficulty.Easy)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("HybridBot apply returns None when no legal moves"):
|
||||
val noMovesRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def allLegalMoves(context: GameContext): List[Move] = Nil
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = true
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
val bot = HybridBot(BotDifficulty.Easy, noMovesRules)
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should be(None)
|
||||
|
||||
test("HybridBot with empty book falls through to search"):
|
||||
val emptyBook = PolyglotBook("/nonexistent/book.bin")
|
||||
val bot = HybridBot(BotDifficulty.Easy, book = Some(emptyBook))
|
||||
val move = bot.apply(GameContext.initial)
|
||||
move should not be None
|
||||
|
||||
test("HybridBot skips move repeated three times"):
|
||||
val repeatedMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
val onlyMoveRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(repeatedMove)
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext = context
|
||||
val ctx = GameContext.initial.copy(moves = List(repeatedMove, repeatedMove, repeatedMove))
|
||||
val bot = HybridBot(BotDifficulty.Easy, onlyMoveRules)
|
||||
bot.apply(ctx) should be(None)
|
||||
|
||||
test("HybridBot uses book move when available"):
|
||||
val tempFile = Files.createTempFile("hybrid_book", ".bin")
|
||||
try
|
||||
val ctx = GameContext.initial
|
||||
val hash = PolyglotHash.hash(ctx)
|
||||
val e2e4: Short = (4 | (3 << 3) | (4 << 6) | (1 << 9)).toShort
|
||||
|
||||
Using(DataOutputStream(FileOutputStream(tempFile.toFile))) { dos =>
|
||||
dos.writeLong(hash)
|
||||
dos.writeShort(e2e4)
|
||||
dos.writeShort(100)
|
||||
dos.writeInt(0)
|
||||
}.get
|
||||
|
||||
val book = PolyglotBook(tempFile.toString)
|
||||
val bot = HybridBot(BotDifficulty.Easy, book = Some(book))
|
||||
bot.apply(ctx) should be(Some(Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())))
|
||||
finally Files.deleteIfExists(tempFile)
|
||||
|
||||
test("HybridBot reports veto when classical and NNUE differ above threshold"):
|
||||
val forcedMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||
val oneMoveRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(forcedMove)
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext =
|
||||
context.copy(turn = context.turn.opposite, moves = context.moves :+ move)
|
||||
|
||||
object LowNnue extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 0
|
||||
|
||||
object HighClassic extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 10_000
|
||||
|
||||
val reported = AtomicBoolean(false)
|
||||
val bot = HybridBot(
|
||||
BotDifficulty.Easy,
|
||||
rules = oneMoveRules,
|
||||
nnueEvaluation = LowNnue,
|
||||
classicalEvaluation = HighClassic,
|
||||
vetoReporter = _ => reported.set(true),
|
||||
)
|
||||
|
||||
bot.apply(GameContext.initial) should be(Some(forcedMove))
|
||||
reported.get should be(true)
|
||||
|
||||
test("HybridBot default veto reporter prints when threshold is exceeded"):
|
||||
val forcedMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||
val oneMoveRules = new RuleSet:
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = List(forcedMove)
|
||||
def allLegalMoves(context: GameContext): List[Move] = List(forcedMove)
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = false
|
||||
def isStalemate(context: GameContext): Boolean = false
|
||||
def isInsufficientMaterial(context: GameContext): Boolean = false
|
||||
def isFiftyMoveRule(context: GameContext): Boolean = false
|
||||
def isThreefoldRepetition(context: GameContext): Boolean = false
|
||||
def applyMove(context: GameContext)(move: Move): GameContext =
|
||||
context.copy(turn = context.turn.opposite, moves = context.moves :+ move)
|
||||
|
||||
object LowNnue extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 0
|
||||
|
||||
object HighClassic extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 10_000
|
||||
|
||||
val bot = HybridBot(
|
||||
BotDifficulty.Easy,
|
||||
rules = oneMoveRules,
|
||||
nnueEvaluation = LowNnue,
|
||||
classicalEvaluation = HighClassic,
|
||||
)
|
||||
|
||||
val printed = Console.withOut(new java.io.ByteArrayOutputStream()) {
|
||||
bot.apply(GameContext.initial)
|
||||
}
|
||||
printed should be(Some(forcedMove))
|
||||
Reference in New Issue
Block a user