Dungeon game : Game « J2ME « Java






Dungeon game

/*
 Title:  J2ME Games With MIDP2
 Authors:  Carol Hamer
 Publisher:  Apress
 ISBN:   1590593820
 */

import java.io.*;

import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;

/**
 * This is the main class of the dungeon game.
 * 
 * @author Carol Hamer
 */
public class Dungeon extends MIDlet implements CommandListener {

  //-----------------------------------------------------
  //    game object fields

  /**
   * The canvas that the dungeon is drawn on.
   */
  private DungeonCanvas myCanvas;

  /**
   * the thread that advances the game clock.
   */
  private GameThread myGameThread;

  //-----------------------------------------------------
  //    command fields

  /**
   * The button to exit the game.
   */
  private Command myExitCommand = new Command("Exit", Command.EXIT, 99);

  /**
   * The command to save the game in progress.
   */
  private Command mySaveCommand = new Command("Save Game", Command.SCREEN, 2);

  /**
   * The command to restore a previously saved game.
   */
  private Command myRestoreCommand = new Command("Restore Game",
      Command.SCREEN, 2);

  /**
   * the command to start moving when the game is paused.
   */
  private Command myGoCommand = new Command("Go", Command.SCREEN, 1);

  /**
   * the command to pause the game.
   */
  private Command myPauseCommand = new Command("Pause", Command.SCREEN, 1);

  /**
   * the command to start a new game.
   */
  private Command myNewCommand = new Command("Next Board", Command.SCREEN, 1);

  //-----------------------------------------------------
  //    initialization and game state changes

  /**
   * Initialize the canvas and the commands.
   */
  public Dungeon() {
    try {
      // create the canvas and set up the commands:
      myCanvas = new DungeonCanvas(this);
      myCanvas.addCommand(myExitCommand);
      myCanvas.addCommand(mySaveCommand);
      myCanvas.addCommand(myRestoreCommand);
      myCanvas.addCommand(myPauseCommand);
      myCanvas.setCommandListener(this);
    } catch (Exception e) {
      // if there's an error during creation, display it as an alert.
      errorMsg(e);
    }
  }

  /**
   * Switch the command to the play again command. (removing other commands
   * that are no longer relevant)
   */
  void setNewCommand() {
    myCanvas.removeCommand(myPauseCommand);
    myCanvas.removeCommand(myGoCommand);
    myCanvas.addCommand(myNewCommand);
  }

  /**
   * Switch the command to the go command. (removing other commands that are
   * no longer relevant)
   */
  void setGoCommand() {
    myCanvas.removeCommand(myPauseCommand);
    myCanvas.removeCommand(myNewCommand);
    myCanvas.addCommand(myGoCommand);
  }

  /**
   * Switch the command to the pause command. (removing other commands that
   * are no longer relevant)
   */
  void setPauseCommand() {
    myCanvas.removeCommand(myNewCommand);
    myCanvas.removeCommand(myGoCommand);
    myCanvas.addCommand(myPauseCommand);
  }

  //----------------------------------------------------------------
  //  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 {
    if (myCanvas != null) {
      if (myGameThread == null) {
        // create the thread and start the game:
        myGameThread = new GameThread(myCanvas);
        myCanvas.start();
        myGameThread.start();
      } else {
        // in case this gets called again after
        // the application has been started once:
        myCanvas.removeCommand(myGoCommand);
        myCanvas.addCommand(myPauseCommand);
        myCanvas.flushKeys();
        myGameThread.resumeGame();
      }
    }
  }

  /**
   * Stop the threads and throw out the garbage.
   */
  public void destroyApp(boolean unconditional)
      throws MIDletStateChangeException {
    myCanvas = null;
    if (myGameThread != null) {
      myGameThread.requestStop();
    }
    myGameThread = null;
    System.gc();
  }

  /**
   * Pause the game.
   */
  public void pauseApp() {
    if (myCanvas != null) {
      setGoCommand();
    }
    if (myGameThread != null) {
      myGameThread.pause();
    }
  }

  //----------------------------------------------------------------
  //  implementation of CommandListener

  /*
   * Respond to a command issued on the Canvas. (reset, exit, or change size
   * prefs).
   */
  public void commandAction(Command c, Displayable s) {
    try {
      if (c == myGoCommand) {
        myCanvas.setNeedsRepaint();
        myCanvas.removeCommand(myGoCommand);
        myCanvas.addCommand(myPauseCommand);
        myCanvas.flushKeys();
        myGameThread.resumeGame();
      } else if (c == myPauseCommand) {
        myCanvas.setNeedsRepaint();
        myCanvas.removeCommand(myPauseCommand);
        myCanvas.addCommand(myGoCommand);
        myGameThread.pause();
      } else if (c == myNewCommand) {
        myCanvas.setNeedsRepaint();
        // go to the next board and restart the game
        myCanvas.removeCommand(myNewCommand);
        myCanvas.addCommand(myPauseCommand);
        myCanvas.reset();
        myGameThread.resumeGame();
      /*} else if (c == Alert.DISMISS_COMMAND) {
        // if there was a serious enough error to
        // cause an alert, then we end the game
        // when the user is done reading the alert:
        // (Alert.DISMISS_COMMAND is the default
        // command that is placed on an Alert
        // whose timeout is FOREVER)
        destroyApp(false);
        notifyDestroyed();*/
      } else if (c == mySaveCommand) {
        myCanvas.setNeedsRepaint();
        myCanvas.saveGame();
      } else if (c == myRestoreCommand) {
        myCanvas.setNeedsRepaint();
        myCanvas.removeCommand(myNewCommand);
        myCanvas.removeCommand(myGoCommand);
        myCanvas.addCommand(myPauseCommand);
        myCanvas.revertToSaved();
      } else if (c == myExitCommand) {
        destroyApp(false);
        notifyDestroyed();
      }
    } catch (Exception e) {
      errorMsg(e);
    }
  }

  //-------------------------------------------------------
  //  error methods

  /**
   * Converts an exception to a message and displays the message..
   */
  void errorMsg(Exception e) {
    if (e.getMessage() == null) {
      errorMsg(e.getClass().getName());
    } else {
      errorMsg(e.getClass().getName() + ":" + 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 represents doors and keys.
 * 
 * @author Carol Hamer
 */

class DoorKey extends Sprite {

  //---------------------------------------------------------
  //    fields

  /**
   * The image file shared by all doors and keys.
   */
  public static Image myImage;

  /**
   * A code int that indicates the door or key's color.
   */
  private int myColor;

  //---------------------------------------------------------
  //    get/set data

  /**
   * @return the door or key's color.
   */
  public int getColor() {
    return (myColor);
  }

  //---------------------------------------------------------
  //    constructor and initializer

  static {
    try {
      myImage = Image.createImage("/images/keys.png");
    } catch (Exception e) {
      throw (new RuntimeException(
          "DoorKey.<init>-->failed to load image, caught "
              + e.getClass() + ": " + e.getMessage()));
    }
  }

  /**
   * Standard constructor sets the image to the correct frame (according to
   * whether this is a door or a key and what color it should be) and then
   * puts it in the correct location.
   */
  public DoorKey(int color, boolean isKey, int[] gridCoordinates) {
    super(myImage, DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_WIDTH);
    myColor = color;
    int imageIndex = color * 2;
    if (isKey) {
      imageIndex++;
    }
    setFrame(imageIndex);
    setPosition(gridCoordinates[0] * DungeonManager.SQUARE_WIDTH,
        gridCoordinates[1] * DungeonManager.SQUARE_WIDTH);
  }

}

/**
 * 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 contains the data for the map of the dungeon..
 * 
 * @author Carol Hamer
 */

class BoardDecoder {

  //--------------------------------------------------------
  //  fields

  /**
   * The coordinates of where the player starts on the map in terms of the
   * array indices.
   */
  private int[] myPlayerSquare;

  /**
   * The coordinates of the goal (crown).
   */
  private int[] myGoalSquare;

  /**
   * The coordinates of the doors. the there should be two in a row of each
   * color, following the same sequence as the keys.
   */
  private int[][] myDoors;

  /**
   * The coordinates of the Keys. the there should be of each color, following
   * the same sequence as the doors.
   */
  private int[][] myKeys;

  /**
   * The coordinates of the stone walls of the maze, encoded bit by bit.
   */
  private TiledLayer myLayer;

  /**
   * The data in bytes that gives the various boards. This was created using
   * EncodingUtils... This is a two-dimensional array: Each of the four main
   * sections corresponds to one of the four possible boards.
   */
  private static byte[][] myData = {
      { 0, 0, -108, -100, -24, 65, 21, 58, 53, -54, -116, -58, -56, -84,
          115, -118, -1, -1, -128, 1, -103, -15, -128, 25, -97, -127,
          -128, 79, -14, 1, -126, 121, -122, 1, -113, -49, -116, 1,
          -100, -3, -124, 5, -25, -27, -128, 1, -1, -1 },
      { 0, 1, 122, 90, -62, 34, -43, 72, -59, -29, 56, -55, 98, 126, -79,
          61, -1, -1, -125, 1, -128, 17, -26, 29, -31, 57, -72, 1,
          -128, -51, -100, 65, -124, 57, -2, 1, -126, 13, -113, 1,
          -97, 25, -127, -99, -8, 1, -1, -1 },
      { 0, 2, 108, -24, 18, -26, 102, 30, -58, 46, -28, -88, 34, -98, 97,
          -41, -1, -1, -96, 1, -126, 57, -9, 97, -127, 69, -119, 73,
          -127, 1, -109, 59, -126, 1, -26, 103, -127, 65, -103, 115,
          -127, 65, -25, 73, -128, 1, -1, -1 },
      { 0, 3, -114, 18, -34, 27, -39, -60, -76, -50, 118, 90, 82, -88,
          34, -74, -1, -1, -66, 1, -128, 121, -26, 125, -128, -123,
          -103, 29, -112, 1, -109, 49, -112, 1, -116, -31, -128, 5,
          -122, 5, -32, 13, -127, -51, -125, 1, -1, -1 }, };

  //--------------------------------------------------------
  //  initialization

  /**
   * Constructor fills data fields by interpreting the data bytes.
   */
  public BoardDecoder(int boardNum) throws Exception {
    // we start by selecting the two dimensional
    // array corresponding to the desired board:
    byte[] data = myData[boardNum];
    // The first two bytes give the version number and
    // the board number, but we ignore them because
    // they are assumed to be correct.
    // The third byte of the first array is the first one
    // we read: it gives the player's starting coordinates:
    myPlayerSquare = DataConverter.decodeCoords(data[2]);
    // the next byte gives the coordinates of the crown:
    myGoalSquare = DataConverter.decodeCoords(data[3]);
    // the next four bytes give the coordinates of the keys:
    myKeys = new int[4][];
    for (int i = 0; i < myKeys.length; i++) {
      myKeys[i] = DataConverter.decodeCoords(data[i + 4]);
    }
    // the next eight bytes give the coordinates of the doors:
    myDoors = new int[8][];
    for (int i = 0; i < myDoors.length; i++) {
      myDoors[i] = DataConverter.decodeCoords(data[i + 8]);
    }
    // now we create the TiledLayer object that is the
    // background dungeon map:
    myLayer = new TiledLayer(16, 16,
        Image.createImage("/images/stone.png"),
        DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_WIDTH);
    // now we call an internal utility that reads the array
    // of data that gives the positions of the blocks in the
    // walls of this dungeon:
    decodeDungeon(data, myLayer, 16);
  }

  //--------------------------------------------------------
  //  get/set data

  /**
   * @return the number of boards currently stored in this class.
   */
  public static int getNumBoards() {
    return (myData.length);
  }

  /**
   * get the coordinates of where the player starts on the map in terms of the
   * array indices.
   */
  public int[] getPlayerSquare() {
    return (myPlayerSquare);
  }

  /**
   * get the coordinates of the goal crown in terms of the array indices.
   */
  public int[] getGoalSquare() {
    return (myGoalSquare);
  }

  /**
   * get the tiled layer that gives the map of the dungeon.
   */
  public TiledLayer getLayer() {
    return (myLayer);
  }

  /**
   * Creates the array of door sprites. (call this only once to avoid creating
   * redundant sprites).
   */
  DoorKey[] createDoors() {
    DoorKey[] retArray = new DoorKey[8];
    for (int i = 0; i < 4; i++) {
      retArray[2 * i] = new DoorKey(i, false, myDoors[2 * i]);
      retArray[2 * i + 1] = new DoorKey(i, false, myDoors[2 * i + 1]);
    }
    return (retArray);
  }

  /**
   * Creates the array of key sprites. (call this only once to avoid creating
   * redundant sprites.)
   */
  DoorKey[] createKeys() {
    DoorKey[] retArray = new DoorKey[4];
    for (int i = 0; i < 4; i++) {
      retArray[i] = new DoorKey(i, true, myKeys[i]);
    }
    return (retArray);
  }

  //--------------------------------------------------------
  //  decoding utilities

  /**
   * Takes a dungeon given as a byte array and uses it to set the tiles of a
   * tiled layer.
   * 
   * The TiledLayer in this case is a 16 x 16 grid in which each square can be
   * either blank (value of 0) or can be filled with a stone block (value of
   * 1). Therefore each square requires only one bit of information. Each byte
   * of data in the array called "data" records the frame indices of eight
   * squares in the grid.
   */
  private static void decodeDungeon(byte[] data, TiledLayer dungeon,
      int offset) throws Exception {
    if (data.length + offset < 32) {
      throw (new Exception(
          "BoardDecoder.decodeDungeon-->not enough data!!!"));
    }
    // a frame index of zero indicates a blank square
    // (this is always true in a TiledLayer).
    // This TiledLayer has only one possible (non-blank)
    // frame, so a frame index of 1 indicates a stone block
    int frame = 0;
    // Each of the 32 bytes in the data array records
    // the frame indices of eight block in the 16 x 16
    // grid. Two bytes give one row of the dungeon,
    // so we have the array index go from zero to 16
    // to set the frame indices fro each of the 16 rows.
    for (int i = 0; i < 16; i++) {
      // 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;
      // Here we check two bytes at the same time
      // (the two bytes together correspond to one row
      // of the dungeon). We use a loop that checks
      // the bytes bit by bit by performing a bitwise
      // and (&) between the data byte and a flag:
      for (int j = 0; j < 8; j++) {
        if ((data[offset + 2 * i] & flag) != 0) {
          frame = 1;
        } else {
          frame = 0;
        }
        dungeon.setCell(j, i, frame);
        if ((data[offset + 2 * i + 1] & flag) != 0) {
          frame = 1;
        } else {
          frame = 0;
        }
        dungeon.setCell(j + 8, i, frame);
        // 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;
      }
    }
  }

}

/**
 * This class contains the loop that keeps the game running.
 * 
 * @author Carol Hamer
 */

class GameThread extends Thread {

  //---------------------------------------------------------
  //   fields

  /**
   * Whether or not the main thread would like this thread to pause.
   */
  private boolean myShouldPause;

  /**
   * Whether or not the main thread would like this thread to stop.
   */
  private static boolean myShouldStop;

  /**
   * A handle back to the graphical components.
   */
  private DungeonCanvas myDungeonCanvas;

  /**
   * The System.time of the last screen refresh, used to regulate refresh
   * speed.
   */
  private long myLastRefreshTime;

  //----------------------------------------------------------
  //   initialization

  /**
   * standard constructor.
   */
  GameThread(DungeonCanvas canvas) {
    myDungeonCanvas = canvas;
  }

  //----------------------------------------------------------
  //   utilities

  /**
   * Get the amount of time to wait between screen refreshes. Normally we wait
   * only a single millisecond just to give the main thread a chance to update
   * the keystroke info, but this method ensures that the game will not
   * attempt to show too many frames per second.
   */
  private long getWaitTime() {
    long retVal = 1;
    long difference = System.currentTimeMillis() - myLastRefreshTime;
    if (difference < 75) {
      retVal = 75 - difference;
    }
    return (retVal);
  }

  //----------------------------------------------------------
  //   actions

  /**
   * pause the game.
   */
  void pause() {
    myShouldPause = true;
  }

  /**
   * restart the game after a pause.
   */
  synchronized void resumeGame() {
    myShouldPause = false;
    notify();
  }

  /**
   * stops the game.
   */
  synchronized void requestStop() {
    myShouldStop = true;
    this.notify();
  }

  /**
   * start the game..
   */
  public void run() {
    // flush any keystrokes that occurred before the
    // game started:
    myDungeonCanvas.flushKeys();
    myShouldStop = false;
    myShouldPause = false;
    while (true) {
      myLastRefreshTime = System.currentTimeMillis();
      if (myShouldStop) {
        break;
      }
      myDungeonCanvas.checkKeys();
      myDungeonCanvas.updateScreen();
      // we do a very short pause to allow the other thread
      // to update the information about which keys are pressed:
      synchronized (this) {
        try {
          wait(getWaitTime());
        } catch (Exception e) {
        }
      }
      if (myShouldPause) {
        synchronized (this) {
          try {
            wait();
          } catch (Exception e) {
          }
        }
      }
    }
  }

}

/**
 * This class contains the data for a game currently in progress. used to store
 * a game and to resume a stored game.
 * 
 * @author Carol Hamer
 */

class GameInfo {

  //--------------------------------------------------------
  //  fields

  /**
   * The name of the datastore.
   */
  public static final String STORE = "GameInfo";

  /**
   * This is set to true if an attempt is made to read a game when no game has
   * been saved.
   */
  private boolean myNoDataSaved;

  /**
   * The number that indicates which board the player is currently on.
   */
  private int myBoardNum;

  /**
   * The amount of time that has passed.
   */
  private int myTime;

  /**
   * The coordinates of where the player is on the board. coordinate values
   * must be between 0 and 15.
   */
  private int[] myPlayerSquare;

  /**
   * The coordinates of where the keys are currently found. MUST BE four sets
   * of two integer coordinates. coordinate values must be between 0 and 15.
   */
  private int[][] myKeyCoords;

  /**
   * The list of which doors are currently open. 0 = open 1 = closed WARNING:
   * this array MUST have length 8.
   */
  private int[] myDoorsOpen;

  /**
   * The number of the key that is currently being held by the player. if no
   * key is held, then the value is -1.
   */
  private int myHeldKey;

  //--------------------------------------------------------
  //  data gets/sets

  /**
   * @return true if no saved game records were found.
   */
  boolean getIsEmpty() {
    return (myNoDataSaved);
  }

  /**
   * @return The number that indicates which board the player is currently on.
   */
  int getBoardNum() {
    return (myBoardNum);
  }

  /**
   * @return The number of the key that is currently being held by the player.
   *         if no key is held, then the value is -1.
   */
  int getHeldKey() {
    return (myHeldKey);
  }

  /**
   * @return The amount of time that has passed.
   */
  int getTime() {
    return (myTime);
  }

  /**
   * @return The coordinates of where the player is on the board. coordinate
   *         values must be between 0 and 15.
   */
  int[] getPlayerSquare() {
    return (myPlayerSquare);
  }

  /**
   * @return The coordinates of where the keys are currently found. MUST BE
   *         four sets of two integer coordinates. coordinate values must be
   *         between 0 and 15.
   */
  int[][] getKeyCoords() {
    return (myKeyCoords);
  }

  /**
   * @return The list of which doors are currently open. 0 = open 1 = closed
   *         WARNING: this array MUST have length 8.
   */
  int[] getDoorsOpen() {
    return (myDoorsOpen);
  }

  //--------------------------------------------------------
  //  constructors

  /**
   * This constructor records the game info of a game currently in progress.
   */
  GameInfo(int boardNum, int time, int[] playerSquare, int[][] keyCoords,
      int[] doorsOpen, int heldKey) throws Exception {
    myBoardNum = boardNum;
    myTime = time;
    myPlayerSquare = playerSquare;
    myKeyCoords = keyCoords;
    myDoorsOpen = doorsOpen;
    myHeldKey = heldKey;
    encodeInfo();
  }

  /**
   * This constructor reads the game configuration from memory. This is used
   * to reconstruct a saved game.
   */
  GameInfo() {
    RecordStore store = null;
    try {
      // if the record store does not yet exist, don't
      // create it
      store = RecordStore.openRecordStore(STORE, false);
      if ((store != null) && (store.getNumRecords() > 0)) {
        // the first record has id number 1
        // it should also be the only record since this
        // particular game stores only one game.
        byte[] data = store.getRecord(1);
        myBoardNum = data[0];
        myPlayerSquare = DataConverter.decodeCoords(data[1]);
        myKeyCoords = new int[4][];
        myKeyCoords[0] = DataConverter.decodeCoords(data[2]);
        myKeyCoords[1] = DataConverter.decodeCoords(data[3]);
        myKeyCoords[2] = DataConverter.decodeCoords(data[4]);
        myKeyCoords[3] = DataConverter.decodeCoords(data[5]);
        myDoorsOpen = DataConverter.decode8(data[6]);
        myHeldKey = data[7];
        byte[] fourBytes = new byte[4];
        System.arraycopy(data, 8, fourBytes, 0, 4);
        myTime = DataConverter.parseInt(fourBytes);
      } else {
        myNoDataSaved = true;
      }
    } catch (Exception e) {
      // this throws when the record store doesn't exist.
      // for that or any error, we assume no data is saved:
      myNoDataSaved = true;
    } finally {
      try {
        if (store != null) {
          store.closeRecordStore();
        }
      } catch (Exception e) {
        // if the record store is open this shouldn't throw.
      }
    }
  }

  //--------------------------------------------------------
  //  encoding method

  /**
   * Turn the data into a byte array and save it.
   */
  private void encodeInfo() throws Exception {
    RecordStore store = null;
    try {
      byte[] data = new byte[12];
      data[0] = (new Integer(myBoardNum)).byteValue();
      data[1] = DataConverter.encodeCoords(myPlayerSquare);
      data[2] = DataConverter.encodeCoords(myKeyCoords[0]);
      data[3] = DataConverter.encodeCoords(myKeyCoords[1]);
      data[4] = DataConverter.encodeCoords(myKeyCoords[2]);
      data[5] = DataConverter.encodeCoords(myKeyCoords[3]);
      data[6] = DataConverter.encode8(myDoorsOpen, 0);
      data[7] = (new Integer(myHeldKey)).byteValue();
      byte[] timeBytes = DataConverter.intToFourBytes(myTime);
      System.arraycopy(timeBytes, 0, data, 8, 4);
      // if the record store does not yet exist, the second
      // arg "true" tells it to create.
      store = RecordStore.openRecordStore(STORE, true);
      int numRecords = store.getNumRecords();
      if (numRecords > 0) {
        store.setRecord(1, data, 0, data.length);
      } else {
        store.addRecord(data, 0, data.length);
      }
    } catch (Exception e) {
      throw (e);
    } finally {
      try {
        if (store != null) {
          store.closeRecordStore();
        }
      } catch (Exception e) {
        // if the record store is open this shouldn't throw.
      }
    }
  }

}

/**
 * This class contains the data for the map of the dungeon. This is a utility
 * class that allows a developer to write the data for a board in a simple
 * format, then this class encodes the data in a format that the game can use.
 * 
 * note that the data that this class encodes is hard-coded. that is because
 * this class is intended to be used only a few times to encode the data. Once
 * the board data has been encoded, it never needs to be encoded again. The
 * encoding methods used in this class could be generalized to be used to create
 * a board editor which would allow a user to easily create new boards, but that
 * is an exercise for another day...
 * 
 * @author Carol Hamer
 */

class EncodingUtils {

  //--------------------------------------------------------
  //  fields

  /**
   * data for which squares are filled and which are blank. 0 = empty 1 =
   * filled
   */
  private int[][] mySquares = {
      { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
      { 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 },
      { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1 },
      { 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1 },
      { 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1 },
      { 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
      { 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1 },
      { 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1 },
      { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1 },
      { 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
      { 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1 },
      { 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1 },
      { 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1 },
      { 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1 },
      { 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
      { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, };

  /**
   * The coordinates of where the player starts on the map in terms of the
   * array indices.
   */
  private int[] myPlayerSquare = { 7, 10 };

  /**
   * The coordinates of the goal (crown).
   */
  private int[] myGoalSquare = { 5, 10 };

  //--------------------------------------------------------
  //  get/set data

  /**
   * Creates the array of door sprites. (call this only once to avoid creating
   * redundant sprites).
   */
  int[][] getDoorCoords() {
    int[][] retArray = new int[8][];
    for (int i = 0; i < retArray.length; i++) {
      retArray[i] = new int[2];
    }
    // red
    retArray[0][0] = 12;
    retArray[0][1] = 5;
    retArray[1][0] = 14;
    retArray[1][1] = 3;
    // green
    retArray[2][0] = 3;
    retArray[2][1] = 8;
    retArray[3][0] = 12;
    retArray[3][1] = 9;
    // blue
    retArray[4][0] = 6;
    retArray[4][1] = 2;
    retArray[5][0] = 7;
    retArray[5][1] = 14;
    // yellow
    retArray[6][0] = 11;
    retArray[6][1] = 1;
    retArray[7][0] = 3;
    retArray[7][1] = 13;
    return (retArray);
  }

  /**
   * Creates the array of key sprites. (call this only once to avoid creating
   * redundant sprites.)
   */
  int[][] getKeyCoords() {
    int[][] retArray = new int[4][];
    for (int i = 0; i < retArray.length; i++) {
      retArray[i] = new int[2];
    }
    // red
    retArray[0][0] = 12;
    retArray[0][1] = 2;
    // green
    retArray[1][0] = 2;
    retArray[1][1] = 2;
    // blue
    retArray[2][0] = 13;
    retArray[2][1] = 5;
    // yellow
    retArray[3][0] = 4;
    retArray[3][1] = 8;
    return (retArray);
  }

  //--------------------------------------------------------
  //  encoding / decoding utilities

  /**
   * Encodes the entire dungeon.
   */
  byte[][] encodeDungeon() {
    byte[][] retArray = new byte[2][];
    retArray[0] = new byte[16];
    // the first byte is the version number:
    retArray[0][0] = 0;
    // the second byte is the board number:
    retArray[0][1] = 0;
    // the player's start square:
    retArray[0][2] = DataConverter.encodeCoords(myPlayerSquare);
    // the goal (crown) square:
    retArray[0][3] = DataConverter.encodeCoords(myGoalSquare);
    //encode the keys:
    int[][] keyCoords = getKeyCoords();
    for (int i = 0; i < keyCoords.length; i++) {
      retArray[0][i + 4] = DataConverter.encodeCoords(keyCoords[i]);
    }
    //encode the doors:
    int[][] doorCoords = getDoorCoords();
    for (int i = 0; i < doorCoords.length; i++) {
      retArray[0][i + 8] = DataConverter.encodeCoords(doorCoords[i]);
    }
    //encode the maze:
    try {
      retArray[1] = encodeDungeon(mySquares);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return (retArray);
  }

  /**
   * Takes a dungeon given in terms of an array of 1s and 0s and turns it into
   * an array of bytes. WARNING: the array MUST BE 16 X 16.
   */
  static byte[] encodeDungeon(int[][] dungeonMap) throws Exception {
    if ((dungeonMap.length != 16) || (dungeonMap[0].length != 16)) {
      throw (new Exception(
          "EncodingUtils.encodeDungeon-->must be 16x16!!!"));
    }
    byte[] retArray = new byte[32];
    for (int i = 0; i < 16; i++) {
      retArray[2 * i] = DataConverter.encode8(dungeonMap[i], 0);
      retArray[2 * i + 1] = DataConverter.encode8(dungeonMap[i], 8);
    }
    return (retArray);
  }

  //--------------------------------------------------------
  //  main prints the bytes to standard out.
  // (note that this class is not intended to be run as a MIDlet)

  /**
   * Prints the byte version of the board to standard out.
   */
  public static void main(String[] args) {
    try {
      EncodingUtils map = new EncodingUtils();
      byte[][] data = map.encodeDungeon();
      System.out.println("EncodingUtils.main-->dungeon encoded");
      System.out.print("{\n   " + data[0][0]);
      for (int i = 1; i < data[0].length; i++) {
        System.out.print(", " + data[0][i]);
      }
      for (int i = 1; i < data[1].length; i++) {
        System.out.print(", " + data[1][i]);
      }
      System.out.println("\n};");
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}
/**
 * This class handles the graphics objects.
 * 
 * @author Carol Hamer
 */

class DungeonManager extends LayerManager {

  //---------------------------------------------------------
  //   dimension fields
  //  (constant after initialization)

  /**
   * The x-coordinate of the place on the game canvas where the LayerManager
   * window should appear, in terms of the coordiantes of the game canvas.
   */
  static int CANVAS_X;

  /**
   * The y-coordinate of the place on the game canvas where the LayerManager
   * window should appear, in terms of the coordiantes of the game canvas.
   */
  static int CANVAS_Y;

  /**
   * The width of the display window.
   */
  static int DISP_WIDTH;

  /**
   * The height of this object's visible region.
   */
  static int DISP_HEIGHT;

  /**
   * the (right or left) distance the player goes in a single keystroke.
   */
  static final int MOVE_LENGTH = 8;

  /**
   * The width of the square tiles that this game is divided into. This is the
   * width of the stone walls as well as the princess and the ghost.
   */
  static final int SQUARE_WIDTH = 24;

  /**
   * The jump index that indicates that no jump is currently in progress..
   */
  static final int NO_JUMP = -6;

  /**
   * The maximum speed for the player's fall..
   */
  static final int MAX_FREE_FALL = 3;

  //---------------------------------------------------------
  //   game object fields

  /**
   * the handle back to the canvas.
   */
  private DungeonCanvas myCanvas;

  /**
   * the background dungeon.
   */
  private TiledLayer myBackground;

  /**
   * the player.
   */
  private Sprite myPrincess;

  /**
   * the goal.
   */
  private Sprite myCrown;

  /**
   * the doors.
   */
  private DoorKey[] myDoors;

  /**
   * the keys.
   */
  private DoorKey[] myKeys;

  /**
   * the key currently held by the player.
   */
  private DoorKey myHeldKey;

  /**
   * The leftmost x-coordinate that should be visible on the screen in terms
   * of this objects internal coordinates.
   */
  private int myViewWindowX;

  /**
   * The top y-coordinate that should be visible on the screen in terms of
   * this objects internal coordinates.
   */
  private int myViewWindowY;

  /**
   * Where the princess is in the jump sequence.
   */
  private int myIsJumping = NO_JUMP;

  /**
   * Whether or not the screen needs to be repainted.
   */
  private boolean myModifiedSinceLastPaint = true;

  /**
   * Which board we're playing on.
   */
  private int myCurrentBoardNum = 0;

  //-----------------------------------------------------
  //    gets/sets

  /**
   * Tell the layer manager that it needs to repaint.
   */
  public void setNeedsRepaint() {
    myModifiedSinceLastPaint = true;
  }

  //-----------------------------------------------------
  //    initialization
  //    set up or save game data.

  /**
   * Constructor merely sets the data.
   * 
   * @param x
   *            The x-coordinate of the place on the game canvas where the
   *            LayerManager window should appear, in terms of the coordiantes
   *            of the game canvas.
   * @param y
   *            The y-coordinate of the place on the game canvas where the
   *            LayerManager window should appear, in terms of the coordiantes
   *            of the game canvas.
   * @param width
   *            the width of the region that is to be occupied by the
   *            LayoutManager.
   * @param height
   *            the height of the region that is to be occupied by the
   *            LayoutManager.
   * @param canvas
   *            the DungeonCanvas that this LayerManager should appear on.
   */
  public DungeonManager(int x, int y, int width, int height,
      DungeonCanvas canvas) throws Exception {
    myCanvas = canvas;
    CANVAS_X = x;
    CANVAS_Y = y;
    DISP_WIDTH = width;
    DISP_HEIGHT = height;
    // create a decoder object that creates the dungeon and
    // its associated Sprites from data.
    BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum);
    // get the background TiledLayer
    myBackground = decoder.getLayer();
    // get the coordinates of the square that the princess
    // starts on.
    int[] playerCoords = decoder.getPlayerSquare();
    // create the player sprite
    myPrincess = new Sprite(Image.createImage("/images/princess.png"),
        SQUARE_WIDTH, SQUARE_WIDTH);
    myPrincess.setFrame(1);
    // we define the reference pixel to be in the middle
    // of the princess image so that when the princess turns
    // from right to left (and vice versa) she does not
    // appear to move to a different location.
    myPrincess.defineReferencePixel(SQUARE_WIDTH / 2, 0);
    // the dungeon is a 16x16 grid, so the array playerCoords
    // gives the player's location in terms of the grid, and
    // then we multiply those coordinates by the SQUARE_WIDTH
    // to get the precise pixel where the player should be
    // placed (in terms of the LayerManager's coordinate system)
    myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH
        * playerCoords[1]);
    // we append all of the Layers (TiledLayer and Sprite)
    // so that this LayerManager will paint them when
    // flushGraphics is called.
    append(myPrincess);
    // get the coordinates of the square where the crown
    // should be placed.
    int[] goalCoords = decoder.getGoalSquare();
    myCrown = new Sprite(Image.createImage("/images/crown.png"));
    myCrown.setPosition(
        (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4),
        (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2));
    append(myCrown);
    // The decoder creates the door and key sprites and places
    // them in the correct locations in terms of the LayerManager's
    // coordinate system.
    myDoors = decoder.createDoors();
    myKeys = decoder.createKeys();
    for (int i = 0; i < myDoors.length; i++) {
      append(myDoors[i]);
    }
    for (int i = 0; i < myKeys.length; i++) {
      append(myKeys[i]);
    }
    // append the background last so it will be painted first.
    append(myBackground);
    // this sets the view screen so that the player is
    // in the center.
    myViewWindowX = SQUARE_WIDTH * playerCoords[0]
        - ((DISP_WIDTH - SQUARE_WIDTH) / 2);
    myViewWindowY = SQUARE_WIDTH * playerCoords[1]
        - ((DISP_HEIGHT - SQUARE_WIDTH) / 2);
    // a number of objects are created in order to set up the game,
    // but they should be eliminated to free up memory:
    decoder = null;
    System.gc();
  }

  /**
   * sets all variables back to their initial positions.
   */
  void reset() throws Exception {
    // first get rid of the old board:
    for (int i = 0; i < myDoors.length; i++) {
      remove(myDoors[i]);
    }
    myHeldKey = null;
    for (int i = 0; i < myKeys.length; i++) {
      remove(myKeys[i]);
    }
    remove(myBackground);
    // now create the new board:
    myCurrentBoardNum++;
    // in this version we go back to the beginning if
    // all boards have been completed.
    if (myCurrentBoardNum == BoardDecoder.getNumBoards()) {
      myCurrentBoardNum = 0;
    }
    // we create a new decoder object to read and interpret
    // all of the data for the current board.
    BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum);
    // get the background TiledLayer
    myBackground = decoder.getLayer();
    // get the coordinates of the square that the princess
    // starts on.
    int[] playerCoords = decoder.getPlayerSquare();
    // the dungeon is a 16x16 grid, so the array playerCoords
    // gives the player's location in terms of the grid, and
    // then we multiply those coordinates by the SQUARE_WIDTH
    // to get the precise pixel where the player should be
    // placed (in terms of the LayerManager's coordinate system)
    myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH
        * playerCoords[1]);
    myPrincess.setFrame(1);
    // get the coordinates of the square where the crown
    // should be placed.
    int[] goalCoords = decoder.getGoalSquare();
    myCrown.setPosition(
        (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4),
        (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2));
    // The decoder creates the door and key sprites and places
    // them in the correct locations in terms of the LayerManager's
    // coordinate system.
    myDoors = decoder.createDoors();
    myKeys = decoder.createKeys();
    for (int i = 0; i < myDoors.length; i++) {
      append(myDoors[i]);
    }
    for (int i = 0; i < myKeys.length; i++) {
      append(myKeys[i]);
    }
    // append the background last so it will be painted first.
    append(myBackground);
    // this sets the view screen so that the player is
    // in the center.
    myViewWindowX = SQUARE_WIDTH * playerCoords[0]
        - ((DISP_WIDTH - SQUARE_WIDTH) / 2);
    myViewWindowY = SQUARE_WIDTH * playerCoords[1]
        - ((DISP_HEIGHT - SQUARE_WIDTH) / 2);
    // a number of objects are created in order to set up the game,
    // but they should be eliminated to free up memory:
    decoder = null;
    System.gc();
  }

  /**
   * sets all variables back to the position in the saved game.
   * 
   * @return the time on the clock of the saved game.
   */
  int revertToSaved() throws Exception {
    int retVal = 0;
    // first get rid of the old board:
    for (int i = 0; i < myDoors.length; i++) {
      remove(myDoors[i]);
    }
    myHeldKey = null;
    for (int i = 0; i < myKeys.length; i++) {
      remove(myKeys[i]);
    }
    remove(myBackground);
    // now get the info of the saved game
    // only one game is saved at a time, and the GameInfo object
    // will read the saved game's data from memory.
    GameInfo info = new GameInfo();
    if (info.getIsEmpty()) {
      // if no game has been saved, we start from the beginning.
      myCurrentBoardNum = 0;
      reset();
    } else {
      // get the time on the clock of the saved game.
      retVal = info.getTime();
      // get the number of the board the saved game was on.
      myCurrentBoardNum = info.getBoardNum();
      // create the BoradDecoder that gives the data for the
      // desired board.
      BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum);
      // get the background TiledLayer
      myBackground = decoder.getLayer();
      // get the coordinates of the square that the princess
      // was on in the saved game.
      int[] playerCoords = info.getPlayerSquare();
      myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH
          * playerCoords[1]);
      myPrincess.setFrame(1);
      // get the coordinates of the square where the crown
      // should be placed (this is given by the BoardDecoder
      // and not from the data of the saved game because the
      // crown does not move during the game.
      int[] goalCoords = decoder.getGoalSquare();
      myCrown.setPosition((SQUARE_WIDTH * goalCoords[0])
          + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1])
          + (SQUARE_WIDTH / 2));
      // The decoder creates the door and key sprites and places
      // them in the correct locations in terms of the LayerManager's
      // coordinate system.
      myDoors = decoder.createDoors();
      myKeys = decoder.createKeys();
      // get an array of ints that lists whether each door is
      // open or closed in the saved game
      int[] openDoors = info.getDoorsOpen();
      for (int i = 0; i < myDoors.length; i++) {
        append(myDoors[i]);
        if (openDoors[i] == 0) {
          // if the door was open, make it invisible
          myDoors[i].setVisible(false);
        }
      }
      // the keys can be moved by the player, so we get their
      // coordinates from the GameInfo saved data.
      int[][] keyCoords = info.getKeyCoords();
      for (int i = 0; i < myKeys.length; i++) {
        append(myKeys[i]);
        myKeys[i].setPosition(SQUARE_WIDTH * keyCoords[i][0],
            SQUARE_WIDTH * keyCoords[i][1]);
      }
      // if the player was holding a key in the saved game,
      // we have the player hold that key and set it to invisible.
      int heldKey = info.getHeldKey();
      if (heldKey != -1) {
        myHeldKey = myKeys[heldKey];
        myHeldKey.setVisible(false);
      }
      // append the background last so it will be painted first.
      append(myBackground);
      // this sets the view screen so that the player is
      // in the center.
      myViewWindowX = SQUARE_WIDTH * playerCoords[0]
          - ((DISP_WIDTH - SQUARE_WIDTH) / 2);
      myViewWindowY = SQUARE_WIDTH * playerCoords[1]
          - ((DISP_HEIGHT - SQUARE_WIDTH) / 2);
      // a number of objects are created in order to set up the game,
      // but they should be eliminated to free up memory:
      decoder = null;
      System.gc();
    }
    return (retVal);
  }

  /**
   * save the current game in progress.
   */
  void saveGame(int gameTicks) throws Exception {
    int[] playerSquare = new int[2];
    // the coordinates of the player are given in terms of
    // the 16 x 16 dungeon grid. We divide the player's
    // pixel coordinates to ge the right grid square.
    // If the player was not precisely alligned with a
    // grid square when the game was saved, the difference
    // will be shaved off.
    playerSquare[0] = myPrincess.getX() / SQUARE_WIDTH;
    playerSquare[1] = myPrincess.getY() / SQUARE_WIDTH;
    // save the coordinates of the current locations of
    // the keys, and if a key is currently held by the
    // player, we save the info of which one it was.
    int[][] keyCoords = new int[4][];
    int heldKey = -1;
    for (int i = 0; i < myKeys.length; i++) {
      keyCoords[i] = new int[2];
      keyCoords[i][0] = myKeys[i].getX() / SQUARE_WIDTH;
      keyCoords[i][1] = myKeys[i].getY() / SQUARE_WIDTH;
      if ((myHeldKey != null) && (myKeys[i] == myHeldKey)) {
        heldKey = i;
      }
    }
    // save the information of which doors were open.
    int[] doorsOpen = new int[8];
    for (int i = 0; i < myDoors.length; i++) {
      if (myDoors[i].isVisible()) {
        doorsOpen[i] = 1;
      }
    }
    // take all of the information we've gathered and
    // create a GameInfo object that will save the info
    // in the device's memory.
    GameInfo info = new GameInfo(myCurrentBoardNum, gameTicks,
        playerSquare, keyCoords, doorsOpen, heldKey);
  }

  //-------------------------------------------------------
  //  graphics methods

  /**
   * paint the game graphic on the screen.
   */
  public void paint(Graphics g) throws Exception {
    // only repaint if something has changed:
    if (myModifiedSinceLastPaint) {
      g.setColor(DungeonCanvas.WHITE);
      // paint the background white to cover old game objects
      // that have changed position since last paint.
      // here coordinates are given
      // with respect to the graphics (canvas) origin:
      g.fillRect(0, 0, DISP_WIDTH, DISP_HEIGHT);
      // here coordinates are given
      // with respect to the LayerManager origin:
      setViewWindow(myViewWindowX, myViewWindowY, DISP_WIDTH, DISP_HEIGHT);
      // call the paint funstion of the superclass LayerManager
      // to paint all of the Layers
      paint(g, CANVAS_X, CANVAS_Y);
      // don't paint again until something changes:
      myModifiedSinceLastPaint = false;
    }
  }

  //-------------------------------------------------------
  //  game movements

  /**
   * respond to keystrokes by deciding where to move and then moving the
   * pieces and the view window correspondingly.
   */
  void requestMove(int horizontal, int vertical) {
    if (horizontal != 0) {
      // see how far the princess can move in the desired
      // horizontal direction (if not blocked by a wall
      // or closed door)
      horizontal = requestHorizontal(horizontal);
    }
    // vertical < 0 indicates that the user has
    // pressed the UP button and would like to jump.
    // therefore, if we're not currently jumping,
    // we begin the jump.
    if ((myIsJumping == NO_JUMP) && (vertical < 0)) {
      myIsJumping++;
    } else if (myIsJumping == NO_JUMP) {
      // if we're not jumping at all, we need to check
      // if the princess should be falling:
      // we (temporarily) move the princess down and see if that
      // causes a collision with the floor:
      myPrincess.move(0, MOVE_LENGTH);
      // if the princess can move down without colliding
      // with the floor, then we set the princess to
      // be falling. The variable myIsJumping starts
      // negative while the princess is jumping up and
      // is zero or positive when the princess is coming
      // back down. We therefore set myIsJumping to
      // zero to indicate that the princess should start
      // falling.
      if (!checkCollision()) {
        myIsJumping = 0;
      }
      // we move the princess Sprite back to the correct
      // position she was at before we (temporarily) moved
      // her down to see if she would fall.
      myPrincess.move(0, -MOVE_LENGTH);
    }
    // if the princess is currently jumping or falling,
    // we calculate the vertical distance she should move
    // (taking into account the horizontal distance that
    // she is also moving).
    if (myIsJumping != NO_JUMP) {
      vertical = jumpOrFall(horizontal);
    }
    // now that we've calculated how far the princess
    // should move, we move her. (this is a call to
    // another internal method of this method
    // suite, it is not a built-in LayerManager method):
    move(horizontal, vertical);
  }

  /**
   * Internal to requestMove. Calculates what the real horizontal distance
   * moved should be after taking obstacles into account.
   * 
   * @return the horizontal distance that the player can move.
   */
  private int requestHorizontal(int horizontal) {
    // we (temporarily) move her to the right or left
    // and see if she hits a wall or a door:
    myPrincess.move(horizontal * MOVE_LENGTH, 0);
    if (checkCollision()) {
      // if she hits something, then she's not allowed
      // to go in that direction, so we set the horizontal
      // move distance to zero and then move the princess
      // back to where she was.
      myPrincess.move(-horizontal * MOVE_LENGTH, 0);
      horizontal = 0;
    } else {
      // if she doesn't hit anything then the move request
      // succeeds, but we still move her back to the
      // earlier position because this was just the checking
      // phase.
      myPrincess.move(-horizontal * MOVE_LENGTH, 0);
      horizontal *= MOVE_LENGTH;
    }
    return (horizontal);
  }

  /**
   * Internal to requestMove. Calculates the vertical change in the player's
   * position if jumping or falling. this method should only be called if the
   * player is currently jumping or falling.
   * 
   * @return the vertical distance that the player should move this turn.
   *         (negative moves up, positive moves down)
   */
  private int jumpOrFall(int horizontal) {
    // by default we do not move vertically
    int vertical = 0;
    // The speed of rise or descent is computed using
    // the int myIsJumping. Since we are in a jump or
    // fall, we advance the jump by one (which simulates
    // the downward pull of gravity by slowing the rise
    // or accellerating the fall) unless the player is
    // already falling at maximum speed. (a maximum
    // free fall speed is necessary because otherwise
    // it is possible for the player to fall right through
    // the bottom of the maze...)
    if (myIsJumping <= MAX_FREE_FALL) {
      myIsJumping++;
    }
    if (myIsJumping < 0) {
      // if myIsJumping is negative, that means that
      // the princess is rising. We calculate the
      // number of pixels to go up by raising 2 to
      // the power myIsJumping (absolute value).
      // note that we make the result negative because
      // the up and down coordinates in Java are the
      // reverse of the vertical coordinates we learned
      // in math class: as you go up, the coordinate
      // values go down, and as you go down the screen,
      // the coordinate numbers go up.
      vertical = -(2 << (-myIsJumping));
    } else {
      // if myIsJumping is positive, the princess is falling.
      // we calculate the distance to fall by raising two
      // to the power of the absolute value of myIsJumping.
      vertical = (2 << (myIsJumping));
    }
    // now we temporarily move the princess the desired
    // vertical distance (with the corresponding horizontal
    // distance also thrown in), and see if she hits anything:
    myPrincess.move(horizontal, vertical);
    if (checkCollision()) {
      // here we're in the case where she did hit something.
      // we move her back into position and then see what
      // to do about it.
      myPrincess.move(-horizontal, -vertical);
      if (vertical > 0) {
        // in this case the player is falling.
        // so we need to determine precisely how
        // far she can fall before she hit the bottom
        vertical = 0;
        // we temporarily move her the desired horizontal
        // distance while calculating the corresponding
        // vertical distance.
        myPrincess.move(horizontal, 0);
        while (!checkCollision()) {
          vertical++;
          myPrincess.move(0, 1);
        }
        // now that we've calculated how far she can fall,
        // we move her back to her earlier position
        myPrincess.move(-horizontal, -vertical);
        // we subtract 1 pixel from the distance calculated
        // because once she has actually collided with the
        // floor, she's gone one pixel too far...
        vertical--;
        // now that she's hit the floor, she's not jumping
        // anymore.
        myIsJumping = NO_JUMP;
      } else {
        // in this case we're going up, so she
        // must have hit her head.
        // This next if is checking for a special
        // case where there's room to jump up exactly
        // one square. In that case we increase the
        // value of myIsJumping in order to make the
        // princess not rise as high. The details
        // of the calculation in this case were found
        // through trial and error:
        if (myIsJumping == NO_JUMP + 2) {
          myIsJumping++;
          vertical = -(2 << (-myIsJumping));
          // now we see if the special shortened jump
          // still makes her hit her head:
          // (as usual, temporarily move her to test
          // for collisions)
          myPrincess.move(horizontal, vertical);
          if (checkCollision()) {
            // if she still hits her head even
            // with this special shortened jump,
            // then she was not meant to jump...
            myPrincess.move(-horizontal, -vertical);
            vertical = 0;
            myIsJumping = NO_JUMP;
          } else {
            // now that we've chhecked for collisions,
            // we move the player back to her earlier
            // position:
            myPrincess.move(-horizontal, -vertical);
          }
        } else {
          // if she hit her head, then she should not
          // jump up.
          vertical = 0;
          myIsJumping = NO_JUMP;
        }
      }
    } else {
      // since she didn't hit anything when we moved
      // her, then all we have to do is move her back.
      myPrincess.move(-horizontal, -vertical);
    }
    return (vertical);
  }

  /**
   * Internal to requestMove. Once the moves have been determined, actually
   * perform the move.
   */
  private void move(int horizontal, int vertical) {
    // repaint only if we actually change something:
    if ((horizontal != 0) || (vertical != 0)) {
      myModifiedSinceLastPaint = true;
    }
    // if the princess is moving left or right, we set
    // her image to be facing the right direction:
    if (horizontal > 0) {
      myPrincess.setTransform(Sprite.TRANS_NONE);
    } else if (horizontal < 0) {
      myPrincess.setTransform(Sprite.TRANS_MIRROR);
    }
    // if she's jumping or falling, we set the image to
    // the frame where the skirt is inflated:
    if (vertical != 0) {
      myPrincess.setFrame(0);
      // if she's just running, we alternate between the
      // two frames:
    } else if (horizontal != 0) {
      if (myPrincess.getFrame() == 1) {
        myPrincess.setFrame(0);
      } else {
        myPrincess.setFrame(1);
      }
    }
    // move the position of the view window so that
    // the player stays in the center:
    myViewWindowX += horizontal;
    myViewWindowY += vertical;
    // after all that work, we finally move the
    // princess for real!!!
    myPrincess.move(horizontal, vertical);
  }

  //-------------------------------------------------------
  //  sprite interactions

  /**
   * Drops the currently held key and picks up another.
   */
  void putDownPickUp() {
    // we do not want to allow the player to put
    // down the key in the air, so we verify that
    // we're not jumping or falling first:
    if ((myIsJumping == NO_JUMP) && (myPrincess.getY() % SQUARE_WIDTH == 0)) {
      // since we're picking something up or putting
      // something down, the display changes and needs
      // to be repainted:
      setNeedsRepaint();
      // if the thing we're picking up is the crown,
      // we're done, the player has won:
      if (myPrincess.collidesWith(myCrown, true)) {
        myCanvas.setGameOver();
        return;
      }
      // keep track of the key we're putting down in
      // order to place it correctly:
      DoorKey oldHeld = myHeldKey;
      myHeldKey = null;
      // if the princess is on top of another key,
      // that one becomes the held key and is hence
      // made invisible:
      for (int i = 0; i < myKeys.length; i++) {
        // we check myHeldKey for null because we don't
        // want to accidentally pick up two keys.
        if ((myPrincess.collidesWith(myKeys[i], true))
            && (myHeldKey == null)) {
          myHeldKey = myKeys[i];
          myHeldKey.setVisible(false);
        }
      }
      if (oldHeld != null) {
        // place the key we're putting down in the Princess's
        // current position and make it visible:
        oldHeld.setPosition(myPrincess.getX(), myPrincess.getY());
        oldHeld.setVisible(true);
      }
    }
  }

  /**
   * Checks of the player hits a stone wall or a door.
   */
  boolean checkCollision() {
    boolean retVal = false;
    // the "true" arg meand to check for a pixel-level
    // collision (so merely an overlap in image
    // squares does not register as a collision)
    if (myPrincess.collidesWith(myBackground, true)) {
      retVal = true;
    } else {
      // Note: it is not necessary to synchronize
      // this block because the thread that calls this
      // method is the same as the one that puts down the
      // keys, so there's no danger of the key being put down
      // between the moment we check for the key and
      // the moment we open the door:
      for (int i = 0; i < myDoors.length; i++) {
        // if she's holding the right key, then open the door
        // otherwise bounce off
        if (myPrincess.collidesWith(myDoors[i], true)) {
          if ((myHeldKey != null)
              && (myDoors[i].getColor() == myHeldKey.getColor())) {
            setNeedsRepaint();
            myDoors[i].setVisible(false);
          } else {
            // if she's not holding the right key, then
            // she has collided with the door just the same
            // as if she had collided with a wall:
            retVal = true;
          }
        }
      }
    }
    return (retVal);
  }

}

/**
 * This class is the display of the game.
 * 
 * @author Carol Hamer
 */

class DungeonCanvas extends GameCanvas {

  //---------------------------------------------------------
  //   dimension fields
  //  (constant after initialization)

  /**
   * the height of the black region below the play area.
   */
  static int TIMER_HEIGHT = 32;

  /**
   * the top corner x coordinate according to this object's coordinate
   * system:.
   */
  static final int CORNER_X = 0;

  /**
   * the top corner y coordinate according to this object's coordinate
   * system:.
   */
  static final int CORNER_Y = 0;

  /**
   * the width of the portion of the screen that this canvas can use.
   */
  static int DISP_WIDTH;

  /**
   * the height of the portion of the screen that this canvas can use.
   */
  static int DISP_HEIGHT;

  /**
   * the height of the font used for this game.
   */
  static int FONT_HEIGHT;

  /**
   * the font used for this game.
   */
  static Font FONT;

  /**
   * color constant
   */
  public static final int BLACK = 0;

  /**
   * color constant
   */
  public static final int WHITE = 0xffffff;

  //---------------------------------------------------------
  //   game object fields

  /**
   * a handle to the display.
   */
  private Display myDisplay;

  /**
   * a handle to the MIDlet object (to keep track of buttons).
   */
  private Dungeon myDungeon;

  /**
   * the LayerManager that handles the game graphics.
   */
  private DungeonManager myManager;

  /**
   * whether or not the game has ended.
   */
  private static boolean myGameOver;

  /**
   * The number of ticks on the clock the last time the time display was
   * updated. This is saved to determine if the time string needs to be
   * recomputed.
   */
  private int myOldGameTicks = 0;

  /**
   * the number of game ticks that have passed since the beginning of the
   * game.
   */
  private int myGameTicks = myOldGameTicks;

  /**
   * we save the time string to avoid recreating it unnecessarily.
   */
  private static String myInitialString = "0:00";

  /**
   * we save the time string to avoid recreating it unnecessarily.
   */
  private String myTimeString = myInitialString;

  //-----------------------------------------------------
  //    gets/sets

  /**
   * This is called when the game ends.
   */
  void setGameOver() {
    myGameOver = true;
    myDungeon.pauseApp();
  }

  /**
   * Find out if the game has ended.
   */
  static boolean getGameOver() {
    return (myGameOver);
  }

  /**
   * Tell the layer manager that it needs to repaint.
   */
  public void setNeedsRepaint() {
    myManager.setNeedsRepaint();
  }

  //-----------------------------------------------------
  //    initialization and game state changes

  /**
   * Constructor sets the data, performs dimension calculations, and creates
   * the graphical objects.
   */
  public DungeonCanvas(Dungeon midlet) throws Exception {
    super(false);
    myDisplay = Display.getDisplay(midlet);
    myDungeon = midlet;
    // calculate the dimensions
    DISP_WIDTH = getWidth();
    DISP_HEIGHT = getHeight();
    if ((!myDisplay.isColor()) || (myDisplay.numColors() < 256)) {
      throw (new Exception("game requires full-color screen"));
    }
    if ((DISP_WIDTH < 150) || (DISP_HEIGHT < 170)) {
      throw (new Exception("Screen too small"));
    }
    if ((DISP_WIDTH > 250) || (DISP_HEIGHT > 250)) {
      throw (new Exception("Screen too large"));
    }
    // since the time is painted in white on black,
    // it shows up better if the font is bold:
    FONT = Font
        .getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM);
    // calculate the height of the black region that the
    // timer is painted on:
    FONT_HEIGHT = FONT.getHeight();
    TIMER_HEIGHT = FONT_HEIGHT + 8;
    // create the LayerManager (where all of the interesting
    // graphics go!) and give it the dimensions of the
    // region it is supposed to paint:
    if (myManager == null) {
      myManager = new DungeonManager(CORNER_X, CORNER_Y, DISP_WIDTH,
          DISP_HEIGHT - TIMER_HEIGHT, this);
    }
  }

  /**
   * This is called as soon as the application begins.
   */
  void start() {
    myGameOver = false;
    myDisplay.setCurrent(this);
    setNeedsRepaint();
  }

  /**
   * sets all variables back to their initial positions.
   */
  void reset() throws Exception {
    // most of the variables that need to be reset
    // are held by the LayerManager:
    myManager.reset();
    myGameOver = false;
    setNeedsRepaint();
  }

  /**
   * sets all variables back to the positions from a previously saved game.
   */
  void revertToSaved() throws Exception {
    // most of the variables that need to be reset
    // are held by the LayerManager, so we
    // prompt the LayerManager to get the
    // saved data:
    myGameTicks = myManager.revertToSaved();
    myGameOver = false;
    myOldGameTicks = myGameTicks;
    myTimeString = formatTime();
    setNeedsRepaint();
  }

  /**
   * save the current game in progress.
   */
  void saveGame() throws Exception {
    myManager.saveGame(myGameTicks);
  }

  /**
   * clears the key states.
   */
  void flushKeys() {
    getKeyStates();
  }

  /**
   * If the game is hidden by another app (or a menu) ignore it since not much
   * happens in this game when the user is not actively interacting with it.
   * (we could pause the timer, but it's not important enough to bother with
   * when the user is just pulling up a menu for a few seconds)
   */
  protected void hideNotify() {
  }

  /**
   * When it comes back into view, just make sure the manager knows that it
   * needs to repaint.
   */
  protected void showNotify() {
    setNeedsRepaint();
  }

  //-------------------------------------------------------
  //  graphics methods

  /**
   * paint the game graphics on the screen.
   */
  public void paint(Graphics g) {
    // color the bottom segment of the screen black
    g.setColor(BLACK);
    g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT - TIMER_HEIGHT, DISP_WIDTH,
        TIMER_HEIGHT);
    // paint the LayerManager (which paints
    // all of the interesting graphics):
    try {
      myManager.paint(g);
    } catch (Exception e) {
      myDungeon.errorMsg(e);
    }
    // draw the time
    g.setColor(WHITE);
    g.setFont(FONT);
    g.drawString("Time: " + formatTime(), DISP_WIDTH / 2, CORNER_Y
        + DISP_HEIGHT - 4, g.BOTTOM | g.HCENTER);
    // write "Dungeon Completed" when the user finishes a board:
    if (myGameOver) {
      myDungeon.setNewCommand();
      // clear the top region:
      g.setColor(WHITE);
      g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT * 2 + 1);
      int goWidth = FONT.stringWidth("Dungeon Completed");
      g.setColor(BLACK);
      g.setFont(FONT);
      g.drawString("Dungeon Completed", (DISP_WIDTH - goWidth) / 2,
          CORNER_Y + FONT_HEIGHT, g.TOP | g.LEFT);
    }
  }

  /**
   * a simple utility to make the number of ticks look like a time...
   */
  public String formatTime() {
    if ((myGameTicks / 16) != myOldGameTicks) {
      myTimeString = "";
      myOldGameTicks = (myGameTicks / 16) + 1;
      int smallPart = myOldGameTicks % 60;
      int bigPart = myOldGameTicks / 60;
      myTimeString += bigPart + ":";
      if (smallPart / 10 < 1) {
        myTimeString += "0";
      }
      myTimeString += smallPart;
    }
    return (myTimeString);
  }

  //-------------------------------------------------------
  //  game movements

  /**
   * update the display.
   */
  void updateScreen() {
    myGameTicks++;
    // paint the display
    try {
      paint(getGraphics());
      flushGraphics(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT);
    } catch (Exception e) {
      myDungeon.errorMsg(e);
    }
  }

  /**
   * Respond to keystrokes.
   */
  public void checkKeys() {
    if (!myGameOver) {
      int vertical = 0;
      int horizontal = 0;
      // determine which moves the user would like to make:
      int keyState = getKeyStates();
      if ((keyState & LEFT_PRESSED) != 0) {
        horizontal = -1;
      }
      if ((keyState & RIGHT_PRESSED) != 0) {
        horizontal = 1;
      }
      if ((keyState & UP_PRESSED) != 0) {
        vertical = -1;
      }
      if ((keyState & DOWN_PRESSED) != 0) {
        // if the user presses the down key,
        // we put down or pick up a key object
        // or pick up the crown:
        myManager.putDownPickUp();
      }
      // tell the manager to move the player
      // accordingly if possible:
      myManager.requestMove(horizontal, vertical);
    }
  }

}

           
       








Related examples in the same category

1.Maze gameMaze game
2.Checkers gameCheckers game
3.Game Action ExampleGame Action Example
4.Game Key EventGame Key Event
5.Sweep GameSweep Game
6.SweepSweep
7.Tumbleweed game
8.Canvas for processing game actionsCanvas for processing game actions