Geek Smithology

February 27, 2008

Domain Specific Adventures in Real Life

Filed under: Craft of Dev,Ruby by Nathan @ 6:49 pm

There is a lot of buzz about Domain Specific Languages and about ten thousand pages describing what they are, so I’m just going to demonstrate a Domain Specific language that I am personally using. It’s for testing our chess engine, pawnzilla.

Probably the simplest thing one can do in chess is move a piece. Here is a sample unit test from earlier in the project:

1
2
3
4
5
6
7
8
9
def test_should_move_piece
    state = GameState.new
    state.clear
    state.place_piece(Coord.new(0, 0), Chess::Piece.new(Chess::Colour::WHITE, Chess::Piece::PAWN))
    state.move_piece(Coord.new(0, 0), Coord.new(0, 1))
    assert_nil(state.sq_at(Coord.new(0, 0).piece)
    assert_not_nil(state.sq_at(Coord.new(0, 1).piece)
    assert_equal(Chess::Piece.new(Chess::Colour::WHITE, Chess::Piece::PAWN)
end

Right now you’re probably thinking “DAMN, that’s a lot of code, and I can barely read it!” And you’d be right. Here’s what the exact same test looks like after some massive rethinking:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def test_should_move_piece
    state = GameState.new
    state.place_pieces("
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        p - - - - - - -
    "
)
    state.move_piece(A1, A2)
    assert_position(state, "
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        p - - - - - - -
        - - - - - - - -
    "
)
end

Now, dear reader, assuming you know even the tiniest little bit about chess, is there any way you can’t understand this test? This relies on two primitives, each very important in their own way. First, the chessboard itself. Chess players will be familiar with FEN, or Forsythe Edwards Notation, which is used to describe chess positions. Now, FEN is less verbose than our chess board object, but we take the idea of piece representation – white pieces are lower case, black pieces are upper case. This makes it trivial to present any conceivable chess position in the same amount of space, and anybody looking at a test knows exactly what it’s trying to accomplish. The second important thing to note is that in the move_piece method, we’re using simple constants that represent the coordinates of all 64 squares in the corresponding algebraic notation. While (0, 0) (0, 1) is convenient for the engine, it’s very tough for a chess playing human – you end up spending several seconds mentally transposing the cartesian coordinates into algebraic notation. Why not just remove the barriers?

Tests that may have previously been a nightmare to read and figure out become dead simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def test_should_detect_blockable_back_rank_checkmate
    state = GameState.new
    state.place_pieces("
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - - - - - - - -
        - b - - - - - -
        - - - - - - - -
        - - - - - p p p
        R - - - - - k -
    "
)
    assert(!state.checkmate?(BLACK))
end

This idea can extend to other tests as well. For example, let’s define a square under attack using an asterisk(*). Then testing attack vector calculations also becomes an easy visual test:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def test_should_calcluate_queen_attack
    state = GameState.new
    state.place_pieces("
        - - - - - - - -
        - - - - - P - -
        - - - - - - - -
        - - - Q - - - -
        - - - - - - - -
        - - - - - p - -
        - - - - - - - -
        - - - - - - - -
    "
)
    state.calculate_queen_attack(BLACK)
    assert_attack_state(state, "
        * - - * - - - -
        - * - * - P - -
        - - * * * - - -
        * * * Q * * * *
        - - * * * - - -
        - * - * - p - -
        * - - * - - - -
        - - - * - - - -
    "
)
end

Now you can really see the power! Can you imagine trying to make sense of a test that had to check all of that board real estate using coordinates?

This demonstration is the quickest way I know how to define a domain specific language. We take concepts directly from our domain, the game of chess, and create primitives that translate directly to real world concepts. Because of its lovely meta-programming facilities, it’s very easy to make Ruby use our DAL by adding our code to the Unit test module. It is important to note that we are not extending the unit test module, but adding behavior to it, as if our domain concepts are native. It’s quite simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
require "geometry"
module Test::Unit
    class TestCase
        A1 = Coord.new(0, 0)
        # etc.
        H8 = Coord.new(7, 7)

        def assert_position(state, position)
            # code
        end

        # and so on
end

Now all of our tests can call upon an incredibly rich toolset that brings us to a holy grail that I previously thought was unattainable: Code that is actually easier to read than it is to write. That it’s not hard to write either is just icing on the cake. You just create a macro that drops an empty grid into the code, place your pieces, and away you go.

One of the most interesting things about this is that from a technical standpoint, this was a very easy thing to implement – Ron did the translation stuff and converted several test in only a few hours. The important breakthrough here, like when Google unveiled their map website, is NOT that it requires crazy voodoo programming, but that it requires a different way of applying what’s right in front of us.

2 Responses to “Domain Specific Adventures in Real Life”

  1. Ron says:

    It’s been a while since I looked at Pawnzilla. I forgot how cool this test extension was. What is interesting were the amount of bugs that were found when the conversion was made to this model.

    For example, when we were testing a pawns attack from a square, only the immediate squares around it tended to be test. It was too cumbersome to test all 64 squares for the correct state, so it was not done for all the tests. This lead to bugs with squares being attacked when they shouldn’t.

    This change has not only made the tests easier to read, it made them more complete and quicker to write.

  2. [...] was recently browsing the Geek Smithology site and reread one of his older posts about Domain Specific Adventures. Go check it out if you haven’t read it already. It’s a great [...]

Leave a Reply

 

Powered by WordPress