/*
* @(#)PushPuzzleCanvas.java 1.55 02/08/02 @(#)
*
* Copyright (c) 1999-2002 Sun Microsystems, Inc. All rights reserved.
* PROPRIETARY/CONFIDENTIAL
* Use is subject to license terms.
*/
package example.pushpuzzle;
import java.io.*;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import javax.microedition.midlet.*;
import javax.microedition.media.*;
import javax.microedition.media.control.ToneControl;
/**
* PushPuzzleCanvas displays the game board and handles key events.
* The PushPuzzle game logic and algorithms are separated into Board.java.
* PushPuzzleCanvas does not setup or use any Commands. Commands for each
* screen and listeners should be setup outside this class.
* PushPuzzleCanvas generates a SELECT_COMMAND when the current level
* is solved. Sequencing through screens is done in the PushPuzzle MIDlet.
* <p>
* PushPuzzleCanvas handles the reading, initialization, and sequencing
* of individual puzzle screens.
* <p>
* PushPuzzleCanvas uses the Score class to restore and save game levels
* and scores for each level. To display the scores use getScoreScreen.
* It will be initialized with the current scores.
* To select a new level use the getLevelScreen and gotoLevel
* methods.
* <p>
* PushPuzzleCanvas handled key events for LEFT, RIGHT, UP, and DOWN to
* move the pusher in the game board. Pointer pressed events
* are used to move the pusher to the target location (if possible).
* <p>
*/
class PushPuzzleCanvas extends GameCanvas implements Runnable {
/** The current level */
private int level = 1;
/** The current theme index */
private int theme;
/** True if the level has been solved */
private boolean solved;
/** number of pixels per cell (updated by readscreen) */
private int cell = 1;
/** The width of the canvas */
private int width;
/** The height of the canvas */
private int height;
/** The width of the board */
private int bwidth;
/** The height of the board */
private int bheight;
/** The board containing the location of each packet, ground, walls, etc */
private Board board;
/** The score object */
private Score score;
/** The main MIDlet */
private PushPuzzle pushpuzzle;
/** The Display of this MIDlet */
private Display display;
/** The listener used to report solved events */
private CommandListener listener;
/** The form for score display */
private Form scoreForm; // form for scores
/** The TextBox to input new level numbers */
private TextBox levelText; // for input of new level
/** Background color */
private static int groundColor = 0xff8080;
/** The index in the image of the Ground */
public final int TILE_GROUND = 1;
/** The index in the image of the Packet */
public final int TILE_PACKET = 2;
/** The index in the image of the Store */
public final int TILE_STORE = 3;
/** The index in the image of the Wall */
public final int TILE_WALL = 4;
/** The index in the image of the Pusher */
public final int TILE_PUSHER = 5;
/** Background image */
private Image themeImage;
/** Tiles forming the background */
private TiledLayer tiles;
/** The Sprite that is the pusher */
private Sprite sprite;
/** Layer manager */
private LayerManager layers;
/** Thread used for key handling and animation */
private Thread thread;
/** The target cell for runTo */
private int targetx;
/** The target cell for runTo */
private int targety;
/** Pan Rate; number of milliseconds between screen updates */
private static final int PanRate = 50;
/** The Tone player */
private Player tonePlayer;
/** The ToneController */
private ToneControl toneControl;
/** Tune to play when puzzle level is solved. */
private byte[] solvedTune = {
ToneControl.VERSION, 1,
74, 8, // 1/8 note
75, 8,
73, 8
};
/** Tune to play when a packet enters a store */
private byte[] storedTune = {
ToneControl.VERSION, 1,
50, 8, // 1/8 note
60, 8,
70, 8
};
/**
* Construct a new canvas
* @param pushpuzzle the main MIDlet
* @param s the score object
*/
public PushPuzzleCanvas(PushPuzzle pushpuzzle, Score s) {
super(false); // Don't suppress key events
this.pushpuzzle = pushpuzzle;
display = Display.getDisplay(pushpuzzle);
score = s;
board = new Board();
layers = new LayerManager();
setupTheme();
targetx = targety = -1;
height = getHeight();
width = getWidth();
}
/**
* Read the previous level number from the score file.
* Read in the level data.
*/
public void init() {
// Read the last level; if it can't be found, revert to level 0
theme = score.getTheme();
setupTheme();
level = score.getLevel();
if (!readScreen(level)) {
level = 0;
readScreen(level);
}
}
/**
* Cleanup and destroy.
*/
public void destroy() {
hideNotify();
}
/**
* Figure out which set of icons to use based on the colors
*/
private void initColors() {
boolean isColor = display.isColor();
int numColors = display.numColors();
if (isColor) {
} else {
if (numColors > 2) {
} else {
}
}
}
/**
* Change themes.
* Cycle to the next index and try it
*/
public void changeTheme() {
theme++;
setupTheme();
score.setLevel(level, theme); // save the level and theme
setupTiles();
updateSprite(0);
}
/**
* Undo the last move if possible. Redraw the cell
* the pusher occupies after the undone move and the cells
* in the direction of the original move.
* Here so undo can be triggered by a command.
*/
public void undoMove() {
int pos = board.getPusherLocation();
int dir = board.undoMove();
if (dir >= 0) {
updateTilesNear(pos, dir);
updateSprite(dir);
}
solved = board.solved();
}
/**
* Restart the current level.
*/
public void restartLevel() {
readScreen(level);
solved = false;
}
/**
* Start the next level.
* @param offset of the next level
* @return true if the new level was loaded
*/
public boolean nextLevel(int offset) {
updateScores(); // save best scores
if (level + offset >= 0 && readScreen(level+offset)) {
level += offset;
score.setLevel(level, theme);
solved = false;
return true;
}
return false;
}
/**
* Get the current level.
* @return the current level.
*/
public int getLevel() {
return level;
}
/**
* Get a screen to let the user change the level.
* A simple numeric TextBox will do.
* @return the textbox used to change the level number
*/
public Screen getLevelScreen() {
if (levelText == null) {
levelText = new TextBox("Enter Level",
Integer.toString(level), // default
4, TextField.NUMERIC);
} else {
levelText.setString(Integer.toString(level));
}
return levelText;
}
/**
* Go to the chosen Level.
* @return true if the new level was loaded.
*/
public boolean gotoLevel() {
if (levelText != null) {
String s = levelText.getString();
int l = Integer.parseInt(s);
updateScores();
if (l >= 0 && readScreen(l)) {
level = l;
score.setLevel(level, theme);
solved = false;
return true;
}
}
return false;
}
/**
* Read and setup the next level.
* Opens the resource file with the name "/Screen.<lev>"
* and tells the board to read from the stream.
* <STRONG>Must be called only with the board locked.</STRONG>
* @param lev the level number to read.
* @return true if the reading of the level worked, false otherwise.
*/
private boolean readScreen(int lev) {
if (lev <= 0) {
board.screen0(); // Initialize the default zero screen.
} else {
InputStream is = null;
try {
is = getClass().getResourceAsStream(
"/example/pushpuzzle/data/screen."
+ lev);
if (is != null) {
board.read(is, lev);
is.close();
} else {
System.out.println(
"Could not find the game board for level "
+ lev);
return false;
}
} catch (java.io.IOException ex) {
return false;
}
}
bwidth = board.getWidth();
bheight = board.getHeight();
setupTiles();
updateSprite(0);
return true;
}
/**
* Create the Tiled layer to represent the current board.
*/
private void setupTiles() {
if (tiles != null) {
layers.remove(tiles);
}
// Figure out how many cells are needed to cover canvas.
int w = (width + cell - 1) / cell;
int h = (height + cell - 1) / cell;
tiles = new TiledLayer(w > bwidth ? w : bwidth,
h > bheight ? h : bheight,
themeImage, cell, cell);
/** Fill it all with background */
tiles.fillCells(0, 0, w, h, TILE_GROUND);
// Initialize the background tileset
for (int y = 0; y < bheight; y++) {
for (int x = 0; x < bwidth; x++) {
updateTile(x, y);
}
}
layers.append(tiles);
}
/**
* Update the tile at the location.
* @param x the offset of the tile to update
* @param y the offset of the tile to update
*/
private void updateTile(int x, int y) {
int tile = 0;
byte v = board.get(x, y);
switch (v & ~Board.PUSHER) {
case Board.WALL:
tile = TILE_WALL;
break;
case Board.PACKET:
case Board.PACKET | Board.STORE:
tile = TILE_PACKET;
break;
case Board.STORE:
tile = TILE_STORE;
break;
case Board.GROUND:
default:
tile = TILE_GROUND;
}
tiles.setCell(x, y, tile);
}
private static final int GroundColor0 = 0xffffff;
private static final int PacketColor0 = 0xff6d00;
private static final int StoreColor0 = 0xb60055;
private static final int WallColor0 = 0x006D55;
private static final int PusherColor0 = 0x6d6dff;
/**
* Setup Theme-0 generated to match screen
* size and board size.
*/
private void setupTheme0() {
int bwidth = board.getWidth();
int bheight = board.getHeight();
int w = getWidth();
int h = getHeight(); // height of Canvas
cell = ((h-14) / bheight < w / bwidth) ? (h-14) / bheight : w / bwidth;
// Create a mutable image and initialize
themeImage = Image.createImage(cell * 5, cell);
Graphics g = themeImage.getGraphics();
g.setColor(GroundColor0);
g.fillRect((TILE_GROUND - 1 ) * cell, 0, cell*TILE_PUSHER, cell);
g.setColor(PacketColor0);
g.fillRect((TILE_PACKET- 1 ) * cell + 1, 1, cell - 2, cell - 2);
g.setColor(StoreColor0);
g.drawRect((TILE_STORE- 1 ) * cell + 1, 1, cell - 2, cell - 2);
g.setColor(WallColor0);
g.fillRect((TILE_WALL- 1 ) * cell, 0, cell, cell);
g.setColor(PusherColor0);
g.fillArc((TILE_PUSHER- 1 ) * cell, 0, cell, cell, 0, 360);
}
/**
* Setup the theme by reading the images and setting up
* the sprite and picking the tile size.
* Uses the current theme index.
* If the image with the current index can't be found
* retry with theme = 0.
* @param image containing all the frames used for the board.
*/
private void setupTheme() {
if (sprite != null) {
layers.remove(sprite);
sprite = null;
}
if (theme > 0) {
try {
StringBuffer name =
new StringBuffer("/example/pushpuzzle/images/Theme-");
name.append(theme);
name.append(".png");
themeImage = Image.createImage(name.toString());
// Cells are square using the minimum of the width and height
int h = themeImage.getHeight();
int w = themeImage.getWidth();
cell = (w < h) ? w : h;
} catch (IOException e) {
theme = 0;
setupTheme0();
}
} else {
setupTheme0();
}
sprite = new Sprite(themeImage, cell, cell);
sprite.defineReferencePixel(cell/2, cell/2);
int seq[] = new int[] {TILE_PUSHER-1};
sprite.setFrameSequence(seq);
layers.insert(sprite, 0);
}
/**
* Return the Screen to display scores.
* It returns a screen with the current scores.
* @return a screen initialized with the current score information.
*/
public Screen getScoreScreen() {
Form scoreForm = null; // Temp until form can do setItem
int currPushes = board.getPushes();
int bestPushes = score.getPushes();
int currMoves = board.getMoves();
int bestMoves = score.getMoves();
boolean newbest = solved &&
(bestPushes == 0 || currPushes < bestPushes);
scoreForm = new Form(null);
scoreForm.append(new StringItem(
newbest ? "New Best:\n" : "Current:\n",
currPushes + " pushes\n" +
currMoves + " moves"));
scoreForm.append(new StringItem(
newbest ? "Old Best:\n" : "Best:\n",
bestPushes + " pushes\n" +
bestMoves + " moves"));
String title = "Scores";
if (newbest) {
title = "Congratulations";
}
scoreForm.setTitle(title);
return scoreForm;
}
/**
* Handle a repeated arrow keys as though it were another press.
* @param keyCode the key pressed.
*/
protected void keyRepeated(int keyCode) {
int action = getGameAction(keyCode);
switch (action) {
case Canvas.LEFT:
case Canvas.RIGHT:
case Canvas.UP:
case Canvas.DOWN:
keyPressed(keyCode);
break;
default:
break;
}
}
/**
* Handle a single key event.
* The LEFT, RIGHT, UP, and DOWN keys are used to
* move the pusher within the Board.
* Other keys are ignored and have no effect.
* Repaint the screen on every action key.
*/
protected void keyPressed(int keyCode) {
boolean newlySolved = false;
// Protect the data from changing during painting.
synchronized (board) {
cancelTo();
int action = getGameAction(keyCode);
int move = 0;
switch (action) {
case Canvas.LEFT:
move = Board.LEFT;
break;
case Canvas.RIGHT:
move = Board.RIGHT;
break;
case Canvas.DOWN:
move = Board.DOWN;
break;
case Canvas.UP:
move = Board.UP;
break;
// case 0: // Ignore keycode that don't map to actions.
default:
return;
}
// Tell the board to move the piece
int stored = board.getStored();
int dir = board.move(move);
if (stored < board.getStored()) {
// Play a note if a packet hit the spot.
play(storedTune);
}
int pos = board.getPusherLocation();
updateTilesNear(pos, dir);
updateSprite(dir);
} // End of synchronization on the Board.
}
/**
* Update the scores for the current level if it has
* been solved and the scores are better than before.
*/
private void updateScores() {
if (!solved)
return;
int sp = score.getPushes();
int bp = board.getPushes();
int bm = board.getMoves();
/*
* Update the scores. If the score for this level is lower
* than the last recorded score save the lower scores.
*/
if (sp == 0 || bp < sp) {
score.setLevelScore(bp, bm);
}
}
/**
* Cancel the animation.
*/
private void cancelTo() {
targetx = -1;
targety = -1;
}
/**
* Called when the pointer is pressed.
* Record the target for the pusher.
* @param x location in the Canvas
* @param y location in the Canvas
*/
protected void pointerPressed(int x, int y) {
targetx = (x - tiles.getX()) / cell;
targety = (y - tiles.getY()) / cell;
}
/**
* Add a listener to notify when the level is solved.
* The listener is send a List.SELECT_COMMAND when the
* level is solved.
* @param l the object implementing interface CommandListener
*/
public void setCommandListener(CommandListener l) {
super.setCommandListener(l);
listener = l;
}
/**
* Update the Sprite location from the board supplied position
* @param dir the sprite is moving
*/
private void updateSprite(int dir) {
int loc = board.getPusherLocation();
int x = (loc & 0x7fff) * cell;
int y = ((loc >> 16) & 0x7fff) * cell;
// Update sprite location
sprite.setPosition(tiles.getX() + x, tiles.getY() + y);
dir = Board.RIGHT; // BUG: Graphics.drawRegion doesn't do xofrm
switch (dir & 0x03) {
case Board.LEFT:
sprite.setTransform(Sprite.TRANS_ROT180);
break;
case Board.UP:
sprite.setTransform(Sprite.TRANS_ROT90);
break;
case Board.DOWN:
sprite.setTransform(Sprite.TRANS_ROT270);
break;
default:
sprite.setTransform(Sprite.TRANS_NONE);
break;
}
}
/**
* Queue a repaint for an area around the specified location.
* @param loc an encoded location from Board.getPusherLocation
* @param dir that the pusher moved and flag if it pushed a packet
*/
void updateTilesNear(int loc, int dir) {
int x = loc & 0x7fff;
int y = (loc >> 16) & 0x7fff;
// Update cells if any were moved
if (dir >= 0 && ((dir & Board.MOVEPACKET) != 0)) {
updateTile(x, y);
updateTile(x+1, y);
updateTile(x-1, y);
updateTile(x, y+1);
updateTile(x, y-1);
}
}
/**
* Paint the contents of the Canvas.
* The clip rectangle of the canvas is retrieved and used
* to determine which cells of the board should be repainted.
* @param g Graphics context to paint to.
*/
public void paint(Graphics g) {
flushGraphics();
}
/**
* The canvas is being displayed.
* Stop the event handling and animation thread.
*/
protected void showNotify() {
thread = new Thread(this);
thread.start();
}
/**
* The canvas is being removed from the screen.
* Stop the event handling and animation thread.
*/
protected void hideNotify() {
thread = null;
}
/**
* The main event processor. Events are polled and
* actions taken based on the directional events.
*/
public void run() {
Graphics g = getGraphics(); // Of the buffered screen image
Thread mythread = Thread.currentThread();
// Loop handling events
while (mythread == thread) {
try { // Start of exception handler
boolean newlySolved = false;
if (!solved && board.solved()) {
newlySolved = solved = true;
play(solvedTune);
}
if (newlySolved && listener != null) {
listener.commandAction(List.SELECT_COMMAND, this);
}
if (targetx >= 0 && targety >= 0) {
int dir = board.runTo(targetx, targety, 1);
int pos = board.getPusherLocation();
if (dir < 0) {
targetx = targety = -1; // Cancel target
} else {
updateTilesNear(pos, dir);
updateSprite(dir);
}
}
// Check that the pusher is not to close to the edge
int loc = board.getPusherLocation();
int x = (loc & 0x7fff) * cell;
int y = ((loc >> 16) & 0x7fff) * cell;
int lx = tiles.getX();
int ly = tiles.getY();
int panScale = cell / 4;
if (panScale < 1)
panScale = 1;
// If the sprite is too near the edge (or off) pan
if (lx + x > width - cell - cell ) {
tiles.move(-panScale, 0);
sprite.move(-panScale, 0);
}
if (lx + x < cell) {
tiles.move(panScale, 0);
sprite.move(panScale, 0);
}
if (ly + y > height - cell - cell) {
tiles.move(0, -panScale);
sprite.move(0, -panScale);
}
if (ly + y < cell) {
tiles.move(0, panScale);
sprite.move(0, panScale);
}
// Draw all the layers and flush
layers.paint(g, 0, 0);
if (mythread == thread) {
flushGraphics();
}
// g.drawString("PushPuzzle Level " + level, 0, height,
// Graphics.BOTTOM|Graphics.LEFT);
try {
mythread.sleep(PanRate);
} catch (java.lang.InterruptedException e) {
// Ignore
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Play the simple tune supplied.
*/
void play(byte[] tune) {
try {
if (tonePlayer == null) {
// First time open the tonePlayer
tonePlayer = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR);
tonePlayer.realize();
toneControl = (ToneControl)tonePlayer.getControl("javax.microedition.media.control.ToneControl");
}
tonePlayer.deallocate();
toneControl.setSequence(tune);
tonePlayer.start();
} catch (Exception ex){
ex.printStackTrace();
}
}
/*
* Close the tune player and release resources.
*/
void closePlayer() {
if ( tonePlayer != null ) {
toneControl = null;
tonePlayer.close();
tonePlayer = null;
}
}
}
|