@@ -0,0 +1,219 @@
|
||||
package de.nowchess.bot
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, 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
|
||||
|
||||
class MoveOrderingTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("queen capture ranks higher than rook capture"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackQueen,
|
||||
Square(File.E, Rank.R6) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val queenCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
val rookCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6), MoveType.Normal(true))
|
||||
|
||||
val queenScore = MoveOrdering.score(context, queenCapture, None)
|
||||
val rookScore = MoveOrdering.score(context, rookCapture, None)
|
||||
queenScore should be > rookScore
|
||||
|
||||
test("quiet move ranks lower than capture"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val capture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
val quiet = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5))
|
||||
|
||||
val captureScore = MoveOrdering.score(context, capture, None)
|
||||
val quietScore = MoveOrdering.score(context, quiet, None)
|
||||
captureScore should be > quietScore
|
||||
|
||||
test("TT best move ranks first"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val bestMove = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||
val otherCapture = Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
|
||||
|
||||
val bestScore = MoveOrdering.score(context, bestMove, Some(bestMove))
|
||||
val otherScore = MoveOrdering.score(context, otherCapture, Some(bestMove))
|
||||
bestScore should equal(Int.MaxValue)
|
||||
otherScore should be < bestScore
|
||||
|
||||
test("promotion to queen ranks high"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val promotionQueen =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val promotionKnight =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight))
|
||||
|
||||
val queenScore = MoveOrdering.score(context, promotionQueen, None)
|
||||
val knightScore = MoveOrdering.score(context, promotionKnight, None)
|
||||
queenScore should be > knightScore
|
||||
queenScore should be > 100_000 // Queen promotion score is > 100_000
|
||||
|
||||
test("en passant is treated as capture"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val epCapture = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
|
||||
val quiet = Move(Square(File.E, Rank.R5), Square(File.E, Rank.R6))
|
||||
|
||||
val epScore = MoveOrdering.score(context, epCapture, None)
|
||||
val quietScore = MoveOrdering.score(context, quiet, None)
|
||||
epScore should be > quietScore
|
||||
|
||||
test("sort returns moves ordered by score"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val moves = List(
|
||||
Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)), // Rook capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true)), // Pawn capture
|
||||
Move(Square(File.E, Rank.R4), Square(File.E, Rank.R6)), // Quiet
|
||||
)
|
||||
val sorted = MoveOrdering.sort(context, moves, None)
|
||||
// Rook capture should be first (higher victim value)
|
||||
sorted.head.to should equal(Square(File.D, Rank.R5))
|
||||
// Pawn capture should be second
|
||||
sorted(1).to should equal(Square(File.E, Rank.R5))
|
||||
// Quiet should be last
|
||||
sorted.last.to should equal(Square(File.E, Rank.R6))
|
||||
|
||||
test("castling move is quiet (not capture)"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
val castleMove = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||
val score = MoveOrdering.score(context, castleMove, None)
|
||||
score should equal(0) // Quiet move
|
||||
|
||||
test("all MoveType variants are handled in victimValue"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.H, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.E, Rank.R2) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Test castling queenside - should have victim value 0
|
||||
val castleQs = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
|
||||
val scoreQs = MoveOrdering.score(context, castleQs, None)
|
||||
scoreQs should equal(0)
|
||||
|
||||
test("attackerValue covers all piece types"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
Square(File.B, Rank.R1) -> Piece.WhiteKnight,
|
||||
Square(File.C, Rank.R1) -> Piece.WhiteBishop,
|
||||
Square(File.D, Rank.R1) -> Piece.WhiteQueen,
|
||||
Square(File.E, Rank.R1) -> Piece.WhiteKing,
|
||||
Square(File.F, Rank.R2) -> Piece.WhitePawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Create captures with each piece type
|
||||
val rookCapture = Move(Square(File.A, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val knightCapture = Move(Square(File.B, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val bishopCapture = Move(Square(File.C, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val queenCapture = Move(Square(File.D, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val kingCapture = Move(Square(File.E, Rank.R1), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
val pawnCapture = Move(Square(File.F, Rank.R2), Square(File.A, Rank.R8), MoveType.Normal(true))
|
||||
|
||||
// Just verify all are scored without error
|
||||
MoveOrdering.score(context, rookCapture, None) should be >= 0
|
||||
MoveOrdering.score(context, knightCapture, None) should be >= 0
|
||||
MoveOrdering.score(context, bishopCapture, None) should be >= 0
|
||||
MoveOrdering.score(context, queenCapture, None) should be >= 0
|
||||
MoveOrdering.score(context, kingCapture, None) should be >= 0
|
||||
MoveOrdering.score(context, pawnCapture, None) should be >= 0
|
||||
|
||||
test("promotion capture is distinct from quiet promotion"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R8) -> Piece.BlackPawn,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board)
|
||||
// Promotion with capture
|
||||
val promotionWithCapture =
|
||||
Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
// Regular queen promotion (no capture)
|
||||
val quietPromotion =
|
||||
Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
|
||||
val score1 = MoveOrdering.score(context, promotionWithCapture, None)
|
||||
val score2 = MoveOrdering.score(context, quietPromotion, None)
|
||||
score1 should be > score2
|
||||
|
||||
test("non-Queen promotion captures trigger promotionPieceType for Knight, Bishop, Rook"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.E, Rank.R7) -> Piece.WhitePawn,
|
||||
Square(File.D, Rank.R8) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val knightPromo = Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Knight))
|
||||
val bishopPromo = Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Bishop))
|
||||
val rookPromo = Move(Square(File.E, Rank.R7), Square(File.D, Rank.R8), MoveType.Promotion(PromotionPiece.Rook))
|
||||
MoveOrdering.score(context, knightPromo, None) should be > 0
|
||||
MoveOrdering.score(context, bishopPromo, None) should be > 0
|
||||
MoveOrdering.score(context, rookPromo, None) should be > 0
|
||||
|
||||
test("negative SEE capture path is scored below neutral capture baseline"):
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.D, Rank.R4) -> Piece.WhiteQueen,
|
||||
Square(File.D, Rank.R5) -> Piece.BlackPawn,
|
||||
Square(File.D, Rank.R8) -> Piece.BlackRook,
|
||||
),
|
||||
)
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val move = Move(Square(File.D, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
|
||||
|
||||
MoveOrdering.score(context, move, None) should be < 100_000
|
||||
|
||||
test("non-capture move keeps fallback scoring at zero"):
|
||||
val board = Board(Map(Square(File.E, Rank.R1) -> Piece.WhiteKing, Square(File.A, Rank.R8) -> Piece.BlackKing))
|
||||
val context = GameContext.initial.withBoard(board).withTurn(Color.White)
|
||||
val castle = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
|
||||
|
||||
MoveOrdering.score(context, castle, None) should be(0)
|
||||
Reference in New Issue
Block a user