Java Tech: An Intelligent Nim Computer Game, Part 2

Last time, I introduced the game of Nim and explored the game tree and minimax tools. You learned how those tools work together, in a computerized version of
Nim, to help the computer player make the optimal move. This article takes
those tools out of the toolbox and puts them to work, as we develop
console-based and GUI-based Java versions of Nim. You learn how each version
gets the human player to select how many matches to take -- and how to
achieve some special effects for the GUI-based version.

Console-Based Nim

I prefer to develop the console-based version first. That way, we focus on the
essentials of how to make a computerized Nim game work, without getting bogged
down with GUI details. The version that I have in mind will involve an initial
pile of eleven matches. Also, the human player will always make the first
move. Why? If the computer player (which always tries to make the optimal
move) goes first, the human player would most likely never win. And that would
make for a very boring game!

The following source code, which I excerpted from the main method
in the ConNim application (see the nim2.zip file
that accompanies this article), describes the essence of the console-based Nim
game:

// Build a game tree to keep track of all possible// game configurations that could occur during// games of Nim with an initial pile of eleven// matches. The first move is made by player A.

Node root = buildGameTree (11, 'A');

// Display startup information banner.

System.out.println ("WELCOME TO THE GAME OF " + "NIM: 11 MATCHES IN PILE");

// Play the game until either player A (the human// player) or player B (the computer player) wins.

while (true){ // Obtain number of matches that are taken by // player A.

// Use the minimax algorithm to determine if // the computer player's optimal move is the // child node left of the current root node, // the child node below the current root node, // or the child node right of the current root // node.

After building the eleven-match game tree and outputting a startup banner, the
code above enters the main game loop. That loop accomplishes these tasks:

The human player -- player A -- inputs the number of matches to take.
This number is then validated: only 1, 2, or 3 is valid, and this number must not exceed
the number of remaining matches.

The number of matches chosen by the human player is used to select the new
root node. If the new root node's number-of-matches variable contains 0, a
terminal configuration node has been reached, the human player loses, and the
computer player -- player B -- wins. The program exits at this point.
Otherwise, the new root node represents the position from which the computer
player makes its move.

A minimax number is computed on the root node's left child. If the root node
also has center and right child nodes, minimax numbers are computed on those
nodes. Because the root node may not have center or right child nodes (look at
Figures 1 and 2 in the previous Java Tech installment for examples), care is
taken to avoid a NullPointerException
(root.center != null, for example). If there is no center or
right node, 2 is selected as the minimax number for that nonexistent node.
That way, the nonexistent node won't be chosen in subsequent comparison logic.

The minimax numbers are compared to find the minimum, which is then used to
identify the number of matches for the computer player to take. In some cases,
there may be no minimum: all of the minimax numbers may be the same. Because
there is no optimal move in those situations, a random number generator
selects 1, 2, or 3 matches to take. (Note: Selecting a random number of
matches to take will not result in an exception, because this situation only
occurs in areas of the game tree where there are immediate left, center, and
right child nodes.)

The number of matches chosen by the computer player is used to select the new
root node. If the new root node's number-of-matches variable contains 0, a
terminal configuration node has been reached, the computer player loses, and
the human player wins. The program exits at this point. Otherwise, the new
root node represents the position from which the human player makes a move.

The main game loop continues.

Compile ConNim's source code and run the resulting application to
play console-based Nim. A sample session appears below:

WELCOME TO THE GAME OF NIM: 11 MATCHES IN PILEHow many matches do you want? 1Human player takes 1 match, leaving 10Computer player takes 1 match, leaving 9How many matches do you want? 1Human player takes 1 match, leaving 8Computer player takes 3 matches, leaving 5How many matches do you want? 1Human player takes 1 match, leaving 4Computer player takes 3 matches, leaving 1How many matches do you want? 1Human player takes 1 match, leaving 0Computer player wins the game!

Numeric Input

ConNim features an inputInteger method for obtaining
numeric input from the human player: 1, 2, or 3 matches. Although I could have
used Java 1.5's java.util.Scanner class for input, I decided upon
a customized inputInteger method, to support previous versions of
Java. That method's source code appears below:

inputInteger reads its input, on a character-by-character basis,
until a new-line character is read. Each character, except for new-line and
carriage return (on Windows platforms), is stored in a StringBuffer
object. When new-line is seen, the contents of the StringBuffer
are parsed and the resulting value returns. But if the contents of theStringBuffer do not represent a number, the parsing logic throws
a NumberFormatException object, which is caught by a catch clause
that outputs a message and immediately terminates the program.

A second catch clause is present to deal with IOException objects
that are thrown when System.in.read is redirected to a file and
something goes wrong during file I/O. Although it is unlikely that you will
ever see an I/O Problem message (that outputs in response to thecatch clause handling an IOException), Java requires this class
of exception to be handled (or declared in a throws clause).

GUI-Based NIM

Let's face it: after a few rounds, console-based Nim loses its appeal. Some of
that loss is due to the predictable nature of the game, which results from the
small number of matches (11) in the initial pile. Although a larger initial
pile would make game play less predictable, the resulting game tree would
require more memory. At some point, we wouldn't be able to fit the entire game
tree into memory, and would need to redesign console-based Nim's game tree and
minimax logic.

I believe most of console-based Nim's loss of appeal is due to its lack of a GUI.
GUIs are fun to look at and interact with: they invite users to play, which is
why a GUI-based version of Nim is needed. Figure 1 shows that version's GUI.

Figure 1's GUI is more interesting than ConNim's console output.
It reveals two match-pile images and a draggable grid of matches. The human
player uses the mouse to select 1, 2, or 3 matches; and then drags the matches
to the human player's match pile, where they are dropped (and disappear). The
computer player then selects 1, 2, or 3 matches, and they also disappear. (We
can assume the matches were dragged to the computer player's match pile before
vanishing.) After the last match has been taken, GuiNim presents
a message box that announces the winning player and lets the human player make
a choice to continue the game or not.

The GUI shown in Figure 1 was built by the GuiNim application.
(See the nim2.zip file for that application's source code.) The application consists of four classes: the GuiNim driver, theGamePanel component, and Match and Node
component support classes. GuiNim's source code appears below:

GuiNim's source code reveals a Swing application. The constructor
adds a GamePanel component to the non-resizable frame window's
content pane.
GamePanel is a big class. Its constructor builds an eleven-match
game tree, loads the logo and match pile images, creates an eleven-element array
of Match objects, and installs listeners that detect "mouse button
pressed," "mouse button released," and "mouse dragged" events. (I examine these
listeners in the next section.) GamePanel also introduces methods
to build the game tree, compute a minimax value, ask the user if they want to
continue the game, return the GamePanel component's preferred
size (that is used by the content pane's layout manager), paint the component,
reorder Match objects (so that onscreen matches drag over and not
under other onscreen matches), and reset the game (if the user chooses to play
another round). The source code to GamePanel's paint
method appears below:

There is a definite order to the way paint works: images and text
are painted before the onscreen versions of all non-dropped Match
objects. (After all, we don't want to hide onscreen matches behind images or
text.)

Onscreen matches are backed by Match objects. That class provides
a constructor, and methods to determine if the mouse coordinates locate within
the boundaries of an onscreen match, draw the match, and handle match dropping
and selection. Match's draw method, which draws the
associated onscreen match, appears below:

// Fill the top of the match (outline and // interior) with red pixels.

g.setColor (Color.red); g.fillRect (ox, oy, MATCHWIDTH+1, 5);}

The ox and oy variables identify the origin for the
onscreen match. That origin is the upper-left corner. If the match hasn't been
selected, the variable selected is false, and a white onscreen match
is drawn. But if that variable is true, the onscreen match is colored cyan, to
emphasize that it has been selected by the human player.

Note: because you've seen the Node class in the last installment,
I have nothing to say about that class.

Compile GuiNim's source code and run the resulting application to
play GUI-based Nim. Figure 2 reveals a pair of selected matches being dragged
to the human player's pile.

Figure 2. Dragging matches to the human player's pile of matches

Match Drag-and-Drop

GuiNim features a more intuitive way than ConNim for
obtaining, from the human player, the number of matches to take from the pile:match drag-and-drop. That input technique involves selecting 1, 2, or 3
onscreen matches, dragging them to some destination, and dropping them at that
destination.

Match drag-and-drop is implemented as a pair of listeners created inGamePanel's constructor: a mouse listener and a mouse motion
listener. For simplicity, match drag-and-drop does not rely on the Abstract
Windowing Toolkit's drag-and-drop API. If you would like to learn about the
AWT's drag-and-drop API, consult the Java 2 SDK documentation on thejava.awt.dnd package.

The mouse listener is an object created from an anonymous inner class that
subclasses java.awt.event.MouseAdapter. Similarly, the mouse
motion listener is an object created from an anonymous inner class that
subclasses java.awt.event.MouseMotionAdapter. Certain methods in
each listener are overridden to achieve match drag-and-drop:

MouseAdapter's mousePressed method handles selection.
It first obtains the current mouse pointer coordinates, then locates theMatch object corresponding to the onscreen match over which the
mouse pointer is hovering, and finally uses that object to select the
associated onscreen match. If the Shift key is not being held down, the
previously selected onscreen match is deselected prior to the new onscreen
match being selected. But if Shift is held down, the newly selected onscreen
match is added to a group of (at most) three selected onscreen matches,
possibly replacing the most recently selected onscreen match (if the group is
full). When an onscreen match is selected, its color changes to cyan, to
provide positive visual feedback to the user.

MouseAdapter's mouseReleased method handles dropping.
It checks if the Shift key is being held down. If so, no drop is performed.
The reason: the human player may still be selecting onscreen matches in
preparation for a drag operation. If Shift is not being held down, the drop
operation occurs, provided at least one of the selected matches is droppable:
it locates over (or near) the human player's pile.

MouseMotionAdapter's mouseDragged method handles
dragging. If at least one onscreen match has been selected, it obtains the
current mouse pointer coordinates and moves each selected onscreen match to
the new location specified by its associated Match object,
provided all onscreen matches would lie completely within the bounds of theGamePanel component after the move. (We don't want to move
onscreen matches past the component's boundaries, because that would likely
confuse the user.)

Match drag-and-drop also keeps track of a drag origin and reordersMatch objects as necessary. The drag origin creates a relative
displacement during dragging, so that each onscreen match moves the same
relative amount. Reordering Match objects causes onscreen matches
to drag over (instead of under) other onscreen matches. Furthermore,
reordering ensures that moving the mouse pointer over an onscreen match, that
locates on top of another onscreen match, and then pressing the mouse button,
results in the top onscreen match (instead of the bottom onscreen match) being
selected.

Note: the match drag-and-drop technique can be generalized to drag any kind of
graphics shape (which is backed by an object) around the screen, and then drop
the shape at a destination. Before you can do that, however, it's important to
understand that technique. Because the GamePanel class is big and
contains unrelated code, it might be difficult to fully understand match
drag-and-drop. For that reason, I'm providing two MatchDrag
applets, whose source codes are completely specific to the match drag-and-drop
technique. MatchDrag1 focuses on selecting and dragging one match
around the applet's drawing area; MatchDrag2, whose logic I used
in GuiNim, focuses on selecting 1, 2, or 3 matches, dragging them
around the drawing area, and finally dropping them. (See this article'snim2.zip file for the source code of both applets.) If you'd like to
adapt match drag-and-drop to other graphics shapes, I recommend that you first
study both applets.

Special Effects

GuiNim is much more fun to play than ConNim. And yetGuiNim is still somewhat lacking in aesthetics that would make it
even more entertaining. In this section, I propose two enhancements to improve
game play: sound effects and image effects.

Note: you can add further enhancements to GuiNim, such as letting
the user enter his or her name, displaying the user's name along with a number that
identifies the current round and the number of rounds the user has won, saving
user information to a file, and loading/displaying info associated with the
user who has achieved the highest number of won rounds. Because these
improvements aren't difficult to achieve, I leave it to you to supply them for
your own version of GuiNim.

Sound Effects

When the human player drops one or more matches onto his or her match pile,
we should hear a sound that positively reinforces that action. The simplest
way to accomplish that task is to employ java.awt.Toolkit'spublic abstract void beep() method. Because hearing a simple beep
isn't that entertaining, I think we should play some arbitrary .wav file, such
as drop.wav.

How do we play the .wav file? The traditional approach (from an applet
perspective) is to use java.applet.Applet'spublic static final AudioClip newAudioClip(URL r) method. However,
that approach means the application is tied to applet functionality, and that
functionality is not guaranteed to be present on all platforms. A better
approach is to use the JavaSound API, which was first integrated into version
1.3 of the core Java platform.

I've created a playSound method that fully encapsulates the
JavaSound logic needed to play the drop.wav file that accompanies
this article. That method's source code appears below:

Because the method is fully commented, I won't elaborate further on what
is happening. (For more information, please consult the SDK documentation on
the various classes and interfaces that make up JavaSound.) However, note that
the three exception handlers are empty: the user is only notified that there's
a problem if a sound cannot be heard. I chose not to pop up a message box,
because the user really doesn't want to see that box pop up each time he or she
drags one or more matches to his or her match pile (and drops them).

The playSound method requires a java.io.File
argument that identifies the .wav file to play. To avoid the excessive
creation of File objects, I've created a DROP_SOUND
constant, which is passed to playSound, as follows:

private final static File DROP_SOUND = new File ("drop.wav");

// ...

playSound (DROP_SOUND);

Image Effects

Is the display of a message box that announces the winner enough feedback when
a player wins? Why not make the screen ripple, shoot off some fireworks, or
offer some other kind of visual pizzazz? To keep this article from becoming
too large, I've settled on something simple: flash both match pile images. To
accomplish that task, we first need to create a negative match pile image. The
following code fragment does just that:

The code fragment above grabs the pile-referenced image's pixels
and stores them in a pixels array. Each array element's RGB (red,
green, blue) value is then inverted via the exclusive or operator. Finally,
those RGB values are combined into a brand-new image that pileNeg
references.

Flashing the match pile images requires animation logic, which must appear in
two places within GamePanel's mouse released
listener -- before both calls to the continueGame method. The
animation logic is provided by the code fragment below:

The animation logic begins by saving pile's ImageIcon
reference, so that the original image can be restored following the animation.
(When we stop the animation, we don't want the negative image to be displayed.
Not only is that unsightly, pile is referencing the negative
match pile image. If we don't restore pile to the original image's
reference, we lose that reference and no more animations are visible.)

The logic next creates an object from an anonymous inner class that implements
the java.awt.event.ActionListener interface. Each call to that
object's actionPerformed method generates one frame of animation:
first it repaints the GamePanel's drawing surface (in response to
the repaint (); method call), and then it sets thepile variable to either the original image's reference (which was
previously saved in oldPile) or the negative image's reference
(in pileNeg). This is done because GamePanel'spaint method (which is responsible for painting that component's
drawing surface) displays whatever image is referenced by pile.

A timer is created, by way of javax.swing.Timer, after creating
the ActionListener object. That timer handles the animation, once
its start method is called. Each timer event invokes the
listener's actionPerformed method. Following the call, the timer
pauses for the number of milliseconds specified by the ANIM_DELAY
constant -- to give the user a chance to view the animation.

Subsequent to the display of the "continue game" message box and the retrieval
of the user's continuation choice, the animation is stopped, pile
is reset to the reference previously stored in oldPile (in casepile contains the pileNeg reference), and a final
repaint occurs (in case the negative match pile image is currently displayed).

Conclusion

We put the game tree and minimax tools to good use as we created console-based
and GUI-based Java versions of Nim. We learned how those versions work and how
they obtain their "number of matches" input from the user: simple integer
input or match drag-and-drop (also applicable to other kinds of game objects,
such as chess or checker pieces). What is a game without special effects? We
added a sound effect and an image effect to the GUI-based Nim game, to make it
more entertaining.

Once again, there is some homework for you to accomplish:

Modify ConNim, so that it uses the Scanner class to
handle input from the user.

GuiNim's onscreen match drag-and-drop logic reveals a quirk.
You've selected 1, 2, or 3 onscreen matches while pressing the Shift key,
released that key after releasing the mouse button, and noticed that all
selected onscreen matches still appear cyan (meaning they are selected). You
can deal with the quirk by moving the mouse pointer to a blank area of the
screen and then clicking the mouse button, or by releasing the Shift key
before releasing the mouse button. Can you think of some better way to handle
this quirk?

Answers to Previous Homework

The previous Java Tech article presented you with some challenging homework on
the game tree and minimax tools. Let's revisit that homework and investigate
solutions.

The number of nodes in Nim's game tree grows quite rapidly as the initial
number of matches increases slightly. For example, 1 match yields 2 nodes, 2
matches yield 4 nodes, 3 matches yield 8 nodes, and 4 matches yield 15 nodes.
For 21 matches, how many nodes are created?

For 21 matches, the number of nodes in Nim's game tree equals 489,396. How did
I obtain this number? One technique: place the expression count++ at
the start of the method buildGameTree, and output count's
value after calling that method. Another (somewhat cumbersome) technique: take
advantage of the Nim game tree property where the number of nodes associated
with the number of matches x (x must be greater than 3) is 1 plus
the sum of the node counts for x-1, x-2, and x-3 matches.
For example, 1+15+8+4 (28) nodes are created when the number of matches is 5.

If you cannot store an entire game tree in memory because of its size, how
could you adapt minimax to work with such a game tree?

When an entire game tree cannot be stored in memory, minimax can be adapted to
work with part of the game tree by integrating the alpha-beta pruning
technique into that algorithm. I will explore the alpha-beta pruning technique in a
future installment of Java Tech.

Jeff Friesen is a freelance software developer and educator specializing in Java technology. Check out his site at javajeff.mb.ca.