Yesterday was about the interface. Today is about the implementation.
Look, a game!
$ bin/crypt hanoi | | | = | | === | | ===== | | ======= | | ========= | | ------------------------------------- > okay... Sorry, the game doesn't recognize that command. :/ 'help' if you're confused as well. > help Goal: get all the disks to the right rod. You can never place a larger disk on a smaller one. Available commands: add <disk> <target> move <source> <target> remove <disk> q[uit] h[elp] s[how] Disks: tiny disk, small disk, medium disk, large disk, huge disk Rods: left, middle, right > show | | | = | | === | | ===== | | ======= | | ========= | | ------------------------------------- > move left right | | | | | | === | | ===== | | ======= | | ========= | = ------------------------------------- > move middle left Cannot move from the middle rod because there is no disk there. > move left middle | | | | | | | | | ===== | | ======= | | ========= === = ------------------------------------- > move small disk right Cannot put the small disk on the tiny disk. > quit
Now, let's implement this.
- A legal move. Things are introduced as we go.
- Can't put a larger disk on a smaller one. We introduce a bit of infrastructure for handling exceptions.
- More illegal things: you can't move from or to nonexistent rods or from a rod with no disks. We create up the exception classes as we go.
- Winning the game. There's actually a little hanoi solver in the test suite, because it made more sense to write out the moves than to generate them. You can't win twice in a row.
- However, you can un-win by moving both the tiny and the small disk.
- If you want, you can say which disk to move instead of which rod to move from. Basically a case of Postel's law: we recognize this and do the right thing with it.
- But of course being accomodating creates problems, too. With the new syntax, it's possible to try to remove a covered disk.
Let's just pause here for a while and say a bit about this method of working.
The tests exercise the Hanoi game. They do this by saying either "calling this
method returns this event" (success) or "calling this method throws this
exception" (failure). The Hanoi game keeps state around, but the attributes are
completely private. There are no getter methods. The public methods (
.add) may read attributes, but the attributes are only
written to in a private
!apply method. (The
! means "private".) The
!apply method is called with an event.
So the only way to actually change the game state is to send an event of some
kind to the
!apply method. This keeps us honest in a way; we have to
publicize all the important internal updates of the Hanoi game as events.
- You can remove a disk.
- Removing disks that are not the tiny disk is forbidden. In the adventure game, the larger disks are too heavy to be lifted away from the Hanoi rods. In the Hanoi game, weight doesn't really exist, so we only say it can't be done.
- If you try to remove a covered disk, you get another exception. If you uncover it and try to remove it and it's still not the tiny disk, it's still forbidden. Tough.
- You can't remove a disk that you already removed. Implementing this is really about thinking about all the possible actions in all the possible situations.
- You can't move a removed disk either. Introducing new commands can create new exceptional conditions in old ones.
Now this last point is interesting. We introduce
.remove, and suddenly we
have to go back to
.move and patch a "vulnerability" that suddenly opened up.
I think many bugs are of this kind; old operations which end up in new
situations for which they were not designed.
- You can add a disk that you removed before. We're always talking about the tiny disk in these cases, but it doesn't matter much.
- You can't add a disk with a made-up name or that's already there.
- At this point I realized that some of my events attributes could be improved. The design was presented as a completed fact yesterday, but it very much evolved together with the test and the implementation. It's comforting to see that that works really well.
- You can't add to a rod that doesn't exist, either.
As soon as I implemented the client that you see above, I got feedback from people. There's a lesson in there somewhere. I got many good bug reports and could put in things that I'd forgotten.
- Removing a non-existent disk gave the wrong exception back. Coke++ discovered this, and I fixed it.
- The client didn't tell you when you won or un-won. It does now. Coke++.
- But the best one, and the one that I consider a real omission and mental error of mine, also discovered by Coke++: you could only win by moving a disk, not by adding a disk. Ouch. I fixed.
See how easy it is to miss some cases? One could argue that this is a great weakness of this way of developing, but I would argue the opposite: this way of focusing on verbs and their results enhances the ability to think in terms of these situations. Also note how small and self-contained each commit is, even the fixes.
I'm really proud how the client turned out. Have a look at the 'final' client. I especially call your attention to the fact that we don't just list the available commands, we ask the Hanoi game for them. (Yay introspection!) Also, the
print_board subroutine turned out really nice, for something that does formatted output. There's a little "cheating" that makes the command parser treat things like 'large disk' as one argument even though it's two words.
75 tests pass and support this hanoi game. I can't guarantee there are no remaining bugs to find, but I'm very confident it'll be easy and even a bit fun to integrate what we now have into the adventure game when the time comes.
Also, I imagine the subsequent posts will be a bit smaller in scope than these two first ones. Still, this was probably a good introduction to the style of programming we'll be using for the rest of the month. Commands, events, and exceptions. We'll see more advantages as we go along.
Now we can leave Hanoi behind us for a while, and... let the adventure begin!