Sunday, February 05, 2006

Distilling Intent from Descriptions

This is a follow up to my previous posts on the use of intents to describe the game of maru batsu (well, maru-batsu sounds much cooler than tic-tac-toe :-)

Every now and again I practice this small maru-batsu kata to see how my perception of things changes in time as I learn new ways of approching the encoding of intent.

Something that I notice by reading my early posts is that, while the code is not bad, I tend to get too technical too early.  I know I shouldn't abstract too early.  I know I shouldn't create abstractions unless my intents prove that I need them (this process of spawning abstractions is described in Shape Code After Language). 

Yet I always fall into the sin of over-abstraction.  It's not a big deal; sometimes it's just a matter of abstracting a Player class, because I feel it's a natural element of the maru-batsu discourse.  They taught us to make classes out of concepts, just in case we might need them.

More I practice this kata, more I realise that I can do with less and less.  It feels pleasant to feel how much lighter the intents (and the code) become.  The Player doesn't need to be a class.  Most of the times it can simply be a symbol, and the way it is used can be made implicit by the context and illustrated by intents and stories.

Today I will practice the kata one more time, in an iterative way.  Be patient, the code doesn't need to run as-it-is, as it is just a first exploratory attempt at designing intents.

This is a first rough natural language description of maru batsu:
Then I get each statement in turn and I try to make it a bit more formal.  I will also put some comments of problems that I noticed only after the fact.

"maru batsu is played by two players on a 3x3 board"
    expect marubatsu.players.size == 2
    expect marubatsu.board.width == marubatsu.board.height == 3

Why players.size when I could have n_of_players ?  Why did I feel the need to make explicit the inner structure of marubatsu?  This just creates just more coupling without any immediate additional benefit.  An ethnographer would probably point out that I am giving implicit importance to the concept of Player.  I am probably reifying  players because they belong to my everyday universe of discourse.  The same line of reasoning applies to board.width.  These declarations of structure are actively implying a universe of games made out of boards and players.  While this might be a useful abstraction, it is an abstraction that I did not produce consciously.  It just seemed to be the 'natural' abstraction.  I erroneously perceived it as being 'the-way-the-real-world-is'.

"a player uses X marks, while the other uses O marks"
    expect (marubatsu.symbol_of :player_A) == :X
    expect (marubatsu.symbol_of :player_B) == :O

Here I resisted using strings to represent X and O.  They are symbols, they stand for things, for marks on the board.  They represent universal concepts.  They relate to the fly-weight structural pattern.

Yet I used 'symbol_of :player_A' rather than ' symbol_of_player_A'.  Again, why did I feel the need to abstract this, as if I could want to play maru batsu with a variable number of players?  It's so hard to forget your symbolic perceptual stance and see things as they are.  I wonder if reading again Drawing on the Right Side of the Brain could help me to get back to a more immediate reality.

"the players take turns"
    marubatsu.turn?.should_be :player_A
    marubatsu.turn?.should_be :player_B
    marubatsu.turn?.should_be :player_A

Again I implicitly assume a multitude of players.  Why not 'is_turn_of_player_A?'

"a player, in his turn, can put his mark on the empty squares of the board"
    marubatsu.turn?.should_be :player_A
    dont_expect marubatsu.board.has_mark_at? row=2, col=1
    marubatsu.board.mark! row=2, col=1
    expect (marubatsu.board.mark_at? row=2, col=1) == :X

Besides the over-complex designs that I have already pointed out, I have managed to do a couple of nice moves here.  I use the ' row=2' parameter passing notation to specify the meaning of the number passed.  I also changed an initial 'marubatsu.board.mark_at?(row=2, col=1) ' to '(marubatsu.board.mark_at? row=2, col=1)'.  There is no semantic difference between the two notations, but I prefer the second one as I like to visualize what is inside the brackets as a single entity on which I do an operation.  In the former case the brackets were used to delimit a function parameters.  I tend to prefer to think in terms of entities than in terms of flows of data between functions.  This doesn't mean I don't like the functional style of programming.. mmh this is an ambiguous point I'll have to expand on in a future post on Stanzas and Dyamic Aggregates.

Take note of the fact that the sentence "a player, in his turn, can put his mark on the empty squares of the board" also implies that "a player, in his turn, can NOT put his mark on the NON empty squares of the board". I am going to state this intent as well.

"a player, in his turn, can NOT put his mark on the NON empty squares of the board" 
    marubatsu.turn?.should_be :player_A
    expect marubatsu.board.has_mark_at? row=1, col=1
    expect_exception( :SquareUsedException ){
        marubatsu.board.mark! row=1, col=1

To show that this intent holds, we state that we expect an exception in case we try to tick twice the same square of the maru batsu board.  From a technical point of view, it's nice to be able to state explicitly what kind of exception I am expecting here.  However, from a legibility point of view, something different might have been better.  Something like "cannot do marubatsu.board.mark! ..., because :SquareUsed" might work even better!

"a player that aligns three of his symbols (Horizontally, vertically or diagonally) on the board wins."
    marubatsu.set_board [:X, :O,:O],
                        [:X, :-,:-],
                        [:X, :-,:-]
    expect marubatsu.winner?
    marubatsu.winner.should_be :player_A
    marubatsu.set_board [:O, :O,:O],
                        [:X, :-,:-],
                        [:X, :-,:-]
    expect marubatsu.winner?
    marubatsu.winner.should_be :player_B

    marubatsu.set_board [:X, :O,:O],
                        [:-, :X,:-],
                        [:-, :-,:X]
    expect marubatsu.winner?
    marubatsu.winner.should_be :player_A

It might have been better to split this intent into three different intents, each one specifying one way to win (horizontal, vertical, diagonal).  Since I didn't want to have to play out a whole game to check winning conditions, I introduced a set_board verb that allows the injection of a board into the game.  I wonder if this is really a good thing.  From a TDD point of view it is a good thing since it introduces flexibility into the design, but this is not flexibility that is needed to describe the game play, so I am not so sure about it.  I am not sure about a valid alternative either, so I'll keep it for the time being.

Another problem is that the examples are not exhaustive at all.  I do not show the whole range of winning combinations, and the combinations that I show are not shown for all players.  There should be 8 (3+3+2) winning combinations for each player, for a total of 16 combinations.  In this simple game I could even enumerate all of them, but what about more complex games?  How would you test moves in a game of chess?

I like the way I set up the board using arrays, and how I resisted making up Board and Cell objects.  I don't like the fact that I tried to stay abstract using symbols and using :- to signify a blank cell.  It would have been much clearer to use strings.  Something like:

    marubatsu.set_board "XOO",

or even something like

    marubatsu.set_board <<-board
which would then have the burden to decode the multiline board string.  While this may be too heavy for this simple case, this approach could also be used to introduce small DLS-like dialects inlined within the code.

As in a previous intent "a player that aligns three of his symbols (Horizontally, vertically or diagonally) on the board wins." also implies the following converse intent:

"there is no winner if three equal symbols are not aligned (Horizontally, vertically or diagonally) on the board"
    marubatsu.set_board [:-, :-,:-],
                        [:-, :-,:-],
                        [:X, :-,:-]

    dont_expect marubatsu.winner?

    marubatsu.set_board [:-, :X,:-],
                        [:-, :O,:-],
                        [:X, :O,:X]

    dont_expect marubatsu.winner?

but how can you 'prove' that there is no winner? how can you prove that there is a winner without enumerating all cases?  Do I need to prove things or do I simply need to enumerate some examples useful to elaborate working code?

"if nobody has won and no new moves are possible, then it's a draw."
    marubatsu.set_board [:X, :O,:O],
                        [:O, :X,:X],
                        [:X, :X,:O]

    dont_expect marubatsu.winner?
    expect marubatsu.draw?

again, should I enumerate all possible draws?

should I also state all the following?

"if someone has won then it's not a draw."
    marubatsu.set_board [:O, :O, :O],
                        [:X, :-, :-],
                        [:X, :-, :-]
    expect marubatsu.winner?
    dont_expect marubatsu.draw?

"if nobody has won but new moves are possible, then it's not a draw."
    marubatsu.set_board [:X, :O, :O],
                        [:O, :X, :X],
                        [:X, :-, :O]

    dont_expect marubatsu.winner?
    dont_expect marubatsu.draw?

Comments: Post a Comment

Links to this post:

Create a Link

<< Home

This page is powered by Blogger. Isn't yours?