Testing Strategy & DSL
In a chess engine, the core logic (move generation, board representation) is performance-critical and heavily bitwise. Testing these components using raw hexadecimal masks or bit shifts is error-prone and difficult for humans to verify.
To solve this, we use a Test Domain-Specific Language (DSL) that allows us to write tests in plain chess notation or even visual ASCII boards.
The Test DSL
Section titled “The Test DSL”Our testing utilities live in src/test/scala/dicechess/engine/testutils/TestBoard.scala. They provide three primary ways to interact with bitboards.
1. Visual ASCII Boards
Section titled “1. Visual ASCII Boards”For complex scenarios like Magic Bitboards (where blockers determine the range of sliding pieces), we can define the board state visually.
import dicechess.engine.testutils.TestBoard
val pos = TestBoard.fromAscii(""" - - - - - - - - - - - - - - - - - - - - P - - - - - - - - - - - - - P - R - P - - - - - - - - - - - - - P - - - - - - - - - - -""")
val occupancy = pos.occupied // Bitboard containing all 'P' and 'R' positionsThe parser uses FEN-standard characters (P, R, k, etc.) and ignores whitespace, dots, and dashes.
2. Algebraic Varargs
Section titled “2. Algebraic Varargs”When you just need to place a few pieces or verify specific targets, you can use the varargs constructor:
// Creates a bitboard with bits set at d3 and f5val occupancy = TestBoard("d3", "f5")3. Extension Methods
Section titled “3. Extension Methods”For maximum brevity, we provide extension methods on standard String literals:
import dicechess.engine.testutils.TestBoard.*
val sq = "e4".sq // Returns a Square objectval bb = "e4".bb // Returns a Bitboard with e4 setExample: Testing a Rook Attack
Section titled “Example: Testing a Rook Attack”Here is how a real test looks using the DSL:
test("Rook attacks with blockers") { val sq = "e4".sq
val pos = TestBoard.fromAscii(""" - - - - - - - - - - - - - - - - - - - - P - - - - - - - - - - - - - P - R - P - - - - - - - - - - - - - P - - - - - - - - - - - """)
val attacks = MagicBitboards.rookAttacks(sq, pos.occupied)
val expected = TestBoard( "e5", "e6", // North "e3", "e2", // South "d4", "c4", // West "f4", "g4" // East )
assertEquals(attacks, expected)}Why this matters
Section titled “Why this matters”- Self-Documenting: The tests show exactly what is being tested without requiring comments.
- Fast Debugging: If a test fails, the diff between two
Bitboardobjects is rendered by MUnit, and you can easily cross-reference it with the visual board in the test code. - Correctness: It is much harder to make a mistake when typing
"e4"than when typing1L << 28.