diff --git a/tests/Bitboards.test.scala b/tests/Bitboards.test.scala new file mode 100644 index 0000000..6250ea6 --- /dev/null +++ b/tests/Bitboards.test.scala @@ -0,0 +1,44 @@ +package gambit + +class BitboardsTest extends munit.FunSuite: + + test("file and rank masks cover eight squares each and intersect once") { + for i <- 0 until 8 do + assertEquals(Bitboards.count(Bitboards.fileMask(i)), 8) + assertEquals(Bitboards.count(Bitboards.rankMask(i)), 8) + for f <- 0 until 8; r <- 0 until 8 do + assertEquals(Bitboards.count(Bitboards.fileMask(f) & Bitboards.rankMask(r)), 1) + } + + test("adjacent files exclude the file itself") { + assertEquals(Bitboards.adjacentFiles(0), Bitboards.fileMask(1)) + assertEquals(Bitboards.adjacentFiles(7), Bitboards.fileMask(6)) + assertEquals(Bitboards.adjacentFiles(3), Bitboards.fileMask(2) | Bitboards.fileMask(4)) + for f <- 0 until 8 do + assertEquals(Bitboards.adjacentFiles(f) & Bitboards.fileMask(f), 0L) + } + + test("white passed mask looks forward on three files") { + val e4 = Square.rankOf(Square.make(4, 3)) * 8 + Square.fileOf(Square.make(4, 3)) + val mask = Bitboards.passedWhite(e4) + assert((mask & Bitboards.bit(5 * 8 + 4)) != 0L, "e5 ahead is covered") + assert((mask & Bitboards.bit(5 * 8 + 3)) != 0L, "d5 ahead is covered") + assert((mask & Bitboards.bit(3 * 8 + 4)) == 0L, "same rank is not covered") + assert((mask & Bitboards.bit(4 * 8 + 2)) == 0L, "distant file is not covered") + } + + test("passed masks are color mirror images") { + for s <- 0 until 64 do + val mirror = (7 - Bitboards.rankOf(s)) * 8 + Bitboards.fileOf(s) + assertEquals(Bitboards.count(Bitboards.passedWhite(s)), Bitboards.count(Bitboards.passedBlack(mirror))) + } + + test("knight attack table matches a hand count in the corner and center") { + assertEquals(Bitboards.count(Bitboards.knightAttacks(0)), 2) + assertEquals(Bitboards.count(Bitboards.knightAttacks(27)), 8) + } + + test("king ring is nine squares in the center and four in the corner") { + assertEquals(Bitboards.count(Bitboards.kingRing(27)), 9) + assertEquals(Bitboards.count(Bitboards.kingRing(0)), 4) + } diff --git a/tests/Fen.test.scala b/tests/Fen.test.scala new file mode 100644 index 0000000..f0a304c --- /dev/null +++ b/tests/Fen.test.scala @@ -0,0 +1,19 @@ +package gambit + +class FenTest extends munit.FunSuite: + val cases = List( + Fen.startPosition, + "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", + "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1", + "8/P7/8/8/8/8/8/k6K w - - 5 42" + ) + + test("fen round trip") { + for fen <- cases do assertEquals(Fen.toFen(Fen.parse(fen)), fen) + } + + test("startpos hash matches recompute") { + val pos = Fen.parse(Fen.startPosition) + assertEquals(pos.hash, pos.computeHash()) + } diff --git a/tests/MakeUnmake.test.scala b/tests/MakeUnmake.test.scala new file mode 100644 index 0000000..2908340 --- /dev/null +++ b/tests/MakeUnmake.test.scala @@ -0,0 +1,32 @@ +package gambit + +class MakeUnmakeTest extends munit.FunSuite: + val positions = List( + Fen.startPosition, + "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", + "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" + ) + + private def snapshot(pos: Position): (Vector[Int], Int, Int, Int, Int, Int, Int, Int, Long) = + (pos.board.toVector, pos.sideToMove, pos.castling, pos.epSquare, pos.halfmove, + pos.fullmove, pos.kingSquare(0), pos.kingSquare(1), pos.hash) + + private def walk(pos: Position, depth: Int): Unit = + if depth > 0 then + val moves = new MoveList() + MoveGen.generateLegal(pos, moves) + var i = 0 + while i < moves.size do + val m = moves(i) + val before = snapshot(pos) + val undo = pos.make(m) + assertEquals(pos.hash, pos.computeHash(), s"incremental hash drift after ${Move.uci(m)}") + walk(pos, depth - 1) + pos.unmake(m, undo) + assertEquals(snapshot(pos), before, s"state not restored after ${Move.uci(m)}") + i += 1 + + test("make/unmake restores exact state to depth 3") { + for fen <- positions do walk(Fen.parse(fen), 3) + }