Coding Chess with TDD
Luke Garrigan
Posted on February 13, 2021
TDD is a pretty simple idea, you write your tests before you write any code and you write just enough code to make that failing test pass.
There are three Laws of TDD
You must write a failing test before you write any production code
You must not write more of a test than is sufficient to fail or fail to compile.
You must not write more production code than is sufficient to make the currently failing test pass.
Recently, I picked up the book Clean Coder by Uncle Bob – among many brilliant points made in the book I was immediately intrigued by the enthusiasm placed on TDD.
Using TDD
Lately I've been playing quite a lot of chess, however, I'm pretty shite. I thought, what better way to learn the game than to code it? And whilst I'm at it I'll give that TDD lark a good stab.
All the code I wrote is open-source and can be found on my GitHub.
The framework I'm using for writing tests is Jest and the canvas library is p5.js.
Creating the tiles
So, what do I need? I need a board which has tiles and there needs to be 8 tiles per row, let's create a failing test.
Note: the cycle time was a bit smaller than I'm showing in these examples, I would write just enough code to produce a failing test and then write just enough production code to make that test pass — so in the example below I'd have created the board class straight after writing new Board()
.
Now we've got ourselves a failing test let's write the code to get that test to pass.
Brilliant, the test is now passing and we've got ourselves a two dimensional array that represents the chess board!
Displaying the board
I should note that I didn't write any tests for actually rendering the board as p5.js does that heavy lifting for me — which also explains why the coverage is not quite 100%.
Creating Pieces
The next logical step was to get some pieces on the board. Let's start with pawns.
Firstly let's start with writing a failing test to check that the Pawn exists on the board at the start of the game:
Let's now write just enough code to get this test to pass.
Brilliant, I then repeated the process for the white pawn.
And we have ourselves some pawns on the board!
The next logical step is to find possible moves for the pawn, but before we do that I need some way to distinguish between the black and the white pieces. So let's right a test to ensure that the pawns at the bottom are white and the pawns at the top are black.
So in this test I've introduced a new constant for the colour of the pieces. Next I need to write just enough code to make this pass, so the simplest path here is to add the new colour property to the Pawn
class, and doing that will make the test pass. Now that I've got this test in place I can refactor, I know that every piece is going to require a colour, so it would make sense — rather than repeating this code in each chess piece (Bishop, King, Queen, Rook, Knight) — to create a base class called Piece
that deals with this.
And I simply know this works by just re-running my test suite, TDD gives you the power to refactor with confidence!
Finding possible moves
So, in chess what can a Pawn do?
- It can move forward 1 square
- Move diagonally capturing an enemy piece
- Move two squares if it's the first move
And a couple moves I'll ignore for now:
- Promotion - when you reach the end of the board
- Can perform an En passant which is a move you make out of principle to show your opponent that, yes, I know what En passant is.
Let's write our first test to check when a Pawn has just one move:
So I've added a couple things here, a new flag which denotes whether a Pawn has moved or not and a new method on Pawn
class which should find the legal moves for us, let's write the production code to make this test pass:
So here we're just checking to see if a piece exists in front of the pawn and if it does that means we can't move there which also means we can't move two spaces ahead if it were our first move!
One might think I was a little naughty here as I'd written too much production code just to get the test passing, and you'd be right. This code is enough to get the following tests to pass too.
This is one of the key lessons I've learnt from practicing TDD, don't get ahead of yourself — write just enough code to get the test to pass and nothing more.
A good image and explanation from codecademy.com in their blog Red, Green, Refactor
- Red — think about what you want to develop
- Green — think about how to make your tests pass
- Refactor — think about how to improve your existing implementation
If you get ahead of yourself like I did then you miss the "Refactor" step. Yes, you can still refactor after you've written all the production code, but refactoring just 3 lines rather than 30 is surely a simpler operation, TDD enforces this.
Now that we've covered a pawn moving forward and a pawn moving two squares on its intitial move, let's add a test to cover attacking.
Let's write the production code to return the diagonal left attacking move:
Brilliant, this test is passing but what happens if our pawn is on the very left of the board? I'm pretty sure the code would error because it'll try to get the value from tiles[-1][y]
, let's write a test to check this:
Just as I expected:
TypeError: Cannot read property '5' of undefined
Let's remedy this by adding a check to see if the current Pawn is at the end of the board:
Brilliant, now our test passes! I then repeat the previous steps for diagonal right, you can imagine what that looked like.
Now we have pawns that can move, I added a little visual code so that when you select a pawn it displays the possible moves.
Rinse and repeat
I then repeated the steps that I took for the Pawn in finding its possible moves for the rook:
And then the Bishops:
And the Knights, Kings and Queens:
And prettied them up a little, who knew Unicode had chess pieces? https://www.wikiwand.com/en/Chess_symbols_in_Unicode
Finally
I continued the process of writing the tests before writing any code and by the end of it I've got a functioning Chess game, yes there are definitely some minor things I've likely missed, but this was just merely an exercise to put TDD to practice. The one takeaway I've learnt from this — and isn't mentioned enough — is that TDD is fun, and I mean a lot of fun. There's nothing more gratifying than seeing your broken test flick to green. The instant release of endorphins makes TDD is almost addictive. Having a reliable suite of tests that runs in under a second gives you certainty when refactoring or adding new code, it's a massive safety net. And because you've written your tests before any production code you can be confident the holes are minimal, and if any, most certainly would have been there if you weren't using TDD.
I hope this blog inspires you to give TDD a go, I know I'm going to be using it by default in the future, as I said, It's bloody fun.
Thank you, if you like my rambling check out my personal blogging site at https://codeheir.com/
Posted on February 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.