feat(official-bots): make HybridBot veto actionable and use it for expert
Build & Test (NowChessSystems) TeamCity build finished
Build & Test (NowChessSystems) TeamCity build finished
When classical and NNUE evals diverge above the veto threshold, HybridBot now re-searches excluding the suspect move and switches to NNUE's preferred alternative instead of merely logging. BotController maps the expert bot to HybridBot so tournament auto-join uses it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -80,76 +80,58 @@ class HybridBotTest extends AnyFunSuite with Matchers:
|
||||
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)
|
||||
// Classical search picks mateMove (delivers mate); NNUE distrusts it and prefers altMove.
|
||||
private val mateMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal())
|
||||
private val altMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R3), MoveType.Normal())
|
||||
|
||||
object LowNnue extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 0
|
||||
private def vetoRules: RuleSet = new RuleSet:
|
||||
private def fresh(ctx: GameContext): Boolean = ctx.moves.isEmpty
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def legalMoves(context: GameContext)(square: Square): List[Move] = Nil
|
||||
def allLegalMoves(context: GameContext): List[Move] =
|
||||
if fresh(context) then List(mateMove, altMove) else Nil
|
||||
def isCheck(context: GameContext): Boolean = false
|
||||
def isCheckmate(context: GameContext): Boolean = context.moves.lastOption.contains(mateMove)
|
||||
def isStalemate(context: GameContext): Boolean = context.moves.lastOption.contains(altMove)
|
||||
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 HighClassic extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = 10_000
|
||||
// NNUE rates the mate move worse for us (higher = better for opponent) than the alternative.
|
||||
private object DistrustfulNnue extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = if context.moves.lastOption.contains(mateMove) then 5_000 else 0
|
||||
|
||||
private object HighClassic extends Evaluation:
|
||||
val CHECKMATE_SCORE: Int = 10_000_000
|
||||
val DRAW_SCORE: Int = 0
|
||||
def evaluate(context: GameContext): Int = if context.moves.lastOption.contains(mateMove) then 10_000 else 0
|
||||
|
||||
test("HybridBot switches to NNUE's preferred move and reports veto when evals diverge"):
|
||||
val reported = AtomicBoolean(false)
|
||||
val bot = HybridBot(
|
||||
BotDifficulty.Easy,
|
||||
rules = oneMoveRules,
|
||||
nnueEvaluation = LowNnue,
|
||||
rules = vetoRules,
|
||||
nnueEvaluation = DistrustfulNnue,
|
||||
classicalEvaluation = HighClassic,
|
||||
vetoReporter = _ => reported.set(true),
|
||||
)
|
||||
|
||||
bot.apply(GameContext.initial) should be(Some(forcedMove))
|
||||
bot.apply(GameContext.initial) should be(Some(altMove))
|
||||
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,
|
||||
rules = vetoRules,
|
||||
nnueEvaluation = DistrustfulNnue,
|
||||
classicalEvaluation = HighClassic,
|
||||
)
|
||||
|
||||
val printed = Console.withOut(new java.io.ByteArrayOutputStream()) {
|
||||
bot.apply(GameContext.initial)
|
||||
}
|
||||
printed should be(Some(forcedMove))
|
||||
printed should be(Some(altMove))
|
||||
|
||||
Reference in New Issue
Block a user