If this cell is not flagged or if you’re right-clicking it, just go ahead and let the MouseEvent pass, but if this cell is flagged and you’re trying to left-click it, stop it dead in its
Trang 1there’s no mine in here, it reveals the cell and dispatches a
MineCellEvent.REVEALED event If there is a mine in here, it sets the color to col-ors[MINE] and dispatches a MineCellEvent.DETONATED event Another anony-mous inner class listens for MouseEvent s.
The superclass, JPRButton3D , doesn’t do anything with right mouse clicks, but this MouseAdapter does It either flags or unflags this cell based on whether the cell is already flagged and dispatches the corresponding event.
Another thing the MineCell class needed to take care of was to prevent action events from mine cells that are flagged You don’t want to let the player click a flagged cell and blow up, right? Nor do you want the cell to be animated You want it to be concrete that if this cell is flagged, you can’t click it with the left mouse button, period To accomplish this, the MineCell class overrides the
processMouseEvent(MouseEvent) method If this cell is not flagged or if you’re right-clicking it, just go ahead and let the MouseEvent pass, but if this cell is flagged and you’re trying to left-click it, stop it dead in its tracks:
public void processMouseEvent(MouseEvent e) {
if (!flagged || e.getModifiers() == MouseEvent.BUTTON3_MASK) super.processMouseEvent(e);
} Here is the full source code listing for MineCell.java : /*
* MineCell
* Defines one cell of the MinePatrol Game
*/
import java.awt.*;
import java.awt.event.*;
import java.util.Vector;
import jpr.lightweight.JPRButton3D;
public class MineCell extends JPRButton3D { protected int contents;
public final static int EMPTY = 0;
public final static int MINE = 9;
//These colors are indexed by the contents Color[EMPTY] is for //revealed cells and colors[MINE] is for detonated cells protected Color[] colors;
protected boolean hidden, detonated, flagged;
//acts as the background color when the cell becomes visible protected static Image flagImg, mineImg, explodeImg;
protected Vector listeners;
public MineCell() { this(EMPTY);
}
458
J a
s o
l ut
n e
Trang 2public MineCell(int contains) { super();
colors = new Color[] {getBackground(), Color.blue,
Color.cyan, Color.green, Color.magenta, Color.yellow, Color.orange, Color.red, Color.black,
Color.red.darker().darker()};
setFont(new Font("Arial", Font.BOLD, 16));
resetContents(contains);
listeners = new Vector();
addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { MineCellEvent mce;
if (flagged) return;
if (contents < MINE) { setHidden(false);
mce = new MineCellEvent(MineCell.this,
MineCellEvent.REVEALED);
} else { detonated = true;
setBackground(colors[MINE]);
setControlColor(colors[MINE]);
setHidden(false);
mce = new MineCellEvent(MineCell.this,
MineCellEvent.DETONATED);
} (new EventThread(mce)).start();
} });
addMouseListener(new MouseAdapter() { MineCellEvent mce;
public void mousePressed(MouseEvent e) {
if (e.getModifiers() == MouseEvent.BUTTON3_MASK && hidden) {
if (flagged) { flagged = false;
repaint();
mce = new MineCellEvent(MineCell.this,
MineCellEvent.UNFLAGGED);
} else { flagged = true;
repaint();
mce = new MineCellEvent(MineCell.this,
MineCellEvent.FLAGGED);
} (new EventThread(mce)).start();
} } });
}
459
i n
Trang 3protected class EventThread extends Thread { MineCellEvent e;
EventThread(MineCellEvent mce) {
e = mce;
} public void run() { switch(e.getID()) { case MineCellEvent.REVEALED:
for (int i=0; i < listeners.size(); i++) { ((MineCellListener)listeners.elementAt(i)).mineCellRevealed(e); }
break;
case MineCellEvent.FLAGGED:
for (int i=0; i < listeners.size(); i++) { ((MineCellListener)listeners.elementAt(i)).mineCellFlagged(e); }
break;
case MineCellEvent.UNFLAGGED:
for (int i=0; i < listeners.size(); i++) { ((MineCellListener)listeners.elementAt(i)).mineCellUnflagged(e); }
break;
case MineCellEvent.DETONATED:
for (int i=0; i < listeners.size(); i++) { ((MineCellListener)listeners.elementAt(i)).mineCellDetonated(e); }
break;
} } } public void setContents(int contains) { contents = contains >= EMPTY && contains <= MINE ? contains : EMPTY; setForeground(colors[contents]);
} public void resetContents(int contains) { setContents(contains);
setHidden(true);
detonated = false;
} public int getContents() { return contents;
} public void setHidden(boolean h) { hidden = h;
if (h) { setBackground(SystemColor.control);
setControlColor(SystemColor.control);
}
460
J a
s o
l ut
n e
Trang 4else if (!detonated) { setBackground(colors[EMPTY]);
setControlColor(colors[EMPTY]);
} flagged = false;
setEnabled(h);
repaint();
} public boolean isHidden() { return hidden;
} public boolean isFlagged() { return flagged;
} public static void setImages(Image f, Image m, Image e) { flagImg = f;
mineImg = m;
explodeImg = e;
} public void paint(Graphics g) { super.paint(g);
if (!hidden || flagged) drawContents(g);
} protected void drawContents(Graphics g) { Image img = null;
if (contents == MINE || flagged) {
if (flagged) img = flagImg;
else if (contents == MINE && detonated) img = explodeImg;
else if (contents == MINE && !detonated) img = mineImg;
if (img != null) { g.drawImage(img, (getSize().width - img.getWidth(this)) / 2,
(getSize().height - img.getHeight(this)) /2, this);
} } else if (contents != EMPTY) { FontMetrics fm = g.getFontMetrics();
g.setColor(getForeground());
g.drawString(String.valueOf(contents), (getSize().width
- fm.stringWidth(String.valueOf(contents))) / 2, (getSize().height + fm.getHeight()) / 2 - fm.getDescent());
} } public void processMouseEvent(MouseEvent e) {
if (!flagged || e.getModifiers() == MouseEvent.BUTTON3_MASK) super.processMouseEvent(e);
}
461
i n
Trang 5public void addMineCellListener(MineCellListener mcl) { listeners.addElement(mcl);
} public void removeMineCellListener(MineCellListener mcl) { listeners.removeElement(mcl);
} }
Testing the MineCell Class
The MineCellTest class constructs 12 MineCell instances in an array When it con-structs the array of MineCell s it sets the contents of the array to the value of the subscript, so the MineCell at cells[0] has its contents set to 0 ( MineCell.EMPTY ), the MineCell at cells[1] has its contents set to 1, and so on Because the indices
of the array go higher than nine, the last three mines are set to nine, ( Mine-Cell.MINE ) MineCellTest also implements MineCellLisetener and adds itself as
a listener of all the MineCell s Next it loads the images flag.gif , mine.gif , and
explode.gif using MediaTracker and sets the images by calling MineCell.setIm-ages(Image, Image, Image)
It implements the MineCellListener methods to notify you of what events are occurring by updating its label each time an event is heard If a cell detonates, all the other cells are revealed too, so you can see the mine graphic in the other two cells that contain mines You can see a run of this in Figure 12.6 Here is the source code:
/*
* MineCellTest
* Tests the MineCell class
*/
import java.awt.*;
public class MineCellTest extends GUIFrame
implements MineCellListener { MineCell[] cells;
Label statusLabel;
public MineCellTest() { super("MineCell Test");
cells = new MineCell[12];
Panel cellPanel = new Panel();
cellPanel.setLayout(new GridLayout(3, 0));
for (int i=0; i < cells.length; i++) { cells[i] = new MineCell(i < 10 ? i : 9);
cells[i].addMineCellListener(this);
cells[i].setSize(50, 50);
cellPanel.add(cells[i]);
}
462
J a
s o
l ut
n e
Trang 6add(cellPanel, BorderLayout.CENTER);
MediaTracker mt = new MediaTracker(this);
Image[] imgs = { Toolkit.getDefaultToolkit().getImage("flag.gif"), Toolkit.getDefaultToolkit().getImage("mine.gif"), Toolkit.getDefaultToolkit().getImage("explode.gif") };
for (int i=0; i < imgs.length; i++) { mt.addImage(imgs[i], i);
} try { mt.waitForAll();
} catch (InterruptedException e) {}
MineCell.setImages(imgs[0], imgs[1], imgs[2]);
statusLabel = new Label();
add(statusLabel, BorderLayout.SOUTH);
pack();
setVisible(true);
} public static void main(String args[]) { new MineCellTest();
} public void mineCellRevealed(MineCellEvent e) { statusLabel.setText("Revealed");
} public void mineCellFlagged(MineCellEvent e) { statusLabel.setText("Flagged");
} public void mineCellUnflagged(MineCellEvent e) { statusLabel.setText("Unflagged");
} public void mineCellDetonated(MineCellEvent e) { statusLabel.setText("Detonated");
for (int i=0; i < cells.length; i++) { cells[i].setHidden(false);
} } }
463
i n
FIGURE 12.6
A test of the MineCellclass is
a success
Trang 7Creating the Mine Field Classes
Similar to the mine cell classes, the mine field classes consist of an event class,
MineFieldEvent , a listener interface, MineFieldListener , and the MineField class itself.
The MineFieldEvent Class
There are four types of events that MineField s can trigger indicated by Mine-FieldEvent static constants, as follows:
• SOLVED is for when the entire field of mine cells is solved, such as when all the cells are either flagged or revealed and no one blew up.
• RANDOM indicates that the MineField was randomized, which means that the mines that are hidden within the mine field were rearranged ran-domly.
• The DETONATED constant indicates that one of the field’s MineCell objects exploded.
• FLAG_COUNT_CHANGED occurs when the player flags or unflags a cell within the MineField
The constructor accepts the object that triggered the thread and also the integer flag that indicates what type of event it fired The eventID member holds this value and you can access its value by calling the getID() method Here is the source code listing for MineFieldEvent.java :
/*
* MineFieldEvent
* Encapsulates events fired by MineFields
*/
public class MineFieldEvent extends java.util.EventObject { protected int eventID;
// event id constants public final static int SOLVED = 0,
RANDOMIZED = 1, DETONATED = 2, FLAG_COUNT_CHANGED = 3;
public MineFieldEvent(Object source, int id) { super(source);
eventID = id;
} public int getID() { return eventID;
} }
464
J a
s o
l ut
n e
Trang 8The MineFieldListener Interface
The MineFieldListener interface provides methods that correspond to the types
of events that are defined in the MineFieldEvent class There’s not much to explain here, so I’ll just list the short source code file:
/*
* MineFieldListener
* Interface for listening for MineFieldEvents
*/
public interface MineFieldListener { public void mineFieldSolved(MineFieldEvent e);
public void mineFieldRandomized(MineFieldEvent e);
public void mineFieldDetonated(MineFieldEvent e);
public void mineFieldFlagCountChanged(MineFieldEvent e);
}
The MineField Class
The MineField class lays out a grid of MineCell s, randomly places a set number
of mines into the cells, and then listens for MineCellEvent s It also has an inner
EventThread class that is uses to fire MineFieldEvent s for all its listeners, which
it keeps in a Vector You know, the same model you’ve been using for custom event handling since the last chapter It should be fresh in your mind from just reading the MineCell class which does the same kind of thing The MineField
class’s members are declared as follows:
protected int rows, cols, mines, flagged, revealed;
protected AudioClip revealClip, flagClip, unflagClip, explodeClip;
protected MineCell[][] cells;
protected Hashtable pointIndex;
protected Vector listeners;
Its integers are rows and cols , which keep track of the number of rows and columns in this MineField , mines keeps track of the number of mines, flagged
counts the number of cells that are flagged, and revealed counts the number of cells that are revealed It also declares four AudioClip objects that play sounds.
You can tell by their names when they are played The cells[][] array is a two-dimensional array of MineCell s that make up this MineField The pointIndex
object is a Hashtable The Hashtable class is provided in the java.util package.
It allows you to index an object by another object The MineField class makes use
of the Hashtable by storing the cells[][] indices as Point objects in the
Hashtable stored by the MineCell s that are stored at that point in the cells[][]
array It’s a mouthful, eh? Here’s what I’m talkin’ bout, Willis.
Say that MineCell a is stored at cells[1][2] Unlike the matrix[][] array of the
PlayArea class from the previous chapter, this array stores by [row][column]
465
i n
Trang 9because you don’t need to mess around with the actual component (x, y) coordi-nates like you did for the BlockGame application So MineCell a is at row 1, col-umn 2 of the cells[][] array.
Conceptually, x coordinates move left to right, specifying the columns, and y coordinates move up and down, specifying the rows The Point in the cells[][]
array where the cell is stored is x=2, y=1 So I construct that point and store the
Point into the Hashtable indexed by the MineCell a ( cells[1][2] stores a and
(Point)pointIndex.get(a) returns the point (2, 1)).
Why is this information useful, you ask? When a MineCell triggers an event you can get a reference to that MineCell by calling getSource() , but what is its posi-tion in the MineField ? You don’t know You’d have to loop through every index of the cells[][] array and compare e.getSource() == cells[row][column] to find
it You need to know where it is because you have to start checking the cells around it to see if they can be revealed.
Remember from the beginning of the chapter that if you click an empty cell, all the surrounding empty cells are revealed and also any empty cells that surround those cells are revealed too, up until it reaches cells that have some sort of mine adjacent to them, those cells don’t have their adjacent cells revealed To make getting the location quick and painless, you just store the location and index it
by the MineCell object it contains, and then you can get any MineCell ’s coordi-nates by checking the pointIndex Hashtable
The pointIndex Hashtable is constructed by passing in its initial size, rows * cols A hash table is a general computer science term that refers to mapping keys
to values In our case, we are mapping the MineCell object (the key) to its Point
location (the value) A Hashtable stores a value by a key by calling the
put(Object, Object) method The first argument is the key and the second argu-ment is the value To retrieve the value based on its key, you then call the
get(Object) method, passing in the key, and it returns the value So in this case passing in the MineCell will return the Point object that stores where the given
MineCell is positioned.
The randomizeField() method first loops on all the MineCell s and sets their con-tents to MineCell.EMPTY Then for each of the mines, it generates a random num-ber based on the total numnum-ber of cells Because there is only one random number, the row is determined by dividing the random number, index , by the number of columns (if there are 10 columns and the random number is 11, 11/10
is 1, so it would be the second row, dividing by the number of rows would not necessarily be correct) The column number is the remainder ( index % cols ), so given the previous example, the column number would be 1, (the second column from the left) So once it has a row and a column figured out, it looks to see
466
J a
s o
l ut
n e
Trang 10whether there is a mine there already (it wouldn’t be for the first mine, but could
be for subsequent mines) If not, it puts one there:
cells[index / cols][index % cols].resetContents(MineCell.MINE);
If there already is one there it just continuously generates new random numbers until it can find an index that is not occupied by a mine already Here is the loop that does this:
for (int m=0; m < mines; m++) {
do { index = rand.nextInt(rows * cols);
} while (cells[index / cols][index % cols].getContents() != MineCell.EMPTY);
cells[index / cols][index % cols].resetContents(MineCell.MINE);
} After all the mines are set in place, the number clues are set This is done by loop-ing on each cell For each cell, it counts the number of mines that are in the eight cells that surround it and sets the contents of the cell to that number If there are zero, this results in MineCell.EMPTY which is equal to zero Here is the code that accomplishes this feat:
protected void setNumberClues() { int nMines;
for (int r=0; r < cells.length; r++) { for (int c=0; c < cells[r].length; c++) {
if (cells[r][c].getContents() != MineCell.MINE) { nMines = 0;
//count the number of mines surrounding this cell for (int dr = r - 1; dr <= r + 1; dr++) { //prevent ArrayIndexOutOfBoundsException //continue puts control back to the beginning of the loop
if (dr < 0 || dr >= cells.length) continue;
for (int dc = c - 1; dc <= c + 1; dc++) {
if (dc < 0 || dc >= cells[dr].length) continue;
if (cells[dr][dc].getContents() == MineCell.MINE) nMines++;
} } cells[r][c].resetContents(nMines);
} } } } You can see that the nested for loops iterate through all the cells and if the cell doesn’t have a mine in it, it counts the number of mines that surround it The dr
and dc variables loop on the cells that are adjacent to this cell (a 3-by-3 cell grid area, actually) Every time it encounters a mine, it increments nMines , and then
it sets the contents of this cell (the center cell of the 3-by-3 grid) to nMines The
continue statements return control to the beginning of the innermost loop in
467
i n