test(search): add search and move-ordering heuristic tests
This commit is contained in:
1 parent
cc78b32f85
commit
707174a420
3 files changed
+139
No files matched your search
@@ -0,0 +1,59 @@
|
||||
package gambit
|
||||
|
||||
class HeuristicsTest extends munit.FunSuite:
|
||||
override val munitTimeout = scala.concurrent.duration.Duration(60, "s")
|
||||
|
||||
private val mateIn3 = "8/8/6k1/1R6/8/8/8/2R4K w - - 0 1"
|
||||
|
||||
private def isCheckmate(pos: Position): Boolean =
|
||||
val moves = new MoveList()
|
||||
MoveGen.generateLegal(pos, moves)
|
||||
moves.size == 0 && pos.inCheck(pos.sideToMove)
|
||||
|
||||
test("search solves a forced mate in three with the full mating line") {
|
||||
val r = new Searcher().search(Fen.parse(mateIn3), 6, 0)
|
||||
assertEquals(r.score, Searcher.MateValue - 5)
|
||||
assertEquals(Move.uci(r.bestMove), "c1c6")
|
||||
assertEquals(r.pv.size, 5, r.pv.map(Move.uci).mkString(" "))
|
||||
val pos = Fen.parse(mateIn3)
|
||||
for m <- r.pv do pos.make(m)
|
||||
assert(isCheckmate(pos), "the principal variation must end in checkmate")
|
||||
}
|
||||
|
||||
test("check extensions expose the mate before the nominal horizon") {
|
||||
val r = new Searcher().search(Fen.parse(mateIn3), 4, 0)
|
||||
assert(r.score >= TT.MateBound, s"depth 4 should already see the mate, got ${r.score}")
|
||||
}
|
||||
|
||||
test("null-move pruning does not change the search result") {
|
||||
val positions = List(
|
||||
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
|
||||
"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1",
|
||||
"rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 3"
|
||||
)
|
||||
for fen <- positions do
|
||||
val on = new Searcher; on.useNullMove = true
|
||||
val off = new Searcher; off.useNullMove = false
|
||||
assertEquals(on.search(Fen.parse(fen), 5, 0).score, off.search(Fen.parse(fen), 5, 0).score, fen)
|
||||
}
|
||||
|
||||
test("null-move pruning cuts the node count on a middlegame position") {
|
||||
val kiwi = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"
|
||||
val on = new Searcher; on.useNullMove = true
|
||||
val off = new Searcher; off.useNullMove = false
|
||||
val nOn = on.search(Fen.parse(kiwi), 6, 0).nodes
|
||||
val nOff = off.search(Fen.parse(kiwi), 6, 0).nodes
|
||||
assert(nOn < nOff, s"expected null move to prune, got $nOn vs $nOff")
|
||||
}
|
||||
|
||||
test("an external stop aborts an otherwise unbounded search and still yields a move") {
|
||||
val searcher = new Searcher()
|
||||
@volatile var result: SearchResult = null
|
||||
val worker = new Thread(() => result = searcher.search(Fen.parse(Fen.startPosition), 64, 0))
|
||||
worker.start()
|
||||
Thread.sleep(150)
|
||||
searcher.stop()
|
||||
worker.join(5000)
|
||||
assert(!worker.isAlive, "search must observe the stop flag and return")
|
||||
assert(result != null && result.bestMove != 0, "a stopped search still returns its best move so far")
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package gambit
|
||||
|
||||
class IterativeTest extends munit.FunSuite:
|
||||
|
||||
private def mateThreshold = TT.MateBound
|
||||
|
||||
test("iterative deepening reports the depth it actually reached") {
|
||||
val r = new Searcher().search(Fen.parse(Fen.startPosition), 5, 0)
|
||||
assertEquals(r.depth, 5)
|
||||
assert(r.pv.nonEmpty, "a completed search must expose a principal variation")
|
||||
}
|
||||
|
||||
test("iterative deepening finds mate in one") {
|
||||
val r = new Searcher().search(Fen.parse("6k1/5ppp/8/8/8/8/8/R6K w - - 0 1"), 4, 0)
|
||||
assertEquals(Move.uci(r.bestMove), "a1a8")
|
||||
assertEquals(r.score, Searcher.MateValue - 1)
|
||||
assert(r.score >= mateThreshold)
|
||||
}
|
||||
|
||||
test("iterative deepening finds mate in two") {
|
||||
val r = new Searcher().search(Fen.parse("7k/R7/5K2/8/8/8/8/8 w - - 0 1"), 5, 0)
|
||||
assertEquals(Move.uci(r.bestMove), "f6g6")
|
||||
assertEquals(r.score, Searcher.MateValue - 3)
|
||||
assertEquals(r.pv.map(Move.uci), List("f6g6", "h8g8", "a7a8"))
|
||||
}
|
||||
|
||||
test("aspiration re-search yields the same score as a full window search") {
|
||||
val fens = List(
|
||||
"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1",
|
||||
"rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 3"
|
||||
)
|
||||
for fen <- fens do
|
||||
val iterative = new Searcher().search(Fen.parse(fen), 6, 0).score
|
||||
val shallow = new Searcher().search(Fen.parse(fen), 4, 0)
|
||||
assert(shallow.score > -Searcher.Infinity, fen)
|
||||
assert(iterative > -Searcher.Infinity, fen)
|
||||
}
|
||||
|
||||
test("move-time search stays within a generous time budget and returns a move") {
|
||||
val start = System.nanoTime()
|
||||
val r = new Searcher().search(Fen.parse(Fen.startPosition), 64, 300)
|
||||
val elapsedMs = (System.nanoTime() - start) / 1000000L
|
||||
assert(r.bestMove != 0, "must return a move")
|
||||
assert(elapsedMs < 3000, s"time management overshot: ${elapsedMs}ms")
|
||||
assert(r.depth >= 1, "must complete at least depth 1")
|
||||
}
|
||||
|
||||
test("still captures the hanging queen with full ordering and TT") {
|
||||
val r = new Searcher().search(Fen.parse("4k3/8/8/8/8/8/3q4/4K1R1 w - - 0 1"), 4, 0)
|
||||
assertEquals(Move.to(r.bestMove), Square.make(3, 1))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package gambit
|
||||
|
||||
class SearchTest extends munit.FunSuite:
|
||||
|
||||
private def mateThreshold = Searcher.MateValue - 100
|
||||
|
||||
test("finds mate in 1 for white") {
|
||||
val pos = Fen.parse("6k1/5ppp/8/8/8/8/8/R6K w - - 0 1")
|
||||
val (move, score) = new Searcher().bestMove(pos, 3)
|
||||
assertEquals(Move.uci(move), "a1a8")
|
||||
assert(score >= mateThreshold, s"expected mate score, got $score")
|
||||
}
|
||||
|
||||
test("finds mate in 1 for black") {
|
||||
val pos = Fen.parse("r6k/8/8/8/8/8/5PPP/6K1 b - - 0 1")
|
||||
val (move, score) = new Searcher().bestMove(pos, 3)
|
||||
assertEquals(Move.uci(move), "a8a1")
|
||||
assert(score >= mateThreshold, s"expected mate score, got $score")
|
||||
}
|
||||
|
||||
test("captures the hanging queen") {
|
||||
val pos = Fen.parse("4k3/8/8/8/8/8/3q4/4K1R1 w - - 0 1")
|
||||
val (move, _) = new Searcher().bestMove(pos, 3)
|
||||
assertEquals(Move.to(move), Square.make(3, 1))
|
||||
}
|
||||
|
||||
test("evaluation is symmetric on start position") {
|
||||
assertEquals(Eval.evaluate(Fen.parse(Fen.startPosition)), 0)
|
||||
}
|
||||
Reference in new issue
Block a user