Special Effects
GuiNim is much more fun to play than ConNim. And yet
GuiNim 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's
public 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's
public 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:
import java.io.*;
import javax.sound.sampled.*;
// ...
private void playSound (File file)
{
try
{
// Get an AudioInputStream from the
// specified file (which must be a
// valid audio file, otherwise an
// UnsupportedAudioFileException
// object is thrown).
AudioInputStream ais =
AudioSystem.getAudioInputStream (file);
// Get the AudioFormat for the sound data
// in the AudioInputStream.
AudioFormat af = ais.getFormat ();
// Create a DataLine.Info object that
// describes the format for data to be
// output over a Line.
DataLine.Info dli =
new DataLine.Info (SourceDataLine.class,
af);
// Do any installed Mixers support the
// Line? If not, we cannot play a sound
// file.
if (AudioSystem.isLineSupported (dli))
{
// Obtain matching Line as a
// SourceDataLine (a Line to which
// sound data may be written).
SourceDataLine sdl = (SourceDataLine)
AudioSystem.getLine (dli);
// Acquire system resources and make
// the SourceDataLine operational.
sdl.open (af);
// Initiate output over the
// SourceDataLine.
sdl.start ();
// Size and create buffer for holding
// bytes read and written.
int frameSize = af.getFrameSize ();
int bufferLenInFrames =
sdl.getBufferSize () / 8;
int bufferLenInBytes =
bufferLenInFrames * frameSize;
byte [] buffer =
new byte [bufferLenInBytes];
// Read data from the AudioInputStream
// into the buffer and then copy that
// buffer's contents to the
// SourceDataLine.
int numBytesRead;
while ((numBytesRead =
ais.read (buffer)) != -1)
sdl.write (buffer, 0, numBytesRead);
}
}
catch (LineUnavailableException e)
{
}
catch (UnsupportedAudioFileException e)
{
}
catch (IOException e)
{
}
}
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:
private ImageIcon pileNeg;
// ...
int [] pixels = new int [pilew*pileh];
java.awt.image.PixelGrabber pg;
pg = new java.awt.image.PixelGrabber (pile.getImage (),
0, 0,
pilew, pileh,
pixels,
0, pilew);
try
{
pg.grabPixels ();
}
catch (InterruptedException e)
{
}
for (int i = 0; i < pixels.length; i++)
pixels [i] = pixels [i] ^ 0xffffff;
java.awt.image.MemoryImageSource mis;
mis = new java.awt.image.MemoryImageSource (pilew, pileh,
pixels,
0, pilew);
pileNeg = new ImageIcon (createImage (mis));
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:
final ImageIcon oldPile = pile;
ActionListener al;
al = new ActionListener ()
{
public void actionPerformed (ActionEvent
e)
{
repaint ();
if (pile == oldPile)
pile = pileNeg;
else
pile = oldPile;
}
};
Timer t = new Timer (ANIM_DELAY, al);
t.start ();
boolean continuePlay;
continuePlay = continueGame ("Computer player " +
"wins. Play again?");
t.stop ();
pile = oldPile;
repaint ();
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 the
pile 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's
paint 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 case
pile 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?
Next month's Java Tech explores the basics of thread synchronization and looks
at Java 1.5's java.util.concurrent.Semaphore class.
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.