MIDP 2.0: Games- Sheepdog

This section provides the source code for the Sheepdog example. For the complete Eclipse project ZIP file, see Forum Nokia.

The example includes the following classes:

SheepdogMIDlet

// unnamed package

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import javax.microedition.rms.*;
import java.util.*;
import java.io.*;


public class SheepdogMIDlet
    extends MIDlet
    implements Runnable
{
    private static final String RS_NAME = "BESTTIME";
    private MenuList menuList;
    private SheepdogCanvas sheepdogCanvas;
    private boolean initDone = false;
    private static final Random random = new Random();
    private boolean hasBestTime = false;
    private long bestTime;


    public SheepdogMIDlet()
    {
    }

    
    public void startApp()
    {
        Displayable current = Display.getDisplay(this).getCurrent();
        if (current == null)
        {
            // first time we've been called
            Display.getDisplay(this).setCurrent(new SplashScreen(this));
        }
        else
        {
            if (current == sheepdogCanvas)
            {
                sheepdogCanvas.start();   // start its animation thread
            }
            Display.getDisplay(this).setCurrent(current);
        }
    }


    public void pauseApp()
    {
        Displayable current = Display.getDisplay(this).getCurrent();
        if (current == sheepdogCanvas)
        {
            sheepdogCanvas.stop();   // kill its animation thread
        }
    }


    public void destroyApp(boolean unconditional)
    {
        if (sheepdogCanvas != null)
        {
            sheepdogCanvas.stop();   // kill its animation thread
        }
    }


    private void quit()
    {
        destroyApp(false);
        notifyDestroyed();
    }


    public void run()
    {
        init();
    }


    private synchronized void init()
    {
        if (!initDone)
        {
            readRecordStore();
            SoundEffects.getInstance();
            menuList = new MenuList(this);
            sheepdogCanvas = new SheepdogCanvas(this);
            initDone = true;
        }
    }


    void splashScreenPainted()
    {
        new Thread(this).start();   // start background initialization
    }


    void splashScreenDone()
    {
        init();   // if not already done
        Display.getDisplay(this).setCurrent(menuList);
    }


    void menuListContinue()
    {
        Display.getDisplay(this).setCurrent(sheepdogCanvas);
        sheepdogCanvas.start();
    }


    void menuListNewGame()
    {
        sheepdogCanvas.init();
        Display.getDisplay(this).setCurrent(sheepdogCanvas);
        sheepdogCanvas.start();
    }


    void menuListInstructions()
    {
        // create and discard a new Instructions screen each time, to
        // avoid keeping heap memory for it when it's not in use
        Display.getDisplay(this).setCurrent(new InstructionsScreen(this));
    }


    void menuListHighScore()
    {
        // create and discard a new High Score screen each time, to
        // avoid keeping heap memory for it when it's not in use
        Display.getDisplay(this).setCurrent(new HighScoreScreen(this));
    }


    void menuListQuit()
    {
        quit();
    }


    void sheepdogCanvasMenu()
    {
        sheepdogCanvas.stop();
        menuList.setGameActive(true);
        Display.getDisplay(this).setCurrent(menuList);
    }


    void sheepdogCanvasGameOver(long time)
    {
        sheepdogCanvas.stop();
        menuList.setGameActive(false);
        Display.getDisplay(this).setCurrent(new GameOverScreen(this, time));
    }


    void gameOverDone()
    {
        Display.getDisplay(this).setCurrent(menuList);
    }


    void instructionsBack()
    {
        Display.getDisplay(this).setCurrent(menuList);
    }


    void highScoreBack()
    {
        Display.getDisplay(this).setCurrent(menuList);
    }


    // method needed by lots of classes, shared by putting it here
    static Image createImage(String filename)
    {
        Image image = null;
        try
        {
            image = Image.createImage(filename);
        }
        catch (java.io.IOException ex)
        {
            // just let return value be null
        }
        return image;
    }


    // method needed by lots of classes, shared by putting it here
    static int random(int size)
    {
        return (random.nextInt() & 0x7FFFFFFF) % size;
    }


    // only the MIDlet has access to the display, so put this method here
    void flashBacklight(int millis)
    {
        Display.getDisplay(this).flashBacklight(millis);
    }


    // only the MIDlet has access to the display, so put this method here
    void vibrate(int millis)
    {
        Display.getDisplay(this).vibrate(millis);
    }


    long getBestTime()
    {
        return hasBestTime ? bestTime : -1;
    }


    boolean checkBestTime(long time)
    {
        if (!hasBestTime || (time < bestTime))
        {
            hasBestTime = true;
            bestTime = time;
            writeRecordStore();
            return true;
        }
        else
        {
            return false;
        }
    }


    private void readRecordStore()
    {
        hasBestTime = false;
        RecordStore rs = null;
        ByteArrayInputStream bais = null;
        DataInputStream dis = null;
        try
        {
            rs = RecordStore.openRecordStore(RS_NAME, false);
            byte[] data = rs.getRecord(1);
            bais = new ByteArrayInputStream(data);
            dis = new DataInputStream(bais);
            bestTime = dis.readLong();
            hasBestTime = true;
        }
        catch (IOException ex)
        {
            // hasBestTime will still be false
        }
        catch (RecordStoreException ex)
        {
            // hasBestTime will still be false
        }
        finally
        {
            if (dis != null)
            {
                try
                {
                    dis.close();
                }
                catch (IOException ex)
                {
                    // no error handling necessary here
                }
            }
            if (bais != null)
            {
                try
                {
                    bais.close();
                }
                catch (IOException ex)
                {
                    // no error handling necessary here
                }
            }
            if (rs != null)
            {
                try
                {
                    rs.closeRecordStore();
                }
                catch (RecordStoreException ex)
                {
                    // no error handling necessary here
                }
            }
        }
    }


    // this will only be called when we have a best time
    private void writeRecordStore()
    {
        RecordStore rs = null;
        ByteArrayOutputStream baos = null;
        DataOutputStream dos = null;
        try
        {
            rs = RecordStore.openRecordStore(RS_NAME, true);
            baos = new ByteArrayOutputStream();
            dos = new DataOutputStream(baos);
            dos.writeLong(bestTime);
            byte[] data = baos.toByteArray();
            if (rs.getNumRecords() == 0)
            {
                // new record store
                rs.addRecord(data, 0, data.length);
            }
            else
            {
                // existing record store: will have one record, id 1
                rs.setRecord(1, data, 0, data.length);
            }
        }
        catch (IOException ex)
        {
            // just leave the best time unrecorded
        }
        catch (RecordStoreException ex)
        {
            // just leave the best time unrecorded
        }
        finally
        {
            if (dos != null)
            {
                try
                {
                    dos.close();
                }
                catch (IOException ex)
                {
                    // no error handling necessary here
                }
            }
            if (baos != null)
            {
                try
                {
                    baos.close();
                }
                catch (IOException ex)
                {
                    // no error handling necessary here
                }
            }
            if (rs != null)
            {
                try
                {
                    rs.closeRecordStore();
                }
                catch (RecordStoreException ex)
                {
                    // no error handling necessary here
                }
            }
        }
    }
}

Field

// unnamed package

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


class Field
    extends TiledLayer
{
    private static final int WIDTH_IN_TILES = 12;
    private static final int HEIGHT_IN_TILES = 12;
    private static final int TILE_WIDTH = 16;
    private static final int TILE_HEIGHT = 16;

    private static int[][] cellTiles =
        {{-3, -2, -3, -1, -2, -1, -3, -1, -2, -3, -1, -2},
         {-2,  3,  4,  3,  1,  2,  3,  2,  1,  5,  2, -3},
         {-1,  2,  1,  2,  3,  4,  5,  3,  2,  4,  3, -1},
         {-2,  1,  4,  9,  9,  9,  9,  4,  5,  2,  1, -2},
         {-3,  3,  5,  9, 10, 10, 10,  2,  1,  3,  5, -1},
         {-2,  2,  3,  9, 10, 10, 10,  5,  4,  2,  1, -3},
         {-1,  4,  2,  9,  9,  9,  9,  3,  1,  3,  2, -2},
         {-3,  2,  5,  1,  3,  1,  4,  2,  5,  4,  3, -3},
         {-2,  1,  4,  2,  5,  2,  3,  4,  2,  1,  2, -1},
         {-1,  5,  1,  4,  3,  4,  1,  2,  3,  4,  1, -2},
         {-3,  2,  4,  5,  2,  3,  2,  4,  1,  2,  3, -3},
         {-2, -3, -2, -1, -2, -1, -3, -2, -1, -3, -1, -2}};
    private static int FOLD_TILE = 10;
    private static int FENCE_TILE = 9;
    private static int[][] waterFrames = {{6, 7, 8}, {7, 8, 6}, {8, 6, 7}};

    private int tickCount = 0;


    Field()
    {
        super(WIDTH_IN_TILES,
              HEIGHT_IN_TILES,
              SheepdogMIDlet.createImage("/field.png"),
              TILE_WIDTH,
              TILE_HEIGHT);

        createAnimatedTile(waterFrames[0][0]);      // tile -1
        createAnimatedTile(waterFrames[1][0]);      // tile -2
        createAnimatedTile(waterFrames[2][0]);      // tile -3

        for (int row = 0; row < HEIGHT_IN_TILES; ++row)
        {
            for (int column = 0; column < WIDTH_IN_TILES; ++column)
            {
                setCell(column, row, cellTiles[row][column]);
            }
        }
    }


    int getSheepdogStartX()
    {
        return getWidth() - 50;
    }


    int getSheepdogStartY()
    {
        return getHeight() - 50;
    }


    void tick()
    {
        int tickState = (tickCount++ >> 3);   // slow down x8
        int tile = tickState % 3;
        setAnimatedTile(-1 - tile, waterFrames[tile][(tickState % 9) / 3]);
    }


    // return true if any part of the rectangle overlaps a water tile
    // or the fence
    boolean containsImpassableArea(int x, int y, int width, int height)
    {
        int rowMin = y / TILE_HEIGHT;
        int rowMax = (y + height - 1) / TILE_HEIGHT;
        int columnMin = x / TILE_WIDTH;
        int columnMax = (x + width - 1) / TILE_WIDTH;

        for (int row = rowMin; row <= rowMax; ++row)
        {
            for (int column = columnMin; column <= columnMax; ++column)
            {
                int cell = getCell(column, row);
                if ((cell < 0) || (cell == FENCE_TILE))
                {
                    return true;
                }
            }
        }

        return false;
    }


    // returns true if every pixel of the sprite is in the fold
    boolean inFold(Sprite s)
    {
        // we can assume that the sprite's reference pixel is unchanged
        int rowMin = s.getY() / TILE_HEIGHT;
        int rowMax = (s.getY() + s.getHeight() - 1) / TILE_HEIGHT;
        int columnMin = s.getX() / TILE_WIDTH;
        int columnMax = (s.getX() + s.getWidth() - 1) / TILE_WIDTH;

        for (int row = rowMin; row <= rowMax; ++row)
        {
            for (int column = columnMin; column <= columnMax; ++column)
            {
                if (getCell(column, row) != FOLD_TILE)
                {
                    return false;
                }
            }
        }

        return true;
    }
}

GameOverScreen

// unnamed package

import javax.microedition.lcdui.*;


class GameOverScreen
    extends Canvas
{
    private final SheepdogMIDlet midlet;
    private boolean wasBestTime;
    private long time;
    private long bestTime;


    GameOverScreen(SheepdogMIDlet midlet, long time)
    {
        super();
        this.midlet = midlet;
        this.time = time;
        setFullScreenMode(true);

        if (midlet.checkBestTime(time))
        {
            wasBestTime = true;
            bestTime = time;
            SoundEffects.getInstance().startHighScoreSound();
        }
        else
        {
            wasBestTime = false;
            bestTime = midlet.getBestTime();
            SoundEffects.getInstance().startGameOverSound();
        }
        midlet.flashBacklight(1000);    // 1 second
    }


    public void paint(Graphics g)
    {
        int width = getWidth();
        int height = getHeight();

        // clear screen to green
        g.setColor(0x0000FF00);
        g.fillRect(0, 0, width, height);

        // Write message. We use a trick to make outlined text: we draw it
        // offset one pixel to the top, bottom, left & right in white, then
        // centred in black.
        g.setFont(Font.getFont(Font.FACE_PROPORTIONAL,
                               Font.STYLE_BOLD,
                               Font.SIZE_LARGE));
        int centerX = width / 2;
        int centerY = height / 2;
        g.setColor(0x00FFFFFF);   // white
        drawText(g, centerX, centerY - 1);
        drawText(g, centerX, centerY + 1);
        drawText(g, centerX - 1, centerY);
        drawText(g, centerX + 1, centerY);
        g.setColor(0x00000000);   // black
        drawText(g, centerX, centerY);
    }


    private void drawText(Graphics g, int centerX, int centerY)
    {
        int fontHeight = g.getFont().getHeight();
        int textHeight = 3 * fontHeight;
        int topY = centerY - textHeight / 2;

        g.drawString("GAME OVER",
                     centerX,
                     topY,
                     Graphics.HCENTER | Graphics.TOP);
        g.drawString("Time: " + time + "s",
                     centerX,
                     topY + fontHeight,
                     Graphics.HCENTER | Graphics.TOP);
        g.drawString(wasBestTime ? "New best time!" :
                                   ("Best time: " + bestTime + "s"),
                     centerX,
                     topY + 2 * fontHeight,
                     Graphics.HCENTER | Graphics.TOP);
    }
    

    public void keyPressed(int keyCode)
    {
        midlet.gameOverDone();
    }
}

HighScoreScreen

// unnamed package

import javax.microedition.lcdui.*;


class HighScoreScreen
    extends Form
    implements CommandListener
{
    private final SheepdogMIDlet midlet;
    private final Command backCommand;


    HighScoreScreen(SheepdogMIDlet midlet)
    {
        super("High score");
        this.midlet = midlet;

        long bestTime = midlet.getBestTime(); 
        String text = (bestTime == -1) ? "none yet"
                                       : (Long.toString(bestTime) + "s");
        append(new StringItem("Best time", text));

        backCommand = new Command("Back", Command.BACK, 1);
        addCommand(backCommand);
        setCommandListener(this);
    }
    

    public void commandAction(Command c, Displayable d)
    {
        midlet.highScoreBack();
    }
}

InstructionsScreen

// unnamed package

import javax.microedition.lcdui.*;


class InstructionsScreen
    extends Form
    implements CommandListener
{
    private final SheepdogMIDlet midlet;
    private final Command backCommand;

    private static final String instructions =
        "Herd the sheep into the fold as quickly as you can.\n" +
        "Sheep won't leave the fold once they've entered it.\n" +
        "If they're not behaving, bark using the Fire key!";


    InstructionsScreen(SheepdogMIDlet midlet)
    {
        super("Instructions");
        this.midlet = midlet;

        append(new StringItem(null, instructions));

        backCommand = new Command("Back", Command.BACK, 1);
        addCommand(backCommand);
        setCommandListener(this);
    }
    

    public void commandAction(Command c, Displayable d)
    {
        midlet.instructionsBack();
    }
}

MenuList

// unnamed package

import javax.microedition.lcdui.*;


class MenuList
    extends List
    implements CommandListener
{
    private SheepdogMIDlet midlet;
    private Command exitCommand;
    private boolean gameActive = false;


    MenuList(SheepdogMIDlet midlet)
    {
        super("Sheepdog", List.IMPLICIT);
        this.midlet = midlet;

        append("New game", null);
        append("High score", null);
        append("Instructions", null);

        exitCommand = new Command("Exit", Command.EXIT, 1);
        addCommand(exitCommand);
        
        setCommandListener(this);
    }


    void setGameActive(boolean active)
    {
        if (active && !gameActive)
        {
            gameActive = true;
            insert(0, "Continue", null);
        }
        else if (!active && gameActive)
        {
            gameActive = false;
            delete(0);
        }
    }


    public void commandAction(Command c, Displayable d)
    {
        if (c == List.SELECT_COMMAND)
        {
            int index = getSelectedIndex();
            if (index != -1)  // should never be -1
            {
                if (!gameActive)
                {
                    index++;
                }
                switch (index)
                {
                case 0:   // Continue
                    midlet.menuListContinue();
                    break;
                case 1:   // New game
                    midlet.menuListNewGame();
                    break;
                case 2:   // High score
                    midlet.menuListHighScore();
                    break;
                case 3:
                    midlet.menuListInstructions();
                    break;
                default:
                    // can't happen
                    break;
                }
            }
        }
        else if (c == exitCommand)
        {
            midlet.menuListQuit();
        }
    }
}

Sheep

// unnamed package

import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;


class Sheep
    extends Sprite
{
    static final int WIDTH = 15;
    static final int HEIGHT = 15;

    private final SheepdogCanvas canvas;
    private int[][][] animations = {{{0},                // stand up
                                     {1, 2, 3, 4}},      // run up
                                    {{5},                // stand left
                                     {6, 7, 8, 9}},      // run left
                                    {{10},               // stand down
                                     {11, 12, 13, 14}}}; // run down
    private int animationTick;
    private static int numSheep = 0;
    private static final int STAND = 0;
    private static final int RUN = 1;
    private int currentDirection = SheepdogCanvas.DOWN;

    private final int flockFactor;
    private final int minDogFactor;
    private final int maxDogFactor;
    private int dogFactor;

    Sheep(SheepdogCanvas canvas)
    {
        super(SheepdogMIDlet.createImage("/sheep.png"), WIDTH, HEIGHT);
        defineCollisionRectangle(2, 2, WIDTH-4, WIDTH-4);
        defineReferencePixel(WIDTH/2, HEIGHT/2);

        this.canvas = canvas;
        animationTick = numSheep++;

        flockFactor = 100 + SheepdogMIDlet.random(100);
        minDogFactor = SheepdogMIDlet.random(20);
        maxDogFactor = minDogFactor + 10;
        dogFactor = minDogFactor;
    }


    void tick()
    {
        // sheep are 4x as slow as dogs
        if ((animationTick++ % 4) != 0)
        {
            return;
        }

        // adjust dog factor
        adjustDogFactor();

        // ARTIFICIAL INTELLIGENCE SECTION
        // - wants to move away from dog, if dog is close
        // - wants to move closer to flock (average position of other
        //   sheep) if they are close
        // - if preferred direction is diagonal and major direction is
        //   blocked, take minor direction
        // - each sheep varies in how much it's scared of the dog, and
        //   how much it wants to flock
        // We do this by calculating a weighted direction vector

        // First calculate dog effect
        Sheepdog sheepdog = canvas.getSheepdog();
        int dx = getX() - sheepdog.getX();
        int dy = getY() - sheepdog.getY();
        int sumsq = dx * dx + dy * dy;
        Field field = canvas.getField();
        int dogEffectX =
            dogFactor * dx * field.getWidth() * field.getWidth() / sumsq;
        int dogEffectY =
            dogFactor * dy * field.getHeight() * field.getHeight() / sumsq;

        // Next calculate flock effect
        int flockDx = 0;
        int flockDy = 0;
        Vector sheep = canvas.getSheep();
        for (int i = 0; i < sheep.size(); ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));
            if (sh != this)
            {
                flockDx += getX() - sh.getX();
                flockDy += getY() - sh.getY();
            }
        }
        int flockEffectX = (flockDx * flockFactor) / (sheep.size() - 1);
        int flockEffectY = (flockDy * flockFactor) / (sheep.size() - 1);

        // Now calculate total effect
        int totalEffectX = dogEffectX - flockEffectX;
        int totalEffectY = dogEffectY - flockEffectY;

        // Determine preferred directions
        int firstDirection;
        int secondDirection;
        int thirdDirection;
        if (Math.abs(totalEffectY) > Math.abs(totalEffectX))
        {
            // Prefer to move vertically
            if (totalEffectY > 0)
            {
                firstDirection = SheepdogCanvas.DOWN;
            }
            else
            {
                firstDirection = SheepdogCanvas.UP;
            }
            if (totalEffectX > 0)
            {
                secondDirection = SheepdogCanvas.RIGHT;
                thirdDirection = SheepdogCanvas.NONE;
            }
            else if (totalEffectX < 0)
            {
                secondDirection = SheepdogCanvas.LEFT;
                thirdDirection = SheepdogCanvas.NONE;
            }
            else  // totalEffectX == 0
            {
                if (SheepdogMIDlet.random(2) == 0)
                {
                    secondDirection = SheepdogCanvas.LEFT;
                    thirdDirection = SheepdogCanvas.RIGHT;
                }
                else
                {
                    secondDirection = SheepdogCanvas.RIGHT;
                    thirdDirection = SheepdogCanvas.LEFT;
                }
            }
        }
        else
        {
            // Prefer to move horizontally
            if (totalEffectX > 0)
            {
                firstDirection = SheepdogCanvas.RIGHT;
            }
            else
            {
                firstDirection = SheepdogCanvas.LEFT;
            }
            if (totalEffectY > 0)
            {
                secondDirection = SheepdogCanvas.DOWN;
                thirdDirection = SheepdogCanvas.NONE;
            }
            else if (totalEffectY < 0)
            {
                secondDirection = SheepdogCanvas.UP;
                thirdDirection = SheepdogCanvas.NONE;
            }
            else  // totalEffectY == 0
            {
                if (SheepdogMIDlet.random(2) == 0)
                {
                    secondDirection = SheepdogCanvas.UP;
                    thirdDirection = SheepdogCanvas.DOWN;
                }
                else
                {
                    secondDirection = SheepdogCanvas.DOWN;
                    thirdDirection = SheepdogCanvas.UP;
                }
            }
        }

        // if we can move in the preferred directions, do so, else stand
        // facing the dog
        if (tryMove(firstDirection) ||
            tryMove(secondDirection) ||
            ((thirdDirection != SheepdogCanvas.NONE) &&
             tryMove(thirdDirection)))
        {
            advanceRunningAnimation();
        }
        else
        {
            if (Math.abs(dx) > Math.abs(dy))
            {
                if (dx > 0)
                {
                    currentDirection = SheepdogCanvas.LEFT;
                }
                else
                {
                    currentDirection = SheepdogCanvas.RIGHT;
                }
            }
            else
            {
                if (dy > 0)
                {
                    currentDirection = SheepdogCanvas.UP;
                }
                else
                {
                    currentDirection = SheepdogCanvas.DOWN;
                }
            }
            setStandingAnimation();
        }

        // Will baa occasionally if dog is close. Dog distance ranges from
        // about 11 minimum to double width of field
        int dogDistance = Math.abs(dx) + Math.abs(dy);
        if (SheepdogMIDlet.random(dogDistance - 10) == 0)
        {
            SoundEffects.getInstance().startSheepSound();
        }
    }


    private void adjustDogFactor()
    {
        dogFactor += SheepdogMIDlet.random(4) - 2;  // -2..1
        if (dogFactor < minDogFactor)
        {
            dogFactor = minDogFactor;
        }
        else if (dogFactor > maxDogFactor)
        {
            dogFactor = maxDogFactor;
        }
    }


    private boolean tryMove(int direction)
    {
        Field field = canvas.getField();
        boolean blocked = true;
        int dx = 0;
        int dy = 0;
        switch (direction)
        {
        case SheepdogCanvas.UP:
            if ((getY() > 0) &&
                !field.containsImpassableArea(getX(),
                                              getY() - 1,
                                              getWidth(),
                                              1))
            {
                blocked = false;
                dy = -1;
            }
            break;
        case SheepdogCanvas.LEFT:
            if ((getX() > 0) &&
                !field.containsImpassableArea(getX() - 1,
                                              getY(),
                                              1,
                                              getHeight()))
            {
                blocked = false;
                dx = -1;
            }
            break;
        case SheepdogCanvas.DOWN:
            if ((getY() + getHeight() - 1 < field.getWidth()) &&
                !field.containsImpassableArea(getX(),
                                              getY() + getHeight(),
                                              getWidth(),
                                              1))
            {
                blocked = false;
                dy = 1;
            }
            break;
        case SheepdogCanvas.RIGHT:
            if ((getX() + getWidth() - 1 < field.getWidth()) &&
                !field.containsImpassableArea(getX() + getWidth(),
                                              getY(),
                                              1,
                                              getHeight()))
            {
                blocked = false;
                dx = 1;
            }
            break;
        default:
            // can't happen
            break;
        }

        boolean success = false;
        if (!blocked)
        {
            boolean wasInFold = field.inFold(this);
            move(dx, dy);
            if (canvas.overlapsOtherSheep(this) ||
                canvas.overlapsSheepdog(this) ||
                (wasInFold && !field.inFold(this)))
            {
                move(-dx, -dy);
            }
            else
            {
                currentDirection = direction;
                success = true;
            }
        }

        return success;
    }


    private void advanceRunningAnimation()
    {
        int[] sequence;
        if (currentDirection == SheepdogCanvas.RIGHT)
        {
            sequence = animations[SheepdogCanvas.LEFT][RUN];
            setTransform(TRANS_MIRROR);
        }
        else
        {
            sequence = animations[currentDirection][RUN];
            setTransform(TRANS_NONE);
        }
        setFrame(sequence[(animationTick >> 2) % sequence.length]);
    }


    private void setStandingAnimation()
    {
        if (currentDirection == SheepdogCanvas.RIGHT)
        {
            setFrame(animations[SheepdogCanvas.LEFT][STAND][0]);
            setTransform(TRANS_MIRROR);
        }
        else
        {
            setFrame(animations[currentDirection][STAND][0]);
            setTransform(TRANS_NONE);
        }
    }


    void handleDogBark()
    {
        // sheep should get nervous
        dogFactor += 5;
        if (dogFactor > maxDogFactor)
        {
            dogFactor = maxDogFactor;
        }
    }
}

Sheepdog

// unnamed package

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


class Sheepdog
    extends Sprite
{
    static final int WIDTH = 15;
    static final int HEIGHT = 15;
    static final int VIBRATION_MILLIS = 200;

    private final SheepdogCanvas canvas;
    private boolean barking = false;
    private int[][][] animations = {{{0},                // stand up
                                     {1, 2, 3, 4}},      // run up
                                    {{5},                // stand left
                                     {6, 7, 8, 9}},      // run left
                                    {{10},               // stand down
                                     {11, 12, 13, 14}}}; // run down
    private int animationTick = 0;
    private static final int STAND = 0;
    private static final int RUN = 1;
    private int currentDirection = SheepdogCanvas.LEFT;

    Sheepdog(SheepdogCanvas canvas)
    {
        super(SheepdogMIDlet.createImage("/dog.png"), WIDTH, HEIGHT);
        defineCollisionRectangle(2, 2, WIDTH-4, WIDTH-4);
        defineReferencePixel(WIDTH/2, HEIGHT/2);

        this.canvas = canvas;
    }


    void tick(int direction, boolean bark)
    {
        animationTick++;

        Field field = canvas.getField();
        boolean moving = false;;
        switch (direction)
        {
        case SheepdogCanvas.UP:
            currentDirection = direction;
            if ((getY() > 0) &&
                !field.containsImpassableArea(getX(),
                                              getY() - 1,
                                              getWidth(),
                                              1) &&
                moveSuccessfully(0, -1))
            {
                moving = true;
            }
            else
            {
                canvas.vibrate(VIBRATION_MILLIS);
            }
            break;
        case SheepdogCanvas.LEFT:
            currentDirection = direction;
            if ((getX() > 0) &&
                !field.containsImpassableArea(getX() - 1,
                                              getY(),
                                              1,
                                              getHeight()) &&
                moveSuccessfully(-1, 0))
            {
                moving = true;
            }
            else
            {
                canvas.vibrate(VIBRATION_MILLIS);
            }
            break;
        case SheepdogCanvas.DOWN:
            currentDirection = direction;
            if ((getY() + getHeight() < field.getWidth()) &&
                !field.containsImpassableArea(getX(),
                                              getY() + getHeight(),
                                              getWidth(),
                                              1) &&
                moveSuccessfully(0, 1))
            {
                moving = true;
            }
            else
            {
                canvas.vibrate(VIBRATION_MILLIS);
            }
            break;
        case SheepdogCanvas.RIGHT:
            currentDirection = direction;
            if ((getX() + getWidth() < field.getWidth()) &&
                !field.containsImpassableArea(getX() + getWidth(),
                                              getY(),
                                              1,
                                              getHeight()) &&
                moveSuccessfully(1, 0))
            {
                moving = true;
            }
            else
            {
                canvas.vibrate(VIBRATION_MILLIS);
            }
            break;
        default:  // must be NONE
            break;
        }

        if (moving)
        {
            advanceRunningAnimation();
        }
        else
        {
            setStandingAnimation();
        }

        // implement a toggle, so bark only happens once per click
        // (will therefore not register very rapid multiple-clicks)
        if (bark)
        {
            if (!barking)
            {
                SoundEffects.getInstance().startDogSound();
                barking = true;
                canvas.handleDogBark();
            }
        }
        else
        {
            barking = false;
        }
    }


    private boolean moveSuccessfully(int dx, int dy)
    {
        move(dx, dy);
        if (canvas.overlapsSheep(this))
        {
            move(-dx, -dy);
            return false;
        }
        else
        {
            return true;
        }
    }


    private void advanceRunningAnimation()
    {
        int[] sequence;
        if (currentDirection == SheepdogCanvas.RIGHT)
        {
            sequence = animations[SheepdogCanvas.LEFT][RUN];
            setTransform(TRANS_MIRROR);
        }
        else
        {
            sequence = animations[currentDirection][RUN];
            setTransform(TRANS_NONE);
        }
        setFrame(sequence[(animationTick >> 1) % sequence.length]);
    }


    private void setStandingAnimation()
    {
        if (currentDirection == SheepdogCanvas.RIGHT)
        {
            setFrame(animations[SheepdogCanvas.LEFT][STAND][0]);
            setTransform(TRANS_MIRROR);
        }
        else
        {
            setFrame(animations[currentDirection][STAND][0]);
            setTransform(TRANS_NONE);
        }
    }
}

SheepdogCanvas

// unnamed package

import java.util.Random;
import java.util.Vector;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import javax.microedition.media.*;
import java.io.*;


class SheepdogCanvas
    extends GameCanvas
    implements Runnable
{
    // shared direction constants
    static final int NONE = -1;
    static final int UP = 0;
    static final int LEFT = 1;
    static final int DOWN = 2;
    static final int RIGHT = 3;

    private static final int MILLIS_PER_TICK = 50;
    private static final int NUM_SHEEP = 5;
    
    private final SheepdogMIDlet midlet;
    private final Field field;
    private final Sheepdog sheepdog;
    private final Vector sheep = new Vector();
    private final LayerManager layerManager;

    private final Graphics graphics;
    private long gameDuration;
    private long startTime;
    private volatile Thread animationThread = null;


    SheepdogCanvas(SheepdogMIDlet midlet)
    {
        super(true);   // suppress key events for game keys
        this.midlet = midlet;
        setFullScreenMode(true);
        graphics = getGraphics();

        layerManager = new LayerManager();
        field = new Field();
        sheepdog = new Sheepdog(this);
        layerManager.append(sheepdog);
        for (int i = 0; i < NUM_SHEEP; ++i)
        {
            Sheep newSheep = new Sheep(this);
            layerManager.append(newSheep);
            sheep.addElement(newSheep);
        }

        layerManager.append(field);   // last layer, behind sprites

        init();
    }


    public void keyPressed(int keyCode)
    {
        // The constructor suppresses key events for game keys, so we'll
        // only get key events for non-game keys. The number keys, * & #
        // have positive keyCodes, so negative keyCodes mean non-game
        // special keys like soft-keys. We'll use key-presses on special
        // keys to take us to the menu.
        if (keyCode < 0)
        {
            stop();
            midlet.sheepdogCanvasMenu();
        }
    }


    void init()
    {
        sheepdog.setPosition(field.getSheepdogStartX(),
                             field.getSheepdogStartY());

        for (int i = 0; i < sheep.size(); ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));

            // find a valid position for the sheep
            do
            {
                int x = midlet.random(field.getWidth() - Sheep.WIDTH);
                int y = midlet.random(field.getHeight() - Sheep.HEIGHT);
                sh.setPosition(x, y);
            } while (field.containsImpassableArea(sh.getX(),
                                                  sh.getY(),
                                                  sh.getWidth(),
                                                  sh.getHeight()) ||
                     overlapsSheepdog(sh) ||
                     overlapsSheep(sh, i) ||
                     field.inFold(sh));
        }
    }
    

    public synchronized void start()
    {
        animationThread = new Thread(this);
        animationThread.start();

        startTime = System.currentTimeMillis() - gameDuration;
    }


    public synchronized void stop()
    {
        gameDuration = System.currentTimeMillis() - startTime;
        animationThread = null;
    }


    public void run()
    {
        Thread currentThread = Thread.currentThread();

        try
        {
            // This ends when animationThread is set to null, or when
            // it is subsequently set to a new thread; either way, the
            // current thread should terminate
            while (currentThread == animationThread)
            {
                long startTime = System.currentTimeMillis();
                // Don't advance game or draw if canvas is covered by
                // a system screen.
                if (isShown())
                {
                    tick();
                    draw();
                    flushGraphics();
                }
                long timeTaken = System.currentTimeMillis() - startTime;
                if (timeTaken < MILLIS_PER_TICK)
                {
                    synchronized (this)
                    {
                        wait(MILLIS_PER_TICK - timeTaken);
                    }
                }
                else
                {
                    currentThread.yield();
                }
            }
        }
        catch (InterruptedException e)
        {
            // won't be thrown
        }
    }


    private void tick()
    {
        // If player presses two or more direction buttons, we ignore them
        // all. But pressing fire is independent. The code below also ignores
        // direction buttons if GAME_A..GAME_D are pressed.
        int keyStates = getKeyStates();
        boolean bark = (keyStates & FIRE_PRESSED) != 0;
        keyStates &= ~FIRE_PRESSED;
        int direction = (keyStates == UP_PRESSED) ? UP :
                        (keyStates == LEFT_PRESSED) ? LEFT:
                        (keyStates == DOWN_PRESSED) ? DOWN :
                        (keyStates == RIGHT_PRESSED) ? RIGHT : NONE;
        sheepdog.tick(direction, bark);

        for (int i = 0; i < sheep.size(); ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));
            sh.tick();
        }

        field.tick();
    }


    Field getField()
    {
        return field;
    }


    Sheepdog getSheepdog()
    {
        return sheepdog;
    }


    Vector getSheep()
    {
        return sheep;
    }


    void handleDogBark()
    {
        for (int i = 0; i < sheep.size(); ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));
            sh.handleDogBark();
        }
    }


    boolean overlapsSheepdog(Sprite sprite)
    {
        return sprite.collidesWith(sheepdog, false); // false -> not pixelLevel
    }


    boolean overlapsSheep(Sprite sprite)
    {
        return overlapsSheep(sprite, sheep.size());
    }


    // whether the sprite overlaps the first 'count' sheep
    boolean overlapsSheep(Sprite sprite, int count)
    {
        for (int i = 0; i < count; ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));
            if (sprite.collidesWith(sh, false))  // false -> not pixelLevel
            {
                return true;
            }
        }
        return false;
    }

    
    boolean overlapsOtherSheep(Sprite sprite)
    {
        for (int i = 0; i < sheep.size(); ++i)
        {
            Object obj = sheep.elementAt(i);
            if (obj != sprite)
            {
                Sheep sh = (Sheep)obj;
                if (sprite.collidesWith(sh, false))  // false -> not pixelLevel
                {
                    return true;
                }
            }
        }
        return false;
    }


    void vibrate(int millis)
    {
        midlet.vibrate(millis);
    }

    
    // draw game
    private void draw()
    {
        int width = getWidth();
        int height = getHeight();

        // clear screen to grey
        graphics.setColor(0x00888888);
        graphics.fillRect(0, 0, width, height);

        // clip and translate to centre
        int dx = origin(sheepdog.getX() + sheepdog.getWidth() / 2,
                        field.getWidth(),
                        width);
        int dy = origin(sheepdog.getY() + sheepdog.getHeight() / 2,
                        field.getHeight(),
                        height);
        graphics.setClip(dx, dy, field.getWidth(), field.getHeight());
        graphics.translate(dx, dy);

        // draw background and sprites
        layerManager.paint(graphics, 0, 0);

        // undo clip & translate
        graphics.translate(-dx, -dy);
        graphics.setClip(0, 0, width, height);

        // display time & score
        long time = (System.currentTimeMillis() - startTime) / 1000;
        int score = numSheepInFold();
        graphics.setColor(0x00FFFFFF); // white
        graphics.drawString(Integer.toString(score),
                            1,
                            1,
                            Graphics.TOP | Graphics.LEFT);
        graphics.drawString(Long.toString(time),
                            width - 2,
                            1,
                            Graphics.TOP | Graphics.RIGHT);

        if (score == sheep.size())
        {
            midlet.sheepdogCanvasGameOver(time);
        }
    }
    
    
    // If the screen is bigger than the field, we center the field
    // in the screen. Otherwise we center the screen on the focus, except
    // that we don't scroll beyond the edges of the field.
    private int origin(int focus, int fieldLength, int screenLength)
    {
        int origin;
        if (screenLength >= fieldLength)
        {
            origin = (screenLength - fieldLength) / 2;
        }
        else if (focus <= screenLength / 2)
        {
            origin = 0;
        }
        else if (focus >= (fieldLength - screenLength / 2))
        {
            origin = screenLength - fieldLength;
        }
        else
        {
            origin = screenLength / 2 - focus;
        }
        return origin;
    }


    int numSheepInFold()
    {
        int count = 0;
        for (int i = 0; i < sheep.size(); ++i)
        {
            Sheep sh = (Sheep)(sheep.elementAt(i));
            if (field.inFold(sh))
            {
                count++;
            }
        }
        return count;
    }
}

SoundEffects

// unnamed package

import javax.microedition.media.*;
import java.io.*;


class SoundEffects
{
    private static SoundEffects instance;
    private Player sheepSoundPlayer;
    private Player dogSoundPlayer;


    private SoundEffects()
    {
        sheepSoundPlayer = createPlayer("/sheep.wav", "audio/x-wav");
        dogSoundPlayer = createPlayer("/dog.wav", "audio/x-wav");
    }


    static SoundEffects getInstance()
    {
        if (instance == null)
        {
            instance = new SoundEffects();
        }
        return instance;
    }


    void startSheepSound()
    {
        startPlayer(sheepSoundPlayer);
    }
    

    void startDogSound()
    {
        startPlayer(dogSoundPlayer);
    }


    void startGameOverSound()
    {
        startPlayer(createPlayer("/gameover.mid", "audio/midi"));
    }
    

    void startHighScoreSound()
    {
        startPlayer(createPlayer("/highscore.mid", "audio/midi"));
    }
    

    private void startPlayer(Player p)
    {
        if (p != null)
        {
            try
            {
                p.stop();
                p.setMediaTime(0L);
                p.start();
            }
            catch (MediaException me)
            {
                // ignore
            }
        }
    }


    private Player createPlayer(String filename, String format)
    {
        Player p = null;
        try
        {
            InputStream is = getClass().getResourceAsStream(filename);
            p = Manager.createPlayer(is, format);
            p.prefetch();
        }
        catch (IOException ioe)
        {
            // ignore
        }
        catch (MediaException me)
        {
            // ignore
        }
        return p;
    }
}

SplashScreen

// unnamed package

import javax.microedition.lcdui.*;


class SplashScreen
    extends Canvas
    implements Runnable
{
    private final SheepdogMIDlet midlet;
    private Image splashImage;
    private volatile boolean dismissed = false;


    SplashScreen(SheepdogMIDlet midlet)
    {
        this.midlet = midlet;
        setFullScreenMode(true);
        splashImage = SheepdogMIDlet.createImage("/splash.png");
        new Thread(this).start();
    }


    public void run()
    {
        synchronized(this)
        {
            try
            {
                wait(3000L);   // 3 seconds
            }
            catch (InterruptedException e)
            {
                // can't happen in MIDP: no Thread.interrupt method
            }
            dismiss();
        }
    }


    public void paint(Graphics g)
    {
        int width = getWidth();
        int height = getHeight();
        g.setColor(0x00FFFFFF);  // white
        g.fillRect(0, 0, width, height);

        g.setColor(0x00FF0000);  // red
        g.drawRect(1, 1, width-3, height-3);  // red border one pixel from edge

        if (splashImage != null)
        {
            g.drawImage(splashImage,
                        width/2,
                        height/2,
                        Graphics.VCENTER | Graphics.HCENTER);
            splashImage = null;
            midlet.splashScreenPainted();
        }
    }


    public synchronized void keyPressed(int keyCode)
    {
        dismiss();
    }


    private void dismiss()
    {
        if (!dismissed)
        {
            dismissed = true;
            midlet.splashScreenDone();
        }
    }
}