Peg Game - GUI

The project

Refactor the previous chapter's implementation into a UI-independent model and a DOS view. Create a second interchangeable GUI view.

Three iterations:

  1. draw board
  2. select peg and remove or reinstate
  3. refactor into model, DOS view, and Swing view

Iteration 1

Implement a PegGameSwing class that displays the starting position of the game in a window. Do not implement any selection or "move" functionality yet.

Instructions and issues:

Iteration 2

In this iteration, add a peg selection capability. When the user selects a peg position,alternately remove and reinstate its peg.

Instructions and issues:

Iteration 3

Now that we have two user interface implementations, let's refactor everything into: a model. two views, and an interface that will provide decoupling between the independent model and the dependent views. The model encapsulates all "business logic" and application state. Each view encapsulates all user presentation and interaction functionality.

How do you want to divvy up the current code base? What state exists in our application? Should any state be replicated (or cached) in each view for the sake of efficiency or simplicity? Which functions represent functionality that is independent of user interface, and which functions are peculiar to the DOS or GUI interfaces? What method signatures are sufficient to establish a contract between the cornerstone model and all optional views?

Instructions and issues:

Iteration 1 - implementation

We could use JRadioButton components to create our playing surface. But they are rather small, the label would go to the side, and this is an excellent vehicle to practice drawing a GUI from scratch. In order to draw graphics primitives, and capture mouse input, a Canvas component is used. There are any number of ways that classes can be specialized, methods overridden, and components configured; but having PegGameSwing extend the Canvas class and then add itself to the BorderLayout manager of the frame's content pane minimizes the number of objects created. The Canvas component provides a paint() method that must be overridden to initially draw the canvas' display, and, redraw the canvas as portions are exposed when windows are moved resized. Redrawing the entire board each time an expose event is generated does not optimize performance, but we are primarily interested in more generic algorithm and data structure subjects.

To draw 15 ovals in the game's triangular shape, we have the choice to "compute or store" the X and Y positions of each. The choice made in listing 5.1 was to store those values. The reason was that it seems to be more self-documenting. The reader can readily browse the adjacent entries, and infer "the method behind the madness".

Rather than hard-coding literal constants throughout the code, life is always made easier by introducing an "extra level of indirection" with symbolic constants. When the inevitable happens, and change occurs, having these kinds of values defined in only one place makes it very easy to change their values.

The array of Y values is easy. Each row is drawn at a constant Y value. That value need only be replicated the correct number of times per row. The array of X values is more subtle. Looking at the left edges of the 15 pegs, we see there are nine different X values. The 11 peg is drawn at the first X value, 7 peg at the second, 4 and 12 pegs at the third, 2 and 8 pegs at the fourth, 1/5/13 pegs at the fifth, 3 and 9 pegs at the sixth, 6 and 14 pegs at the seventh, 10 peg at the eighth, 15 peg at the ninth.

Drawing the label for each peg is more interesting. The expression ""+(i+1) is a cheap way to convert an integer to a string. Labels 1 through 9 are narrower than labels 10 through 15, and need to be drawn further to the right. We could use the ternary operator to explicitly choose which XS array element to use. Listing 5.1 uses integer division instead. This operator throws away the remainder of the division (precisely the thing that the modulus operator returns). The expression below returns 0 for values 0 through 8, and 1 for values 9 through 14.

Listing 5.1

Iteration 2 - implementation

Shall we use the same data structure to model peg state here that we did for the DOS application? Let's. This pegsPresent array will be used in drawPeg() to decide whether to draw blue ovals and white labels, or vice versa.

To receive "mouse pressed" events the MouseListener interface must be implemented. The only method that needs to be defined is mousePressed(). The X and Y pixel locations of the mouse pick are retrieved from the MouseEvent parameter. How shall we transform these two values into one integer that represents the selected peg?

If the pegs were layed out in a regular grid, then we could use integer division on the X and Y values to compute the corresponding peg. But the triangular arrangement doesn't seem to be sufficiently regular. Perhaps we could compute the correct row with integer division, and then use multiple tiers of "case" statements to figure out the correct column.

On the other had, if we are willing to throw a little extra memory at the problem, we could overlay a "fine grained" grid on the game board, and have each grid location reference a single peg. We could even let seemingly unused grid locations reference some reasonable peg. Given the mapping demonstrated below, the user can be fairly sloppy horizontally when selecting pegs 1, 2, 3, 4, 6, 7, and 10. The cells data structure in listing 5.2 implements this strategy.

Now that we have a regular grid, we can use integer division to transpose X and Y pixel values into column and row cell values. The division operations are a little sloppy - they do not account for the MARG (margin) constant. The only discernable ill-effect is the possibility of computing a column or row that is one too high. This edge effect is handled by the if statements below. The column and row computed can then be used to look-up the correct peg. Listing 5.2

Iteration 3 - implementation

The members of the class PegGameDos are: Which members seem to represent core "business logic" that is independent of any current (or future) user interfaces?

Let's migrate the first three to a new PegGameModel class. See listing 5.3.

What interaction seems necessary from the PegGameModel object to the PegGameView object?

When attemptMove() finds a legal move, it either needs to refresh the entire user interface, or, it needs to vacate two peg locations and fill one peg location. The latter choice would be more efficient. Having the model change the state of individual peg locations in the view would seem to be a sufficient contract to decouple one from the other.

The removePeg() method in listing 5.3 will be introduced next.

Listing 5.3

The PegGameDos class should now implement the contract that the PegGameModel expects. It creates the model object, and initializes that object with a reference to itself. Now that the "business logic" attributes and methods have been removed from the view, it must ask the model to attemptMove(). A new method was needed in the model to update its state at the start of the game when the user designates where the initial hole should be. Note the removePeg() call below. Listing 5.3 shows the peg state being maintained in the model, but, in listing 5.4, it has been replicated (or cached) in the view. This allows the view to redisplay itself independent of the model.

Listing 5.4

Now that we have the model, the interface, and the DOS view working, let's complete the implementation of the Swing view.

How does listing 5.2 need to evolve in order to complete the PegGame Swing implementation?

Lets first set-up the roles and relationship of model and view. This is almost identical to the DOS view.

The current user interaction functionality is limited to selecting and removing pegs. This needs to be extended to support jumping pegs. Is there a way of separating select initial hole and move (from to) selection events? How can the from and to selections be distinguished?

All user selection events are funneled through the mousePressed() method. Unless we introduce new GUI features (like JButton components, or modifier keys), well need to conceive of some way for this one method to serve three different purposes: remove a peg to start the game, select the from peg, and select the to hole.

State machine describes an application whose response to external inputs depends upon its current state. An example is a push button on/off switch for a radio. It has two states: ON, and OFF; and one input: push. When the machine is in the OFF state and receives a push input, its response is to go to the ON state. When its in the ON state and a push is received, its response is to return to the OFF state.

In our application, the only input is select. The number of responses needed is three: remove, from, and to. Each response corresponds to a state. The program starts off waiting for a remove request. This could be described as the REMOVE state. A select input causes the designated peg to be removed, and transitions the application to the FROM state. The next select input toggles the background of the selected peg, and sets the program to the TO state. One more select input completes a move request, and returns the application to the FROM state. The program cycles between the FROM and TO states until no more moves are possible.

How can the application enforce this flow of control?

An integer attribute could be used to remember the programs current state. A value of 0 could represent the REMOVE state, 1 could designate the FROM state, and 2 could map to the TO state.

When the TO hole is selected, how will we remember what the previous FROM peg was?

A second integer attribute would work. Or, we could try assigning both responsibilities to the state attribute we just specified. That attribute could hold the from peg (a value between 0 and 14) when the program transitions to the TO state. After the to hole is selected, the state attribute could be assigned some special value (-1 is used below) to represent the FROM state. Finally, a second special value could designate the initial REMOVE state (-2 is used below).

The implementation below exercises an additional slight optimization. If the user wants to unselect her from peg (firstPeg == i), there is no need to repaint() the entire board, just update that single location.

The final change is an update to drawPeg(). The from peg needs to be drawn in red while the application is waiting for the to hole selection. This can be accomplished by testing whether the specified peg is the previously selected from peg. Listing 5.5