/*
Title: J2ME Games With MIDP2
Authors: Carol Hamer
Publisher: Apress
ISBN: 1590593820
*/
import java.util.Vector;
import java.io.*;
import javax.microedition.io.*;
import javax.microedition.rms.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
/**
* This is the main class of the checkers game.
*
* @author Carol Hamer
*/
public class Checkers extends MIDlet implements CommandListener {
//-----------------------------------------------------
// game object fields
/**
* The canvas that the checkerboard is drawn on.
*/
private CheckersCanvas myCanvas;
/**
* The class that makes the http connection.
*/
private Communicator myCommunicator;
//-----------------------------------------------------
// command fields
/**
* The button to exit the game.
*/
private Command myExitCommand = new Command("Exit", Command.EXIT, 99);
//-----------------------------------------------------
// initialization and game state changes
/**
* Initialize the canvas and the commands.
*/
public Checkers() {
try {
//create the canvas and set up the commands:
myCanvas = new CheckersCanvas(Display.getDisplay(this));
myCanvas.addCommand(myExitCommand);
myCanvas.setCommandListener(this);
CheckersGame game = myCanvas.getGame();
myCommunicator = new Communicator(this, myCanvas, game);
game.setCommunicator(myCommunicator);
} catch(Exception e) {
// if there's an error during creation, display it as an alert.
errorMsg(e);
}
}
//----------------------------------------------------------------
// implementation of MIDlet
// these methods may be called by the application management
// software at any time, so we always check fields for null
// before calling methods on them.
/**
* Start the application.
*/
public void startApp() throws MIDletStateChangeException {
// tell the canvas to set up the game data and paint the
// checkerboard.
if(myCanvas != null) {
myCanvas.start();
}
// tell the communicator to start its thread and make a
// connection.
if(myCommunicator != null) {
myCommunicator.start();
}
}
/**
* Throw out the garbage.
*/
public void destroyApp(boolean unconditional)
throws MIDletStateChangeException {
// tell the communicator to send the end game
// message to the other player and then disconnect:
if(myCommunicator != null) {
myCommunicator.endGame();
}
// throw the larger game objects in the garbage:
myCommunicator = null;
myCanvas = null;
System.gc();
}
/**
* Pause the game.
* This method merely ends the game because this
* version of the Checkers game does not support
* re-entering a game that is in play. A possible
* improvement to the game would be to allow
* a player to diconeect and leave a game and then
* later return to it, using some sort of session
* token to find the correct game in progress on
* the server side.
*/
public void pauseApp() {
try {
destroyApp(false);
notifyDestroyed();
} catch (MIDletStateChangeException ex) {
}
}
//----------------------------------------------------------------
// implementation of CommandListener
/*
* Respond to a command issued on the Canvas.
*/
public void commandAction(Command c, Displayable s) {
if(c == myExitCommand) {
try {
destroyApp(false);
notifyDestroyed();
} catch (MIDletStateChangeException ex) {
}
}
}
//-------------------------------------------------------
// error methods
/**
* Converts an exception to a message and displays
* the message..
*/
void errorMsg(Exception e) {
e.printStackTrace();
if(e.getMessage() == null) {
errorMsg(e.getClass().getName());
} else {
errorMsg(e.getMessage());
}
}
/**
* Displays an error message alert if something goes wrong.
*/
void errorMsg(String msg) {
Alert errorAlert = new Alert("error",
msg, null, AlertType.ERROR);
errorAlert.setCommandListener(this);
errorAlert.setTimeout(Alert.FOREVER);
Display.getDisplay(this).setCurrent(errorAlert);
}
}
/**
* This class is the display of the game.
*
* @author Carol Hamer
*/
class CheckersCanvas extends Canvas {
//---------------------------------------------------------
// static fields
/**
* color constant
*/
public static final int BLACK = 0;
/**
* color constant
*/
public static final int WHITE = 0xffffff;
/**
* color constant.
* (not quite bright red)
*/
public static final int RED = 0xf96868;
/**
* color constant
*/
public static final int GREY = 0xc6c6c6;
/**
* color constant
*/
public static final int LT_GREY = 0xe5e3e3;
/**
* how many rows and columns the display is divided into.
*/
public static final int GRID_WIDTH = 8;
//---------------------------------------------------------
// instance fields
/**
* The black crown to draw on the red pieces..
*/
private Image myBlackCrown;
/**
* The red crown to draw on the black pieces..
*/
private Image myWhiteCrown;
/**
* a handle to the display.
*/
private Display myDisplay;
/**
* a handle to the object that stores the game logic
* and game data.
*/
private CheckersGame myGame;
/**
* checkers dimension: the width of the squares of the checkerboard.
*/
private int mySquareSize;
/**
* checkers dimension: the minimum width possible for the
* checkerboard squares.
*/
private int myMinSquareSize = 15;
/**
* whether or not we're waiting for another player to join
* the game.
*/
private boolean myIsWaiting;
//-----------------------------------------------------
// gets / sets
/**
* @return a handle to the class that holds the logic of the
* checkers game.
*/
CheckersGame getGame() {
return(myGame);
}
/**
* Display a screen to inform the player that we're
* waiting for another player.
*/
void setWaitScreen(boolean wait) {
myIsWaiting = wait;
}
//-----------------------------------------------------
// initialization and game state changes
/**
* Constructor performs size calculations.
* @throws Exception if the display size is too
* small to make a checkers.
*/
CheckersCanvas(Display d) throws Exception {
myDisplay = d;
myGame = new CheckersGame();
// a few calculations to make the right checkerboard
// for the current display.
int width = getWidth();
int height = getHeight();
// get the smaller dimension fo the two possible
// screen dimensions in order to determine how
// big to make the checkerboard.
int screenSquareWidth = height;
if(width < height) {
screenSquareWidth = width;
}
mySquareSize = screenSquareWidth / GRID_WIDTH;
// if the display is too small to make a reasonable checkerboard,
// then we throw an Exception
if(mySquareSize < myMinSquareSize) {
throw(new Exception("Display too small"));
}
// initialize the crown images:
myBlackCrown = Image.createImage("/blackCrown.png");
myWhiteCrown = Image.createImage("/whiteCrown.png");
}
/**
* This is called as soon as the application begins.
*/
void start() {
myDisplay.setCurrent(this);
// prepare the game data for the first move:
myGame.start();
}
//-------------------------------------------------------
// graphics methods
/**
* Repaint the checkerboard..
*/
protected void paint(Graphics g) {
int width = getWidth();
int height = getHeight();
g.setColor(WHITE);
// clear the board (including the region around
// the board, which can get menu stuff and other
// garbage painted onto it...)
g.fillRect(0, 0, width, height);
// If we need to wait for another player to join the
// game before we can start, this displays the appropriate
// message:
if(myIsWaiting) {
// perform some calculations to place the text correctly:
Font font = g.getFont();
int fontHeight = font.getHeight();
int fontWidth = font.stringWidth("waiting for another player");
g.setColor(WHITE);
g.fillRect((width - fontWidth)/2, (height - fontHeight)/2,
fontWidth + 2, fontHeight);
// write in black
g.setColor(BLACK);
g.setFont(font);
g.drawString("waiting for another player", (width - fontWidth)/2,
(height - fontHeight)/2,
g.TOP|g.LEFT);
return;
}
// now draw the checkerboard:
// first the dark squares:
byte offset = 0;
for(byte i = 0; i < 4; i++) {
for(byte j = 0; j < 8; j++) {
// the offset is used to handle the fact that in every
// other row the dark squares are shifted one place
// to the right.
if(j % 2 != 0) {
offset = 1;
} else {
offset = 0;
}
// now if this is a selected square, we draw it lighter:
if(myGame.isSelected(i, j)) {
g.setColor(LT_GREY);
g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize,
mySquareSize, mySquareSize);
} else {
// if it's not selected, we draw it dark grey:
g.setColor(GREY);
g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize,
mySquareSize, mySquareSize);
}
// now put the pieces in their places:
g.setColor(RED);
int piece = myGame.getPiece(i, j);
int circleOffset = 2;
int circleSize = mySquareSize - 2*circleOffset;
if(piece < 0) {
// color the piece in black
g.setColor(BLACK);
g.fillRoundRect((2*i + offset)*mySquareSize + circleOffset,
j*mySquareSize + circleOffset,
circleSize, circleSize, circleSize, circleSize);
// if the player is a king, draw a crown on:
if(piece < -1) {
g.drawImage(myWhiteCrown,
(2*i + offset)*mySquareSize + mySquareSize/2,
j*mySquareSize + 1 + mySquareSize/2,
Graphics.VCENTER|Graphics.HCENTER);
}
} else if(piece > 0) {
// color the piece in red
g.fillRoundRect((2*i + offset)*mySquareSize + circleOffset,
j*mySquareSize + circleOffset,
circleSize, circleSize, circleSize, circleSize);
// if the player is a king, draw a crown on:
if(piece > 1) {
g.drawImage(myBlackCrown,
(2*i + offset)*mySquareSize + mySquareSize/2,
j*mySquareSize + 1 + mySquareSize/2,
Graphics.VCENTER|Graphics.HCENTER);
}
}
}
}
// now the blank squares:
// actually, this part is probably not necessary...
g.setColor(WHITE);
for(int i = 0; i < 4; i++) {
for(int j = 0; j < 8; j++) {
if(j % 2 == 0) {
offset = 1;
} else {
offset = 0;
}
g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize,
mySquareSize, mySquareSize);
}
}
// if the player has reached the end of the game,
// we display the end message.
if(myGame.getGameOver()) {
// perform some calculations to place the text correctly:
Font font = g.getFont();
int fontHeight = font.getHeight();
int fontWidth = font.stringWidth("Game Over");
g.setColor(WHITE);
g.fillRect((width - fontWidth)/2, (height - fontHeight)/2,
fontWidth + 2, fontHeight);
// write in black
g.setColor(BLACK);
g.setFont(font);
g.drawString("Game Over", (width - fontWidth)/2,
(height - fontHeight)/2,
g.TOP|g.LEFT);
}
}
//-------------------------------------------------------
// handle keystrokes
/**
* Move the player.
*/
public void keyPressed(int keyCode) {
if(myGame.isMyTurn()) {
int action = getGameAction(keyCode);
switch (action) {
case LEFT:
myGame.leftPressed();
break;
case RIGHT:
myGame.rightPressed();
break;
case UP:
myGame.upPressed();
break;
case DOWN:
myGame.deselect();
break;
}
repaint();
serviceRepaints();
}
}
}
/**
* This class contacts a remote server in order to
* play a game of checkers against an opponent..
*
* @author Carol Hamer
*/
class Communicator extends Thread {
//--------------------------------------------------------
// static fields
/**
* This is the URL to contact.
* IMPORTANT: before compiling, the following URL
* must be changed to the correct URL of the
* machine running the server code.
*/
public static final String SERVER_URL
= "socket://malbec:8007";
/**
* The int to signal that the game is to begin.
*/
public static final byte START_GAME_FLAG = -4;
/**
* The byte to signal that the game is to end.
*/
public static final byte END_GAME_FLAG = -3;
/**
* The byte to signal the end of a turn.
*/
public static final byte END_TURN_FLAG = -2;
//--------------------------------------------------------
// game instance fields
/**
* The MIDlet subclass, used to set the Display
* in the case where an error message needs to be sent..
*/
private Checkers myCheckers;
/**
* The Canvas subclass, used to set the Display
* in the case where an error message needs to be sent..
*/
private CheckersCanvas myCanvas;
/**
* The game logic class that we send the opponent's
* moves to..
*/
private CheckersGame myGame;
/**
* Whether or not the MIDlet class has requested the
* game to end.
*/
private boolean myShouldStop;
//--------------------------------------------------------
// data exchange instance fields
/**
* The data from the local player that is to
* be sent to the opponent.
*/
private byte[] myMove;
/**
* Whether or not the current turn is done and
* should be sent.
*/
private boolean myTurnIsDone = true;
//--------------------------------------------------------
// initialization
/**
* Constructor is used only when the program wants
* to spawn a data-fetching thread, not for merely
* reading local data with static methods.
*/
Communicator(Checkers checkers, CheckersCanvas canvas,
CheckersGame game) {
myCheckers = checkers;
myCanvas = canvas;
myGame = game;
}
//--------------------------------------------------------
// methods called by CheckersGame to send move
// information to the opponent.
/**
* Stop the game entirely. Notify the servlet that
* the user is exiting the game.
*/
synchronized void endGame() {
myShouldStop = true;
if(myGame != null) {
myGame.setGameOver();
}
notify();
}
/**
* This is called when the player moves a piece.
*/
synchronized void move(byte sourceX, byte sourceY, byte destinationX,
byte destinationY) {
myMove = new byte[4];
myMove[0] = sourceX;
myMove[1] = sourceY;
myMove[2] = destinationX;
myMove[3] = destinationY;
myTurnIsDone = false;
notify();
}
/**
* This is called when the local player's turn is over.
*/
synchronized void endTurn() {
myTurnIsDone = true;
notify();
}
//--------------------------------------------------------
// main communication method
/**
* Makes a connection to the server and sends and receives
* information about moves.
*/
public void run() {
DataInputStream dis = null;
DataOutputStream dos = null;
SocketConnection conn = null;
byte[] fourBytes = new byte[4];
try {
// tell the user that we're waiting for the other player to join:
myCanvas.setWaitScreen(true);
myCanvas.repaint();
myCanvas.serviceRepaints();
// now make the connection:
conn = (SocketConnection)Connector.open(SERVER_URL);
conn.setSocketOption(SocketConnection.KEEPALIVE, 1);
dos = conn.openDataOutputStream();
dis = conn.openDataInputStream();
// we read four bytes to make sure the connection works...
dis.readFully(fourBytes);
if(fourBytes[0] != START_GAME_FLAG) {
throw(new Exception("server-side error"));
}
// On this line it will block waiting for another
// player to join the game or make a move:
dis.readFully(fourBytes);
// if the server sends the start game flag again,
// that means that we start with the local player's turn.
// Otherwise, we read the other player's first move from the
// stream:
if(fourBytes[0] != START_GAME_FLAG) {
// verify that the other player sent a move
// and not just a message ending the game...
if(fourBytes[0] == END_GAME_FLAG) {
throw(new Exception("other player quit"));
}
// we move the opponent on the local screen.
// then we read from the opponent again,
// in case there's a double-jump:
while(fourBytes[0] != END_TURN_FLAG) {
myGame.moveOpponent(fourBytes);
dis.readFully(fourBytes);
}
}
// now signal the local game that the opponent is done
// so the board must be updated and the local player
// prompted to make a move:
myGame.endOpponentTurn();
myCanvas.setWaitScreen(false);
myCanvas.repaint();
myCanvas.serviceRepaints();
// begin main game loop:
while(! myShouldStop) {
// now it's the local player's turn.
// wait for the player to move a piece:
synchronized(this) {
wait();
}
// after every wait, we check if the game
// ended while we were waiting...
if(myShouldStop) {
break;
}
while(! myTurnIsDone) {
// send the current move:
if(myMove != null) {
dos.write(myMove, 0, myMove.length);
myMove = null;
}
// If the player can continue the move with a double
// jump, we wait for the player to do it:
synchronized(this) {
// make sure the turn isn't done before we start waiting
// (the end turn notify might accidentally be called
// before we start waiting...)
if(! myTurnIsDone) {
wait();
}
}
}
// after every wait, we check if the game
// ended while we were waiting...
if(myShouldStop) {
break;
}
// now we tell the other player the this player's
// turn is over:
fourBytes[0] = END_TURN_FLAG;
dos.write(fourBytes, 0, fourBytes.length);
// now that we've sent the move, we wait for a response:
dis.readFully(fourBytes);
while((fourBytes[0] != END_TURN_FLAG) &&
(fourBytes[0] != END_GAME_FLAG) && (!myShouldStop)) {
// we move the opponent on the local screen.
// then we read from the opponent again,
// in case there's a double-jump:
myGame.moveOpponent(fourBytes);
dis.readFully(fourBytes);
}
// if the other player has left the game, we tell the
// local user that the game is over.
if((fourBytes[0] == END_GAME_FLAG) || (myShouldStop)) {
endGame();
break;
}
myGame.endOpponentTurn();
myCanvas.repaint();
myCanvas.serviceRepaints();
} // end while loop
} catch(Exception e) {
// if there's an error, we display its messsage and
// end the game.
myCheckers.errorMsg(e.getMessage());
} finally {
// now we send the information that we're leaving the game,
// then close up and delete everything.
try {
if(dos != null) {
dos.write(END_GAME_FLAG);
dos.close();
}
if(dis != null) {
dis.close();
}
if(conn != null) {
conn.close();
}
dis = null;
dos = null;
conn = null;
} catch(Exception e) {
// if this throws, at least we made our best effort
// to close everything up....
}
}
// one last paint job to display the "Game Over"
myCanvas.repaint();
myCanvas.serviceRepaints();
}
}
/**
* This class is a set of simple utility functions that
* can be used to convert standard data types to bytes
* and back again. It is used especially for data storage,
* but also for sending and receiving data.
*
* @author Carol Hamer
*/
class DataConverter {
//--------------------------------------------------------
// utilities to encode small, compactly-stored small ints.
/**
* Encodes a coordinate pair into a byte.
* @param coordPair a pair of integers to be compacted into
* a single byte for storage.
* WARNING: each of the two values MUST BE
* between 0 and 15 (inclusive). This method does not
* verify the length of the array (which must be 2!)
* nor does it verify that the ints are of the right size.
*/
public static byte encodeCoords(int[] coordPair) {
// get the byte value of the first coordinate:
byte retVal = (new Integer(coordPair[0])).byteValue();
// move the first coordinate's value up to the top
// half of the storage byte:
retVal = (new Integer(retVal << 4)).byteValue();
// store the second coordinate in the lower half
// of the byte:
retVal += (new Integer(coordPair[1])).byteValue();
return(retVal);
}
/**
* Encodes eight ints into a byte.
* This could be easily modified to encode eight booleans.
* @param eight an array of at least eight ints.
* WARNING: all values must be 0 or 1! This method does
* not verify that the values are in the correct range
* nor does it verify that the array is long enough.
* @param offset the index in the array eight to start
* reading data from. (should usually be 0)
*/
public static byte encode8(int[] eight, int offset) {
// get the byte value of the first int:
byte retVal = (new Integer(eight[offset])).byteValue();
// progressively move the data up one bit in the
// storage byte and then record the next int in
// the lowest spot in the storage byte:
for(int i = offset + 1; i < 8 + offset; i++) {
retVal = (new Integer(retVal << 1)).byteValue();
retVal += (new Integer(eight[i])).byteValue();
}
return(retVal);
}
//--------------------------------------------------------
// utilities to decode small, compactly-stored small ints.
/**
* Turns a byte into a pair of coordinates.
*/
public static int[] decodeCoords(byte coordByte) {
int[] retArray = new int[2];
// we perform a bitwise and with the value 15
// in order to just get the bits of the lower
// half of the byte:
retArray[1] = coordByte & 15;
// To get the bits of the upper half of the
// byte, we perform a shift to move them down:
retArray[0] = coordByte >> 4;
// bytes in Java are generally assumed to be
// signed, but in this coding algorithm we
// would like to treat them as unsigned:
if(retArray[0] < 0) {
retArray[0] += 16;
}
return(retArray);
}
/**
* Turns a byte into eight ints.
*/
public static int[] decode8(byte data) {
int[] retArray = new int[8];
// The flag allows us to look at each bit individually
// to determine if it is 1 or 0. The number 128
// corresponds to the highest bit of a byte, so we
// start with that one.
int flag = 128;
// We use a loop that checks
// the data bit by bit by performing a bitwise
// and (&) between the data byte and a flag:
for(int i = 0; i < 8; i++) {
if((flag & data) != 0) {
retArray[i] = 1;
} else {
retArray[i] = 0;
}
// move the flag down one bit so that we can
// check the next bit of data on the next pass
// through the loop:
flag = flag >> 1;
}
return(retArray);
}
//--------------------------------------------------------
// standard integer interpretation
/**
* Uses an input stream to convert an array of bytes to an int.
*/
public static int parseInt(byte[] data) throws IOException {
DataInputStream stream
= new DataInputStream(new ByteArrayInputStream(data));
int retVal = stream.readInt();
stream.close();
return(retVal);
}
/**
* Uses an output stream to convert an int to four bytes.
*/
public static byte[] intToFourBytes(int i) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(4);
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(i);
baos.close();
dos.close();
byte[] retArray = baos.toByteArray();
return(retArray);
}
//--------------------------------------------------------
// integer interpretation illustrated
/**
* Java appears to treat a byte as being signed when
* returning it as an int--this function converts from
* the signed value to the corresponding unsigned value.
* This method is used by nostreamParseInt.
*/
public static int unsign(int signed) {
int retVal = signed;
if(retVal < 0) {
retVal += 256;
}
return(retVal);
}
/**
* Takes an array of bytes and returns an int.
* This version will return the same value as the
* method parseInt above. This version is included
* in order to illustrate how Java encodes int values
* in terms of bytes.
* @param data an array of 1, 2, or 4 bytes.
*/
public static int nostreamParseInt(byte[] data) {
// byte 0 is the high byte which is assumed
// to be signed. As we add the lower bytes
// one by one, we unsign them because because
// a single byte alone is interpreted as signed,
// but in an int only the top byte should be signed.
// (note that the high byte is the first one in the array)
int retVal = data[0];
for(int i = 1; i < data.length; i++) {
retVal = retVal << 8;
retVal += unsign(data[i]);
}
return(retVal);
}
/**
* Takes an arbitrary int and returns
* an array of four bytes.
* This version will return the same byte array
* as the method intToFourBytes above. This version
* is included in order to illustrate how Java encodes
* int values in terms of bytes.
*/
public static byte[] nostreamIntToFourBytes(int i) {
byte[] fourBytes = new byte[4];
// when you take the byte value of an int, it
// only gives you the lowest byte. So we
// get all four bytes by taking the lowest
// byte four times and moving the whole int
// down by one byte between each one.
// (note that the high byte is the first one in the array)
fourBytes[3] = (new Integer(i)).byteValue();
i = i >> 8;
fourBytes[2] = (new Integer(i)).byteValue();
i = i >> 8;
fourBytes[1] = (new Integer(i)).byteValue();
i = i >> 8;
fourBytes[0] = (new Integer(i)).byteValue();
return(fourBytes);
}
/**
* Takes an int between -32768 and 32767 and returns
* an array of two bytes. This does not verify that
* the argument is of the right size. If the absolute
* value of i is too high, it will not be encoded
* correctly.
*/
public static byte[] nostreamIntToTwoBytes(int i) {
byte[] twoBytes = new byte[2];
// when you take the byte value of an int, it
// only gives you the lowest byte. So we
// get the lower two bytes by taking the lowest
// byte twice and moving the whole int
// down by one byte between each one.
twoBytes[1] = (new Integer(i)).byteValue();
i = i >> 8;
twoBytes[0] = (new Integer(i)).byteValue();
return(twoBytes);
}
}
/**
* This class takes care of the underlying logic and data of
* the checkers game being played. That includes where
* all of the pieces are on the board and where it is okay
* for them to move to.
*
* @author Carol Hamer
*/
class CheckersGame {
//-------------------------------------------------------
// static fields
/**
* The length of the checkerboard in the x-direction.
*/
public static final byte X_LENGTH = 4;
/**
* The length of the checkerboard in the y-direction.
*/
public static final byte Y_LENGTH = 8;
//-------------------------------------------------------
// instance fields
/**
* a handle to the communications class that exchanges
* data with the server.
*/
private Communicator myCommunicator;
/**
* This array represents the black squares of the
* checkerboard. The two dimensions of the array
* represent the two dimensions of the checkerboard.
* The value represents what type of piece is on
* the square.
* 0 = empty
* 1 = local player's piece
* 2 = local player's king
* -1 = remote player's piece
* -2 = remote player's king
*/
private byte[][] myGrid;
/**
* If the user has currently selected a piece to move,
* this is its X grid coordinate. (-1 if none selected)
*/
private byte mySelectedX = -1;
/**
* If the user has currently selected a piece to move,
* this is its Y grid coordinate.(-1 if none selected)
*/
private byte mySelectedY = -1;
/**
* If the user has currently selected a possible
* destination square for a move, this is its X coordinate..
* (-1 if none selected)
*/
private byte myDestinationX = -1;
/**
* If the user has currently selected a possible
* destination square for a move, this is its Y coordinate..
* (-1 if none selected)
*/
private byte myDestinationY = -1;
/**
* This Vector contains the coordinates of all of the
* squares that the player could currently move to.
*/
private Vector myPossibleMoves = new Vector(4);
/**
* Whether or not the currently displayed checkers has
* been completed.
*/
private boolean myGameOver = false;
/**
* Whether or not it is currently this player's turn.
*/
private boolean myTurn = false;
/**
* This is true if the player has just jumped and can
* jump again.
*/
private boolean myIsJumping = false;
//-------------------------------------------------------
// get/set data
/**
* get the piece on the given grid square.
*/
byte getPiece(byte x, byte y) {
return(myGrid[x][y]);
}
/**
* This is callsed by CheckersCanvas to determine if
* the square is currently selected (as containing
* a piece to move or a destination square).
*/
boolean isSelected(byte x, byte y) {
boolean retVal = false;
if((x == mySelectedX) && (y == mySelectedY)) {
retVal = true;
} else if((x == myDestinationX) && (y == myDestinationY)) {
retVal = true;
}
return(retVal);
}
/**
* This tells whether or not the keystrokes should currently
* be taken into account.
*/
boolean isMyTurn() {
boolean retVal = false;
if((!myGameOver) && ((myTurn) || (myIsJumping))) {
retVal = true;
}
return(retVal);
}
/**
* This tells whether or not the game has ended.
*/
boolean getGameOver() {
boolean retVal = false;
if(myGameOver) {
retVal = true;
}
return(retVal);
}
/**
* tell the CheckersGame that the other player has ended the game.
*/
void setGameOver() {
myGameOver = true;
}
/**
* set the communicator object.
*/
void setCommunicator(Communicator comm) {
myCommunicator = comm;
}
//-------------------------------------------------------
// initialization
/**
* Constructor puts the pieces in their initial positions:
*/
CheckersGame() {
myGrid = new byte[X_LENGTH][];
for(byte i = 0; i < myGrid.length; i++) {
myGrid[i] = new byte[Y_LENGTH];
for(byte j = 0; j < myGrid[i].length; j++) {
if(j < 3) {
// fill the top of the board with remote players
myGrid[i][j] = -1;
} else if(j > 4) {
// fill the bottom of the board with local players
myGrid[i][j] = 1;
}
}
}
}
/**
* This is called just before the player makes the
* first move.
*/
void start() {
mySelectedX = 0;
mySelectedY = 5;
myTurn = true;
getMoves(mySelectedX, mySelectedY, myPossibleMoves, false);
}
//-------------------------------------------------------
// move the opponent
// to be called by Communicator
/**
* This is called when the opponent wants to move
* its piece.
* @param moveData an array of four bytes:
* moveData[0] = opponent's initial X coordinate
* moveData[1] = opponent's initial Y coordinate
* moveData[2] = opponent's destination X coordinate
* moveData[3] = opponent's destination Y coordinate
*/
void moveOpponent(byte[] moveData) {
// since both players appear on their own screens
// as the red side (bottom of the screen), we need
// to invert the opponent's move:
moveData[0] = (new Integer(X_LENGTH - moveData[0] - 1)).byteValue();
moveData[2] = (new Integer(X_LENGTH - moveData[2] - 1)).byteValue();
moveData[1] = (new Integer(Y_LENGTH - moveData[1] - 1)).byteValue();
moveData[3] = (new Integer(Y_LENGTH - moveData[3] - 1)).byteValue();
myGrid[moveData[2]][moveData[3]]
= myGrid[moveData[0]][moveData[1]];
myGrid[moveData[0]][moveData[1]] = 0;
// deal with an opponent's jump:
if((moveData[1] - moveData[3] > 1) ||
(moveData[3] - moveData[1] > 1)) {
int jumpedY = (moveData[1] + moveData[3])/2;
int jumpedX = moveData[0];
int parity = moveData[1] % 2;
if((parity > 0) && (moveData[2] > moveData[0])) {
jumpedX++;
} else if((parity == 0) && (moveData[0] > moveData[2])) {
jumpedX--;
}
myGrid[jumpedX][jumpedY] = 0;
}
// if the opponent reaches the far side,
// make him a king:
if(moveData[3] == Y_LENGTH - 1) {
myGrid[moveData[2]][moveData[3]] = -2;
}
}
/**
* This is called when the opponent's turn is over.
* Note that the turn doesn't automatically end after
* the opponent moves because the opponent may make
* a double or triple jump.
*/
void endOpponentTurn() {
myTurn = true;
// Now begin the local player's turn:
// First select the first local piece that can be
// moved. (rightPressed will select an appropriate
// piece or end the game if the local player has
// no possible moves to make)
mySelectedX = 0;
mySelectedY = 0;
myDestinationX = -1;
myDestinationY = -1;
rightPressed();
// the local player's thread has been waiting
// for the opponent's turn to end.
synchronized(this) {
notify();
}
}
//-------------------------------------------------------
// handle keystrokes
// to be called by CheckersCanvas
/**
* if the left button is pressed, this method takes
* the correct course of action depending on the situation.
*/
void leftPressed() {
// in the first case the user has not yet selected a
// piece to move:
if(myDestinationX == -1) {
// find the next possible piece (to the left)
// that can move:
selectPrevious();
// if selectPrevious fails to fill myPossibleMoves, that
// means that the local player cannot move, so the game
// is over:
if(myPossibleMoves.size() == 0) {
myCommunicator.endGame();
}
} else {
// if the user has already selected a piece to move,
// we give the options of where the piece can move to:
for(byte i = 0; i < myPossibleMoves.size(); i++) {
byte[] coordinates = (byte[])myPossibleMoves.elementAt(i);
if((coordinates[0] == myDestinationX) &&
(coordinates[1] == myDestinationY)) {
i++;
i = (new Integer(i % myPossibleMoves.size())).byteValue();
coordinates = (byte[])myPossibleMoves.elementAt(i);
myDestinationX = coordinates[0];
myDestinationY = coordinates[1];
break;
}
}
}
}
/**
* if the left button is pressed, this method takes
* the correct course of action depending on the situation.
*/
void rightPressed() {
// in the first case the user has not yet selected a
// piece to move:
if(myDestinationX == -1) {
// find the next possible piece that can
// move:
selectNext();
// if selectNext fails to fill myPossibleMoves, that
// means that the local player cannot move, so the game
// is over:
if(myPossibleMoves.size() == 0) {
myCommunicator.endGame();
}
} else {
// if the user has already selected a piece to move,
// we give the options of where the piece can move to:
for(byte i = 0; i < myPossibleMoves.size(); i++) {
byte[] coordinates = (byte[])myPossibleMoves.elementAt(i);
if((coordinates[0] == myDestinationX) &&
(coordinates[1] == myDestinationY)) {
i++;
i = (new Integer(i % myPossibleMoves.size())).byteValue();
coordinates = (byte[])myPossibleMoves.elementAt(i);
myDestinationX = coordinates[0];
myDestinationY = coordinates[1];
break;
}
}
}
}
/**
* If no piece is selected, we select one. If a piece
* is selected, we move it.
*/
void upPressed() {
// in the first case the user has not yet selected a
// piece to move:
if(myDestinationX == -1) {
fixSelection();
} else {
// if the source square and destination square
// have been chosen, we move the piece:
move();
}
}
/**
* If the user decided not to move the selected piece
* (and instead wants to select again), this undoes
* the selection. This corresponds to pressing the
* DOWN key.
*/
void deselect() {
// if the player has just completed a jump and
// could possibly jump again but decides not to
// (i.e. deselects), then the turn ends:
if(myIsJumping) {
mySelectedX = -1;
mySelectedY = -1;
myDestinationX = -1;
myDestinationY = -1;
myIsJumping = false;
myTurn = false;
myCommunicator.endTurn();
} else {
// setting the destination coordinates to -1
// is the signal that the the choice of which
// piece to move can be modified:
myDestinationX = -1;
myDestinationY = -1;
}
}
//-------------------------------------------------------
// internal square selection methods
/**
* When the player has decided that the currently selected
* square contains the piece he really wants to move, this
* is called. This method switches to the mode where
* the player selects the destination square of the move.
*/
private void fixSelection() {
byte[] destination = (byte[])myPossibleMoves.elementAt(0);
// setting the destination coordinates to valid
// coordinates is the signal that the user is done
// selecting the piece to move and now is choosing
// the destination square:
myDestinationX = destination[0];
myDestinationY = destination[1];
}
/**
* This method starts from the currently selected square
* and finds the next square that contains a piece that
* the player can move.
*/
private void selectNext() {
// Test the squares one by one (starting from the
// currently selected square) until we find a square
// that contains one of the local player's pieces
// that can move:
byte testX = mySelectedX;
byte testY = mySelectedY;
while(true) {
testX++;
if(testX >= X_LENGTH) {
testX = 0;
testY++;
testY = (new Integer(testY % Y_LENGTH)).byteValue();
}
getMoves(testX, testY, myPossibleMoves, false);
if((myPossibleMoves.size() != 0) ||
((testX == mySelectedX) && (testY == mySelectedY))) {
mySelectedX = testX;
mySelectedY = testY;
break;
}
}
}
/**
* This method starts from the currently selected square
* and finds the next square (to the left) that contains
* a piece that the player can move.
*/
private void selectPrevious() {
// Test the squares one by one (starting from the
// currently selected square) until we find a square
// that contains one of the local player's pieces
// that can move:
byte testX = mySelectedX;
byte testY = mySelectedY;
while(true) {
testX--;
if(testX < 0) {
testX += X_LENGTH;
testY--;
if(testY < 0) {
testY += Y_LENGTH;
}
}
getMoves(testX, testY, myPossibleMoves, false);
if((myPossibleMoves.size() != 0) ||
((testX == mySelectedX) && (testY == mySelectedY))) {
mySelectedX = testX;
mySelectedY = testY;
break;
}
}
}
//-------------------------------------------------------
// internal utilities
/**
* Once the user has selected the move to make, this
* updates the data accordingly.
*/
private void move() {
// the piece that was on the source square is
// now on the destination square:
myGrid[myDestinationX][myDestinationY]
= myGrid[mySelectedX][mySelectedY];
// the source square is emptied:
myGrid[mySelectedX][mySelectedY] = 0;
if(myDestinationY == 0) {
myGrid[myDestinationX][myDestinationY] = 2;
}
// tell the communicator to inform the other player
// of this move:
myCommunicator.move(mySelectedX, mySelectedY,
myDestinationX, myDestinationY);
// deal with the special rules for jumps::
if((mySelectedY - myDestinationY > 1) ||
(myDestinationY - mySelectedY > 1)) {
int jumpedY = (mySelectedY + myDestinationY)/2;
int jumpedX = mySelectedX;
int parity = mySelectedY % 2;
// the coordinates of the jumped square depend on
// what row we're in:
if((parity > 0) && (myDestinationX > mySelectedX)) {
jumpedX++;
} else if((parity == 0) && (mySelectedX > myDestinationX)) {
jumpedX--;
}
// remove the piece that was jumped over:
myGrid[jumpedX][jumpedY] = 0;
// now get ready to jump again if possible:
mySelectedX = myDestinationX;
mySelectedY = myDestinationY;
myDestinationX = -1;
myDestinationY = -1;
// see if another jump is possible.
// The "true" argument tells the program to return
// only jumps because the player can go again ONLY
// if there's a jump:
getMoves(mySelectedX, mySelectedY, myPossibleMoves, true);
// if there's another jump possible with the same piece,
// allow the player to continue jumping:
if(myPossibleMoves.size() != 0) {
myIsJumping = true;
byte[] landing = (byte[])myPossibleMoves.elementAt(0);
myDestinationX = landing[0];
myDestinationY = landing[1];
} else {
myTurn = false;
myCommunicator.endTurn();
}
} else {
// since it's not a jump, we just end the turn
// by deselecting everything.
mySelectedX = -1;
mySelectedY = -1;
myDestinationX = -1;
myDestinationY = -1;
myPossibleMoves.removeAllElements();
myTurn = false;
// tell the other player we're done:
myCommunicator.endTurn();
}
}
/**
* Given a square on the grid, get the coordinates
* of one of the adjoining (diagonal) squares.
* 0 = top left
* 1 = top right
* 2 = bottom left
* 3 = bottom right.
* @return the coordinates or null if the desired corner
* is off the board.
*/
private byte[] getCornerCoordinates(byte x, byte y, byte corner) {
byte[] retArray = null;
if(corner < 2) {
y--;
} else {
y++;
}
// Where the corner is on the grid depends on
// whether this is an odd row or an even row:
if((corner % 2 == 0) && (y % 2 != 0)) {
x--;
} else if((corner % 2 != 0) && (y % 2 == 0)) {
x++;
}
try {
if(myGrid[x][y] > -15) {
// we don't really care about the value, this
// if statement is just there to get it to
// throw if the coordinates aren't on the board.
retArray = new byte[2];
retArray[0] = x;
retArray[1] = y;
}
} catch(ArrayIndexOutOfBoundsException e) {
// this throws if the coordinates do not correspond
// to a square on the board. It's not a problem,
// so we do nothing--we just return null instead
// of returning coordinates since no valid
// coordinates correspond to the desired corner.
}
return(retArray);
}
/**
* Determines where the piece in the given
* grid location can move. Clears the Vector
* and fills it with the locations that
* the piece can move to.
* @param jumpsOnly if we should return only moves that
* are jumps.
*/
private void getMoves(byte x, byte y, Vector toFill, boolean jumpsOnly) {
toFill.removeAllElements();
// if the square does not contain one of the local player's
// pieces, then there are no corresponding moves and we just
// return an empty vector.
if(myGrid[x][y] <= 0) {
return;
}
// check each of the four corners to see if the
// piece can move there:
for(byte i = 0; i < 4; i++) {
byte[] coordinates = getCornerCoordinates(x, y, i);
// if the coordinate array is null, then the corresponding
// corner is off the board and we don't deal with it.
// The later two conditions in the following if statement
// ensure that either the move is a forward move or the
// current piece is a king:
if((coordinates != null) &&
((myGrid[x][y] > 1) || (i < 2))) {
// if the corner is empty (and we're not looking
// for just jumps), then this is a possible move
// so we add it to the vector of moves:
if((myGrid[coordinates[0]][coordinates[1]] == 0) && (! jumpsOnly)) {
toFill.addElement(coordinates);
// if the space is occupied by an opponent, see if we can jump it:
} else if(myGrid[coordinates[0]][coordinates[1]] < 0) {
byte[] jumpLanding = getCornerCoordinates(coordinates[0],
coordinates[1], i);
// if the space on the far side of the opponent's piece
// is on the board and is unoccupied, then a jump
// is possible, so we add it to the vector of moves:
if((jumpLanding != null) &&
(myGrid[jumpLanding[0]][jumpLanding[1]] == 0)) {
toFill.addElement(jumpLanding);
}
}
}
} // end for loop
}
}
|