June 01, 2002
Connecting the DotsFrom shoot-'em-ups to number guessing, a father-son exercise in elementary gameJeff Langr
Last year, my 11-year-old son Tim expressed an interest in learning how to program. With visions of StarCraft in his head, he wanted to tackle building a game. I suspect that the compromises we madefirst trying the game of dots and finally settling on a number-guessing gamewere a disappointment to him. They were to me, too, but I've learned to become just a bit realistic over the years.
Last year, my 11-year-old son Tim expressed an interest in learning how to program. With visions of StarCraft in his head, he wanted to tackle building a game. I suspect that the compromises we madefirst trying the game of dots and finally settling on a number-guessing gamewere a disappointment to him. They were to me, too, but I've learned to become just a bit realistic over the years.
We sat down and briefly discussed programming, talking about computers, programs, classes, objects and messages. Tim took all this in and asked some good questions. We decided that the game of dots would be simpler than a full-blown shoot-'em-up adventure game. So far, so good. We then talked a bit more, about languages and compilers. Tim knew that there was a language called Java, and that Dad knew something about it. I talked a bit about other possible choices, including Smalltalk and Ruby. No diceTim wanted to jump on the bandwagon, so we settled on Java. That was mistake number two.
Our first mistake was choosing the game of dots.
Game Glitches On the surface, this is a reasonably simple game. From a programming perspective, it requires some knowledge of drawing and a bit of math to figure out where someone clicked within a grid. But I didn't realize that 11-year-olds haven't worked with coordinate spaces, and thus the math quickly overwhelmed Tim. The next roadblock was Java. Yes, Java is a simple language to learnif you already know another C-based language. I did, when I learned Java. But poor Tim's school system had already failed him by neglecting to ensure that he had completed computer language basics before moving on to the fifth grade. Java also has many words that are too "adult" and abstract for an 11-year-old to grasp, such as interface, void and static. Tim was getting frustrated.
Trying a New Target However, I was still interested in the game of dots, since I hadn't built any GUI code using test-first design. How would my code evolve were I to build it from scratch with TfD?
Where to Start?
I suspected that hit-testing would be the most complex part of the application, so I wanted to ensure that I had tests for it. It also didn't belong in the model, since that should contain only basic game concepts. I needed an intermediary class somewhere: The Swing code would get a mouse event and delegate it to this intermediary class. The intermediary would interpret the mouse coordinates and determine if something had to happen. It would possibly do this by interacting with the model. If anything did happen, an event would be broadcast back to the Swing class.
I came up with a set of initial names for the classes: Board for the underlying model, BoardPanel for the JPanel that would represent the visual interface, and (for lack of a better name) BoardController for the intermediary class. Ultimately, the name for this class became BoardPresenter (more on this later). I also later renamed the Board class to Game.
With the initial design sketch in place, I wanted to minimize the Swing code as much as possible; first, because I'm not greatly enamored of SwingI find it to be fairly painful at times. Second, I wanted to get the code so small and stable that it would never have to change, and it couldn't possibly break.
Drawing Dots I wanted the UI to send a single message to the presenter to tell it to initialize the board. The presenter would contain the physical characteristics of the game (space between dots, offset from the left-hand side and so on). It would use this information to iterate through the grid size of the game and calculate dot locations. For each dot to be drawn, the presenter would send a message to an interested listener to draw a dot at a specific coordinate. From this proposed solution, I incrementally built a test to specify it. The code below shows a portion of the test written for drawing dots. (I've abbreviated some of my descriptive variable names to accommodate the narrow column sizes.)
final Set gotDots = new HashSet();
BoardPresenterDrawListener listener =
new BoardPresenterDrawAdapter() {
public void drawDot(int x, int y, int size) {
gotDots.add(new Dot(x, y, size));
}};
BoardPresenter presenter =
new BoardPresenter(new Game(3), listener);
Set needDots = new HashSet();
int size = presenter.getDotSize();
int halfSize = size / 2;
int numDots = gameSize + 1;
for (int i = 0, x = _leftX; i < numDots; i++, x += _side)
for (int j = 0, y = _topY; j < numDots; j++, y += _side)
needDots.add(new Dot(x - halfSize, y - halfSize, size));
presenter.initializeBoard();
assertSetEquals(needDots, gotDots);
Stepping through this code, line by line:
public void initializeBoard() {
int dotSize = getDotSize();
int halfDot = dotSize / 2;
for (int i = 0; i < _game.getSize() + 1; i++)
for (int j = 0; j < _game.getSize() + 1; j++) {
int dotX = (getLeftOffset() + i * getSideLength()) -
halfDot;
int dotY = (getTopOffset() + j * getSideLength()) -
halfDot;
listener.drawDot(dotX, dotY, dotSize);
}
}
The toughest part, figuring out how the objects were going to talk to each other, was behind me now. I figured that the line drawing would work much the same: The presenter would process a click, and, if a line were drawn, send a message with start and end points to the listener. The second most difficult part was figuring out what to do with a click. I tried to sit down and write testClick, but realized that it was too big a method to tackle. The x and y coordinates of a click could represent one of many things: The click was exactly on a line, the click was out-of-bounds, the click was close to a line and so on. I started with the simplest case: The click was out-of-bounds, and nothing should happen.
public void testClickOutOfBounds() {
int x = _leftX - 10;
int y = _topY;
assertTrue(_presenter.isOutOfBounds(getEvent(x, y)));
y += (_side * _presenter.getBoard().getSize()) + _tolerance;
assertTrue(_presenter.isOutOfBounds(getEvent(x, y)));
}
The idea of these click tests is straightforward: First specify x and y coordinates for a click to emulate. Then directly construct a MouseEvent with these coordinates (using my utility method getEvent). testClickOutOfBounds sends the MouseEvent along with the message isOutOfBounds, which returns a boolean.
Once I built the first assertion in this test, I coded the simplest thing possible in BoardPresenter to get it to work: Have isOutOfBounds always return true. I then added another set of x and y coordinates and a second assertion to the test. I enhanced the logic to get this test to pass.
After testClickOutOfBounds, I went on to more tests:
testClickInBounds testClickOnDotCornerIsAmbiguous testClickOffCornerIsNotAmbiguous testClickOffBothLinesIsTooFarOff testClickOnOneLineIsNotTooFarOff testXClickIsOnLine testXClickIsOffLine testYClickIsOnLine testYClickIsOffLineI then wrote testValidClick to ensure that the method isValid returned true only if the click was not out of bounds, not ambiguous and not too far off a line. Now I was able to go back and flesh out testClick.
Ultimately, testClick worked like the dots test: Create a mock listener, emulate a mouse click and expect that the mock listener receives all the drawLine messages expected. For a click on what should be a line, the presenter code would have to send a drawLine message to interested parties.
You'll note that there's a distinction between x and y clicks. It turned out that the easiest way to build incrementallyand not wait too long between successful test runswas to break the logic down so that I tested the x coordinate separate from the y.
final List lines = new ArrayList();
BoardPresenterDrawListener listener =
new BoardPresenterDrawAdapter() {
public void drawLine(int x0, int y0, int x1, int y1) {
lines.add(new Line(x0, y0, x1, y1));
}};
BoardPresenter _presenter =
new BoardPresenter(new Game(3), listener);
_presenter.processClick(getEvent(getArbitraryPointOnUlNorth()));
assertEquals(1, lines.size());
assertEquals(getUlNorth(), lines.get(0));
_presenter.processClick(getEvent(getArbitraryPointOnUlWest()));
assertEquals(2, lines.size());
assertEquals(getUlWest(), lines.get(1));
The abbreviation Ul in getArbitraryPointOnUlNorth() stands for upper leftthe left-most and top-most cell in any dots grid. The method itself returns an arbitrary point on the north line of the upper-left cell. I created a handful of utility methods to represent common points in testing. The data structure I ultimately used to store game information was based on the concept of a two-dimensional array of cells, each having a north, east, south and west line.
The code in the GUI was twice as complex: I had to ensure that a repaint occurred each time a line was drawn.
public void drawLine(int xFrom, int yFrom, int xTo, int yTo) {
_offscreenGraphics.drawLine(xFrom, yFrom, xTo, yTo);
repaint();
}
Drawing InitialsFinally, I built code to ensure that initials were being sent properly when a box was closed. I used the same concept as drawLine and drawDot, except that now I would have to emulate a series of mouse clicks before a drawInitials message was sent.
The code now required interaction with the Game class. As a line was clicked, I had to track that somehow in the Game object. I diverted my attention to GameTest, building a testBoxClose method, such that the presenter was a listener on the Game, and was notified when the box closed; the listener on the presenter was subsequently notified to draw the appropriate initials.
The GUI code ended up being two lines of code, instead of one.
public void drawInitials(Point center, String initials) {
Point startOn = _presenter.getStartForString(
center, initials, _offscreenGraphics);
_offscreenGraphics.drawString(initials,
startOn.x, startOn.y);
}
Why two lines? Because it was easiest to send the centerpoint of the initials along with the
The problem? In order to determine the starting point of a string, it must be rendered in a graphics context. In order to render it, the string actually has to be drawn on-screenat least that's what I discovered, based on my knowledge of Swing and experimentation. This means that the test must construct a frame and panel, create an image, set the font, get the font metrics and so on. Not only are these manipulations a bear to manage in tests, they result in the test actually flashing windows on-screen.
I resigned myself to constructing the frame in order to test
I'm not sure I like this trade-off, but it's what I ended up with. I'll fix it the next time I have to touch that portion of the code.
Evolving Into Patterns
I did a search on Google, and discovered that I had developed a pattern known as Model-View-Presenter (MVP), a variant of Model-View-Controller (MVC). MVP was mined at IBM and is used heavily in the architecture of Dolphin Smalltalk (www.object-arts.com/DolphinSmalltalk.htm)
In MVP, the View actually serves as both the view and controller (presenting output and managing input). The Model is the Model, as in MVC. The extra middle layer is considered a bridge between the View and the Model. It's specific to the application and is often considered throwaway if the View needs to change (for example) from Swing to HTML. In this situation, the Model would remain unchanged.
Once I realized that I had unwittingly implemented MVP, I had the appropriate name for my intermediary class: BoardPresenter.
Connecting the Dots This satisfaction comes about because of the second cool thing: the extremely simple BoardPanel, which represents the bulk of the visual interface. The source file for BoardPanel contains 56 lines, and that's nicely formatted with blank lines. The bulk of the display work is in three methods, each only one or two lines long. The only real complexity is in managing the double-buffering. The evolution of the code into the recognized MVP pattern was cool thing #3. The last cool thing: When you're developing GUIs (including Web pages) test-first, the need to constantly crank up the GUI itself diminishes. My real jollies come from seeing the tests run. My JUnit green bar gave me confidence that the underlying model was doing its job properly. I've seen developers wait until the end of the day before kicking off the actual GUI, along with a "Ho-hum, of course it works." No ho-hums here: This exercise was a blast for me. Here's what the game of dots looks like (see "A Deceptively Simple GUI," above).
Click here to download a zip file of the complete code. Please note that you will need JUnit (www.junit.org).
|
|
||||||||||||||||||||||||||||||
|
|
|
|