/************************************************************************************************

SnakePit.java

  Usage:

  <applet code="SnakePit.class" width=625 height=430></applet>

  Keyboard Controls:

  S           - Start Game
  P           - Pause Game
  M           - Mute Sound
  Cursor Keys - Change Direction

************************************************************************************************/

import java.awt.*;
import java.net.*;
import java.util.*;
import java.applet.Applet;
import java.applet.AudioClip;

/************************************************************************************************
  Main applet code.
************************************************************************************************/

public class SnakePit extends Applet implements Runnable {

  // Thread control variables.

  Thread loopThread;
  Thread loadThread;

  // Constants

  static final int MAX_DELAY  = 75;    // Milliseconds between screen updates.
  static final int MIN_DELAY  = 50;
  static final int DELAY_INCR =  5;

  static final int GRID_WIDTH  = 39;    // Size of playing field.
  static final int GRID_HEIGHT = 25;
  static final int GRID_SIZE   = 16;    // Grid size in pixels.

  static final int NUM_LEVELS = 5;    // Number of levels.
  static final int NUM_MICE   = 6;    // Number of active mice.
  static final int NUM_LIVES  = 3;    // Starting number of lives.

  static final int INIT  = 1;    // Game states.
  static final int PLAY  = 2;
  static final int LEVEL = 3;
  static final int END   = 4;
  static final int OVER  = 5;

  static final int END_COUNT   = 30;    // Counter values.
  static final int LEVEL_COUNT = 40;

  static final int TYPE_MASK  = 0x00FF0000;    // Grid cell types.
  static final int EMPTY      = 0x00000000;
  static final int BLOCK      = 0x00010000;
  static final int SNAKE      = 0x00020000;
  static final int MOUSE      = 0x00030000;
  static final int KEY        = 0x00040000;

  static final int DIR_MASK   = 0x0000FF00;    // Grid cell directions.
  static final int NONE       = 0x00000000;
  static final int LEFT       = 0x00000100;
  static final int RIGHT      = 0x00000200;
  static final int UP         = 0x00000300;
  static final int DOWN       = 0x00000400;

  static final int SHAPE_MASK = 0x000000FF;    // Grid cell shapes.
  static final int SQUARE     = 0x00000000;
  static final int SNAKEHEAD  = 0x00000001;
  static final int SNAKEBODY  = 0x00000002;
  static final int SNAKEELB1  = 0x00000003;
  static final int SNAKEELB2  = 0x00000004;
  static final int SNAKETAIL  = 0x00000005;
  static final int MOUSEBODY  = 0x00000006;
  static final int KEYSHAPE   = 0x00000007;

  static final int MOUSE_POINTS =  10;    // Scoring values.
  static final int LEVEL_POINTS = 200;
  static final int EXTRA_LIFE   = 500;

  // Sizing data.

  int width;
  int height;

  // Game data.

  int     score;         // Scoring data.
  int     highScore;
  int     lives;
  int     extraLife;

  int     level;         // Level data.
  int     levelTotal;
  int     miceNeeded;
  int     miceEaten;
  int     delay;

  int[][] grid;          // Playing field.

  Point[] snake;         // Snake data.
  int     headPtr;
  int     tailPtr;
  int     direction;
  int     lastDirection;
  boolean lockKeys;

  Point[] mouse;         // Mouse data.

  Point   key;           // Key data.
  boolean keyActive;

  int     gameState;     // Game state data.
  int     levelCounter;
  int     endCounter;
  boolean paused;

  // Screen colors.

  Color bgColor    = Color.black;
  Color fgColor    = Color.white;
  Color blockColor = new Color(0, 0, 153);
  Color fieldColor = new Color(204, 153, 102);
  Color snakeColor = new Color(0, 153, 0);
  Color mouseColor = Color.gray;
  Color keyColor   = new Color(204, 102, 102);

  // Basic shapes.

  Polygon snakeHead;
  Polygon snakeBody;
  Polygon snakeTail;
  Polygon snakeElb1;
  Polygon snakeElb2;
  Polygon mouseBody;
  Polygon keyShape;

  // Sound data.

  boolean   loaded = false;
  boolean   sound;
  AudioClip bonkSound;
  AudioClip munchSound;
  AudioClip squeakSound;
  AudioClip chimeSound;
  AudioClip advanceSound;

  // Values for the offscreen image.

  Dimension offDimension;
  Image     offImage;
  Graphics offGraphics;

  // Font data.

  Font font = new Font("Helvetica", Font.BOLD, 12);
  FontMetrics fm;
  int fontWidth;
  int fontHeight;

  // Applet information.

  public String getAppletInfo() {

    return("Snake Pit, Copyright 1998 by Mike Hall.");
  }

  public void init() {

    Graphics g;
    int i;

    // Take credit.

    System.out.println("Snake Pit, Copyright 1998 by Mike Hall.");

    // Set font data.

    g = getGraphics();
    g.setFont(font);
    fm = g.getFontMetrics();
    fontWidth = fm.getMaxAdvance();
    fontHeight = fm.getHeight();

    // Define the playing grid.

    grid = new int[GRID_WIDTH][GRID_HEIGHT];

    // Initialize basic shapes.

    snakeHead = new Polygon();
    snakeHead.addPoint(4, 15);
    snakeHead.addPoint(3, 14);
    snakeHead.addPoint(1, 12);
    snakeHead.addPoint(1, 8);
    snakeHead.addPoint(3, 6);
    snakeHead.addPoint(3, 4);
    snakeHead.addPoint(6, 1);
    snakeHead.addPoint(9, 1);
    snakeHead.addPoint(12, 4);
    snakeHead.addPoint(12, 6);
    snakeHead.addPoint(14, 8);
    snakeHead.addPoint(14, 12);
    snakeHead.addPoint(12, 14);
    snakeHead.addPoint(11, 15);

    snakeBody = new Polygon();
    snakeBody.addPoint(11, 0);
    snakeBody.addPoint(11, 15);
    snakeBody.addPoint(4, 15);
    snakeBody.addPoint(4, 0);

    snakeElb1 = new Polygon();
    snakeElb1.addPoint(11, 0);
    snakeElb1.addPoint(11, 4);
    snakeElb1.addPoint(15, 4);
    snakeElb1.addPoint(15, 11);
    snakeElb1.addPoint(7, 11);
    snakeElb1.addPoint(4, 8);
    snakeElb1.addPoint(4, 0);

    snakeElb2 = mirror(snakeElb1);

    snakeTail = new Polygon();
    snakeTail.addPoint(11, 0);
    snakeTail.addPoint(8, 15);
    snakeTail.addPoint(7, 15);
    snakeTail.addPoint(4, 0);

    mouseBody = new Polygon();
    mouseBody.addPoint(8, 1);
    mouseBody.addPoint(12, 5);
    mouseBody.addPoint(12, 6);
    mouseBody.addPoint(10, 7);
    mouseBody.addPoint(12, 9);
    mouseBody.addPoint(12, 12);
    mouseBody.addPoint(10, 14);
    mouseBody.addPoint(5, 14);
    mouseBody.addPoint(3, 12);
    mouseBody.addPoint(3, 9);
    mouseBody.addPoint(5, 7);
    mouseBody.addPoint(3, 6);
    mouseBody.addPoint(3, 5);
    mouseBody.addPoint(7, 1);

    keyShape = new Polygon();
    keyShape.addPoint(1, 6);
    keyShape.addPoint(7, 6);
    keyShape.addPoint(9, 4);
    keyShape.addPoint(13, 4);
    keyShape.addPoint(15, 6);
    keyShape.addPoint(15, 10);
    keyShape.addPoint(13, 12);
    keyShape.addPoint(9, 12);
    keyShape.addPoint(7, 10);
    keyShape.addPoint(3, 10);
    keyShape.addPoint(3, 11);
    keyShape.addPoint(0, 11);
    keyShape.addPoint(0, 7);

    // Initialize data.

    highScore = 0;
    sound = true;
    snake = new Point[GRID_WIDTH * GRID_HEIGHT];
    for (i = 0; i < snake.length; i++)
      snake[i] = new Point(-1, -1);
    mouse = new Point[NUM_MICE];
    for (i = 0; i < NUM_MICE; i++)
      mouse[i] = new Point(-1, -1);
    key = new Point(-1, -1);
    lockKeys = false;
    initGame();
    endGame();
    for (i = 0; i < NUM_MICE; i++)
      killMouse(i);
    gameState = INIT;
  }

  public void initGame() {

    // Initialize game data.

    score = 0;
    lives = NUM_LIVES;
    level = 0;
    levelTotal = 0;
    extraLife = EXTRA_LIFE;
    delay = MAX_DELAY;
    paused = false;
    initLevel();
  }

  public void endGame() {

    gameState = OVER;
  }

  public void initLevel() {

    int i;

    // Advance level. Once we have gone thru each, start at the beginning and
    // increase speed.

    level++;
    if (level > NUM_LEVELS) {
      level = 1;
      if (delay > MIN_DELAY)
        delay -= DELAY_INCR;
    }

    // Level total for display.

    levelTotal++;

    // Initialize game data.

    initBlocks();
    initSnake();
    for (i = 0; i < NUM_MICE; i++)
      initMouse(i);
    miceEaten = 0;
    miceNeeded = 3 * (NUM_MICE + NUM_LEVELS - level);
    keyActive = false;
    gameState = PLAY;
  }

  public void endLevel() {

    // Start counter to pause before changing level.

    gameState = LEVEL;
    levelCounter = LEVEL_COUNT;
  }


  public void initLife() {

    // Create a new snake.

    killSnake();
    initSnake();
    gameState = PLAY;
  }

  public void endLife() {

    // Start counter to pause before starting a new snake.

    gameState = END;
    endCounter = END_COUNT;
  }

  public void start() {

    if (loopThread == null) {
      loopThread = new Thread(this);
      loopThread.start();
    }
    if (!loaded && loadThread == null) {
      loadThread = new Thread(this);
      loadThread.start();
    }
  }

  public void stop() {

    if (loopThread != null) {
      loopThread.stop();
      loopThread = null;
    }
    if (loadThread != null) {
      loadThread.stop();
      loadThread = null;
    }
  }

  public void run() {

    int i;
    long startTime;

    // Lower this thread's priority and get the current time.

    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
    startTime = System.currentTimeMillis();

    // Run thread for loading sounds.

    if (!loaded && Thread.currentThread() == loadThread) {
      loadSounds();
      loaded = true;
      loadThread.stop();
    }

    // This is the main loop.

    while (Thread.currentThread() == loopThread) {

      if (!paused) {

        // Move snake and mice.

        if (gameState == PLAY) {
          moveSnake();
          for (i = 0; i < NUM_MICE; i++)
            moveMouse(i);
        }

        // Check the score and advance high score if necessary.

        if (score > highScore)
          highScore = score;

        // Add an extra life if score is high enough.

        if (score >= extraLife) {
          lives++;
          extraLife += EXTRA_LIFE;
        }

        // See if the snake was killed. If any lives are left, continue with
        // a new one.

        if (gameState == END && --endCounter < 0)
          if (--lives == 0)
            endGame();
          else
            initLife();

        // If enough mice have been eaten, show the key.

        if (gameState == PLAY && miceEaten == miceNeeded && !keyActive) {
          if (loaded && sound)
            chimeSound.play();
          initKey();
        }

        // If level was completed, go to the next one when the counter finishes.

        if (gameState == LEVEL && --levelCounter < 0)
          initLevel();
      }

      // Update the screen and set the timer for the next loop.

      repaint();
      try {
        if (gameState == PLAY)
          startTime += delay;
        else
          startTime += MIN_DELAY;
        Thread.sleep(Math.max(0, startTime - System.currentTimeMillis()));
      }
      catch (InterruptedException e) {
        break;
      }
    }
  }

  public void loadSounds() {

    // Load all sound clips by playing and immediately stopping them.

    try {
      bonkSound    = getAudioClip(new URL(getDocumentBase(), "bonk.au"));
      munchSound   = getAudioClip(new URL(getDocumentBase(), "munch.au"));
      squeakSound  = getAudioClip(new URL(getDocumentBase(), "squeak.au"));
      chimeSound   = getAudioClip(new URL(getDocumentBase(), "chime.au"));
      advanceSound = getAudioClip(new URL(getDocumentBase(), "advance.au"));
    }
    catch (MalformedURLException e) {}

    bonkSound.play();    bonkSound.stop();
    munchSound.play();   munchSound.stop();
    squeakSound.play();  squeakSound.stop();
    chimeSound.play();   chimeSound.stop();
    advanceSound.play(); advanceSound.stop();
  }

  public boolean keyDown(Event e, int key) {

    // Check if any cursor keys have been pressed and set direction but don't
    // allow a reversal of direction.

    if (!paused && !lockKeys) {
      if (key == Event.LEFT && lastDirection != RIGHT)
        direction = LEFT;
      else if (key == Event.RIGHT && lastDirection != LEFT && lastDirection != NONE)
        direction = RIGHT;
      else if (key == Event.UP && lastDirection != DOWN)
        direction = UP;
      else if (key == Event.DOWN && lastDirection != UP)
        direction = DOWN;
    }

    // 'M' key: toggle sound.

    if (key == 109) {
      if (sound) {
        bonkSound.stop();
        munchSound.stop();
        squeakSound.stop();
        chimeSound.stop();
        advanceSound.stop();
      }
      sound = !sound;
    }

    // 'P' key: toggle pause mode.

    if (key == 112)
      paused = !paused;

    // 'S' key: start the game, if not already in progress.

    if (loaded && key == 115 && (gameState == INIT || gameState == OVER))
      initGame();

    return true;
  }

  public void initBlocks() {

    int i, j;

    // Clear the grid.

    for (i = 0; i < GRID_WIDTH; i++)
      for (j = 0; j < GRID_HEIGHT; j++)
        grid[i][j] = EMPTY | NONE | SQUARE;

    // Add outer walls.

    for (i = 0; i < GRID_WIDTH; i++) {
      grid[i][0] = BLOCK | NONE | SQUARE;
      grid[i][GRID_HEIGHT - 1] = BLOCK | NONE | SQUARE;
    }
    for (j = 1; j < GRID_HEIGHT - 1; j++) {
      grid[0][j] = BLOCK | NONE | SQUARE;
      grid[GRID_WIDTH - 1][j] = BLOCK | NONE | SQUARE;
    }

    // Add inner walls depending on current level.

    if (level == 2 || level == 4) {
      i = GRID_WIDTH / 4;
      for (j = GRID_HEIGHT / 4; j <= GRID_HEIGHT / 2 - 3; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
      j = GRID_HEIGHT / 4;
      for (i = GRID_WIDTH / 4; i <= GRID_WIDTH / 2 - 3; i++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
    }

    if (level == 3) {
      i = GRID_WIDTH / 2;
      for (j = 0; j <= 3; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
    }

    if (level == 3 || level == 5) {
      j = GRID_HEIGHT / 4;
      for (i = 5; i <= GRID_WIDTH / 4; i++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
      i = GRID_WIDTH / 4;
      for (j = GRID_HEIGHT / 4; j <= GRID_HEIGHT / 2; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
      j = GRID_HEIGHT / 2;
      for (i = 0; i <= GRID_WIDTH / 4 - 5; i++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
      }
    }

    if (level == 4) {
      i = 4;
      for (j = GRID_HEIGHT / 2 - 2; j <= GRID_HEIGHT / 2; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
      i = GRID_WIDTH / 2;
      for (j = 0; j <= 2; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
    }

    if (level == 5) {
      i = 3 * GRID_WIDTH / 8;
      for (j = 4; j <= GRID_HEIGHT / 3 - 1; j++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
      j = GRID_HEIGHT / 3 - 1;
      for (i = 3 * GRID_WIDTH / 8; i <= GRID_WIDTH / 2; i++) {
        grid[i][j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][j] = BLOCK | NONE | SQUARE;
        grid[i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
        grid[GRID_WIDTH - 1 - i][GRID_HEIGHT - 1 - j] = BLOCK | NONE | SQUARE;
      }
    }
  }

  public void initSnake() {

    int i, j;
    int m, n;

    // Create a new snake in the center of the grid and reset cell list.

    i = GRID_WIDTH  / 2;
    j = GRID_HEIGHT / 2;

    snake[4].x = i - 2; snake[4].y = j;
    snake[3].x = i - 1; snake[3].y = j;
    snake[2].x = i - 0; snake[2].y = j;
    snake[1].x = i + 1; snake[1].y = j;
    snake[0].x = i + 2; snake[0].y = j;

    headPtr = 4;
    tailPtr = 0;

    grid[i - 2][j] = SNAKE | LEFT | SNAKEHEAD;
    grid[i - 1][j] = SNAKE | LEFT | SNAKEBODY;
    grid[i][j]     = SNAKE | LEFT | SNAKEBODY;
    grid[i + 1][j] = SNAKE | LEFT | SNAKEBODY;
    grid[i + 2][j] = SNAKE | LEFT | SNAKETAIL;

    // Clear snake direction.

    direction     = NONE;
    lastDirection = NONE;

    // Relocate any mice that were overwritten.

    for (m = 0; m < NUM_MICE; m++)
      for (n = -2; n <= 2 ; n++)
        if (mouse[m].x == i + n && mouse[m].y == j)
          initMouse(m);

    // Relocate key if overwritten.

    if (keyActive)
      for (n = -2; n <= 2 ; n++)
        if (key.x == i + n && key.y == j)
          initKey();
  }

  public void moveSnake() {

    Point pt;
    int i, j;
    int k;
    int c;
    int d;

    // Lock cursor keys to prevent the current direction from being changed
    // until we are done processing it.

    lockKeys = true;

    // Move snake's head into the next cell based on current direction.

    pt = snake[headPtr];
    i = pt.x;
    j = pt.y;
    switch (direction) {
      case LEFT:
        i--;
        break;
      case RIGHT:
        i++;
        break;
      case UP:
        j--;
        break;
      case DOWN:
        j++;
        break;
      default:
        lockKeys = false;
        return;
    }

    // Unlock cursor keys and save direction.

    lockKeys = false;
    lastDirection = direction;

    // Skip if no direction given.

    if (direction == NONE)
      return;

    // Get the type of the new cell.

    c = grid[i][j] & TYPE_MASK;

    // Check if we hit a wall or ourselves.

    if (c == BLOCK || c == SNAKE) {
      if (loaded && sound)
        bonkSound.play();
      endLife();
      return;
    }

    // Replace current head with the appropriate body part.

    d = grid[pt.x][pt.y] & DIR_MASK;
    if (d == direction)
      grid[pt.x][pt.y] = SNAKE | d | SNAKEBODY;
    else if (d == LEFT && direction == UP)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB1;
    else if (d == UP && direction == RIGHT)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB1;
    else if (d == RIGHT && direction == DOWN)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB1;
    else if (d == DOWN && direction == LEFT)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB1;
    else if (d == UP && direction == LEFT)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB2;
    else if (d == LEFT && direction == DOWN)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB2;
    else if (d == DOWN && direction == RIGHT)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB2;
    else if (d == RIGHT && direction == UP)
      grid[pt.x][pt.y] = SNAKE | direction | SNAKEELB2;

    // Change the new cell to a snake's head and set it in the cell list.

    grid[i][j] = SNAKE | direction | SNAKEHEAD;
    if (++headPtr >= snake.length)
      headPtr = 0;
    snake[headPtr].x = i;
    snake[headPtr].y = j;

    // If we got a mouse, bump the score and create a new one. Otherwise,
    // move the tail.

    if (c == MOUSE) {
      score += MOUSE_POINTS;
      if (loaded && sound)
        munchSound.play();
      for (k = 0; k < NUM_MICE; k++)
        if (mouse[k].x == i && mouse[k].y == j)
          initMouse(k);
      miceEaten++;
    }
    else {
      pt = snake[tailPtr];
      grid[pt.x][pt.y] = EMPTY & NONE & SQUARE;
      if (++tailPtr >= snake.length)
        tailPtr = 0;
      pt = snake[tailPtr];
      d = grid[pt.x][pt.y] & DIR_MASK;
      grid[pt.x][pt.y] = SNAKE | d | SNAKETAIL;
    }

    // Check if we got the key. If so, end the current level.

    if (c == KEY) {
      score += LEVEL_POINTS;
      if (loaded && sound)
        advanceSound.play();
      endLevel();
    }
  }

  public void killSnake() {

    Point pt;
    int   n;

    // Remove snake from the grid.

    n = headPtr + 1;
    if (n >= snake.length)
      n = 0;
    while(tailPtr != n) {
      pt = snake[tailPtr];
      grid[pt.x][pt.y] = EMPTY | NONE | SQUARE;
      if (++tailPtr >= snake.length)
        tailPtr = 0;
    }
  }

  public void initMouse(int n) {

    int i, j;
    int d;

    // Find an empty cell.

    do {
      i = (int) (Math.random() * GRID_WIDTH);
      j = (int) (Math.random() * GRID_HEIGHT);
    } while ((grid[i][j] & TYPE_MASK) != EMPTY);

    // Get a random direction.

    d = (int) (Math.random() * 4) + 1;
    d <<= 8;

    // Save mouse position.

    mouse[n].x = i;
    mouse[n].y = j;

    // Set the cell with mouse data.

    grid[i][j] = MOUSE | d | MOUSEBODY;
  }

  public void moveMouse(int n) {

    int i, j;
    int d;
    int m;

    // Skip move at random.

    if (Math.random() > 0.25)
      return;

    // Skip move if mouse is dead.

    if (mouse[n].x == -1 || mouse[n].y == -1)
      return;

    // Toss in a random squeak.

    if (loaded && sound && Math.random() > 0.975)
      squeakSound.play();

    // Get a random direction.

    d = (int) (Math.random() * 5);
    d <<= 8;

    // Don't allow a reversal of direction.

    m = grid[mouse[n].x][mouse[n].y] & DIR_MASK;
    if ((m == LEFT  && d == RIGHT) ||
        (m == RIGHT && d == LEFT)  ||
        (m == UP    && d == DOWN)  ||
        (m == DOWN  && d == UP))
      return;

    i = mouse[n].x;
    j = mouse[n].y;
    switch (d) {
      case LEFT:
        i--;
        break;
      case RIGHT:
        i++;
        break;
      case UP:
        j--;
        break;
      case DOWN:
        j++;
        break;
      default:
        return;
    }

    // See if the new cell is empty. If not, skip move.

    if ((grid[i][j] & TYPE_MASK) != EMPTY)
      return;

    // Clear mouse from old cell and move to the new one.

    grid[mouse[n].x][mouse[n].y] = EMPTY | NONE | SQUARE;
    mouse[n].x = i;
    mouse[n].y = j;
    grid[i][j] = MOUSE | d | MOUSEBODY;
  }

  public void killMouse(int n) {

    // Clear mouse from grid and set coordinates to (-1, -1) so we know it is
    // dead.

    grid[mouse[n].x][mouse[n].y] = EMPTY | NONE | SQUARE;
    mouse[n].x = -1;
    mouse[n].y = -1;
  }

  public void initKey() {

    int i, j;

    // Find an empty cell.

    do {
      i = (int) (Math.random() * GRID_WIDTH);
      j = (int) (Math.random() * GRID_HEIGHT);
    } while ((grid[i][j] & TYPE_MASK) != EMPTY);

    // Save key position.

    key.x = i;
    key.y = j;

    // Set the cell with key data and set the flag to show it's been added.

    grid[i][j] = KEY | NONE | KEYSHAPE;
    keyActive = true;
  }

  public Polygon translate(Polygon p, int dx, int dy) {

    Polygon polygon;
    int i;

    // Returns the polygon created by translating the given shape.

    polygon = new Polygon();
    for (i = 0; i < p.npoints; i++)
      polygon.addPoint(p.xpoints[i] + dx, p.ypoints[i] + dy);

    return polygon;
  }

  public Polygon rotate(Polygon p, int d) {

    Polygon polygon;
    int i;

    // Returns the polygon created by rotating the given shape in 90 deg. increments.

    polygon = new Polygon();
    for (i = 0; i < p.npoints; i++)
      switch (d) {
        case LEFT:
          polygon.addPoint(p.ypoints[i], (GRID_SIZE - 1) - p.xpoints[i]);
          break;
        case RIGHT:
          polygon.addPoint((GRID_SIZE - 1) - p.ypoints[i], p.xpoints[i]);
          break;
        case DOWN:
          polygon.addPoint((GRID_SIZE - 1) - p.xpoints[i], (GRID_SIZE - 1) - p.ypoints[i]);
          break;
        default:
          polygon.addPoint(p.xpoints[i], p.ypoints[i]);
          break;
      }

    return polygon;
  }

  public Polygon mirror(Polygon p) {

    Polygon polygon;
    int i;

    // Returns the polygon created by mirroring the given shape.

    polygon = new Polygon();
    for (i = 0; i < p.npoints; i++)
      polygon.addPoint((GRID_SIZE - 1) - p.xpoints[i], p.ypoints[i]);

    return polygon;
  }

  public Color fade(Color s, Color e, double pct) {

    int r, g, b;

    // Fade the starting color to the ending color by the given percentage.

    if (pct < 0.0)
      return e;
    if (pct > 1.0)
      return s;

    r = e.getRed() + (int) Math.round(pct * (s.getRed() - e.getRed()));
    g = e.getGreen() + (int) Math.round(pct * (s.getGreen() - e.getGreen()));
    b = e.getBlue() + (int) Math.round(pct * (s.getBlue() - e.getBlue()));

    return(new Color(r, g, b)); 
  }

  public void paint(Graphics g) {

    update(g);
  }

  public void update(Graphics g) {

    Dimension d = size();
    int width, height;
    int xOff, yOff;
    int i, j;
    int m;
    int n;
    Polygon p;
    String s;

    // Create the offscreen graphics context, if no good one exists.

    if (offGraphics == null || d.width != offDimension.width || d.height != offDimension.height) {
      offDimension = d;
      offImage = createImage(d.width, d.height);
      offGraphics = offImage.getGraphics();
    }

    // Fill in applet background.

    offGraphics.setColor(bgColor);
    offGraphics.fillRect(0, 0, d.width, d.height);

    // Center game area.

    width  = GRID_WIDTH  * GRID_SIZE;
    height = GRID_HEIGHT * GRID_SIZE;
    xOff = (d.width - width) / 2;
    yOff = (d.height - (height + 2 * fontHeight)) / 2;
    offGraphics.translate(xOff, yOff);

    // Fill in playing field.

    if (gameState == LEVEL)
      offGraphics.setColor(fade(fieldColor, bgColor, (double) levelCounter / (double) LEVEL_COUNT));
    else
      offGraphics.setColor(fieldColor);
    offGraphics.fillRect(0, 0, GRID_WIDTH * GRID_SIZE, GRID_HEIGHT * GRID_SIZE);

    // Fill in each grid cell with the appropriate shape.

    for (i = 0; i < GRID_WIDTH; i++)
      for (j = 0; j < GRID_HEIGHT; j++)
        switch (grid[i][j] & TYPE_MASK) {

          case EMPTY:
            break;

          case BLOCK:
            if (gameState == LEVEL)
              offGraphics.setColor(fade(blockColor, bgColor, (double) levelCounter / (double) LEVEL_COUNT));
            else
              offGraphics.setColor(blockColor);
            offGraphics.fillRect(i * GRID_SIZE, j * GRID_SIZE, GRID_SIZE, GRID_SIZE);
            offGraphics.setColor(bgColor);
            offGraphics.drawRect(i * GRID_SIZE, j * GRID_SIZE, GRID_SIZE, GRID_SIZE);
            break;

          case SNAKE:
            n = grid[i][j] & SHAPE_MASK;
            switch (n) {
              case SNAKEHEAD:
                p = snakeHead;
                break;
              case SNAKEBODY:
                p = snakeBody;
                break;
              case SNAKEELB1:
                p = snakeElb1;
                break;
              case SNAKEELB2:
                p = snakeElb2;
                break;
              case SNAKETAIL:
                p = snakeTail;
                break;
              default:
                p = snakeHead;
                break;
            }
            p = translate(rotate(p, grid[i][j] & DIR_MASK), i * GRID_SIZE, j * GRID_SIZE);
            if (gameState == LEVEL)
              offGraphics.setColor(fade(snakeColor, bgColor, (double) levelCounter / (double) LEVEL_COUNT));
            else if (gameState == END || gameState == OVER)
              offGraphics.setColor(fade(snakeColor, fieldColor, (double) endCounter / (double) END_COUNT));
            else
              offGraphics.setColor(snakeColor);
            offGraphics.fillPolygon(p);
            offGraphics.drawPolygon(p);
            offGraphics.drawLine(p.xpoints[p.npoints - 1], p.ypoints[p.npoints - 1], p.xpoints[0], p.ypoints[0]);
            if (gameState == END || gameState == OVER)
              offGraphics.setColor(fade(bgColor, fieldColor, (double) endCounter / (double) END_COUNT));
            else
              offGraphics.setColor(bgColor);
            if (n == SNAKEHEAD || n == SNAKETAIL)
              for (m = 0; m < p.npoints - 1; m++)
                offGraphics.drawLine(p.xpoints[m], p.ypoints[m], p.xpoints[m + 1], p.ypoints[m + 1]);
            if (n == SNAKEBODY) {
              offGraphics.drawLine(p.xpoints[0], p.ypoints[0], p.xpoints[1], p.ypoints[1]);
              offGraphics.drawLine(p.xpoints[2], p.ypoints[2], p.xpoints[3], p.ypoints[3]);
            }
            if (n == SNAKEELB1 || n == SNAKEELB2) {
              offGraphics.drawLine(p.xpoints[0], p.ypoints[0], p.xpoints[1], p.ypoints[1]);
              offGraphics.drawLine(p.xpoints[1], p.ypoints[1], p.xpoints[2], p.ypoints[2]);
              offGraphics.drawLine(p.xpoints[3], p.ypoints[3], p.xpoints[4], p.ypoints[4]);
              offGraphics.drawLine(p.xpoints[4], p.ypoints[4], p.xpoints[5], p.ypoints[5]);
              offGraphics.drawLine(p.xpoints[5], p.ypoints[5], p.xpoints[6], p.ypoints[6]);
            }
            break;

          case MOUSE:
            if (gameState == LEVEL)
              offGraphics.setColor(fade(mouseColor, bgColor, (double) levelCounter / (double) LEVEL_COUNT));
            else
              offGraphics.setColor(mouseColor);
            p = translate(rotate(mouseBody, grid[i][j] & DIR_MASK), i * GRID_SIZE, j * GRID_SIZE);
            offGraphics.fillPolygon(p);
            offGraphics.setColor(bgColor);
            offGraphics.drawPolygon(p);
            offGraphics.drawLine(p.xpoints[p.npoints - 1], p.ypoints[p.npoints - 1], p.xpoints[0], p.ypoints[0]);
            break;

          case KEY:
            p = translate(rotate(keyShape, grid[i][j] & DIR_MASK), i * GRID_SIZE, j * GRID_SIZE);
            offGraphics.setColor(keyColor);
            offGraphics.fillPolygon(p);
            offGraphics.setColor(bgColor);
            offGraphics.drawPolygon(p);
            offGraphics.drawLine(p.xpoints[p.npoints - 1], p.ypoints[p.npoints - 1], p.xpoints[0], p.ypoints[0]);
            break;

          default:
            break;
        }

    // Outline playing field.

    offGraphics.setColor(fieldColor);
    offGraphics.drawRect(0, 0, GRID_WIDTH * GRID_SIZE, GRID_HEIGHT * GRID_SIZE - 1);

    // Display status and messages.

    offGraphics.setFont(font);

    offGraphics.setColor(fieldColor);
    i = height - 1;
    j = height + 3 * fontHeight / 2;
    offGraphics.drawRect(0, i, width, 2 * fontHeight);

    offGraphics.setColor(fgColor);
    s = "Score: " + score;
    offGraphics.drawString(s, fontWidth, j);
    s = "Level: " + levelTotal;
    offGraphics.drawString(s, (width - fm.stringWidth(s)) / 4, j);
    s = "Lives: " + lives;
    offGraphics.drawString(s, 3 * (width - fm.stringWidth(s)) / 4, j);
    s = "High: " + highScore;
    offGraphics.drawString(s, width - (fontWidth + fm.stringWidth(s)), j);
    if (paused) {
      s = "Paused";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, j);
    }
    else if (!sound) {
      s = "Muted";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, j);
    }

    if (gameState == INIT || gameState == OVER) {
      offGraphics.setColor(bgColor);
      s = "Snake Pit";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2 + 1, height / 3 + fontHeight + 1);
      s = "Copyright 1998 by Mike Hall";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2 + 1, height / 3 + 2 * fontHeight + 1);
      offGraphics.setColor(fgColor);
      s = "Snake Pit";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, height / 3 + fontHeight);
      s = "Copyright 1998 by Mike Hall";
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, height / 3 + 2 * fontHeight);
      offGraphics.setColor(fgColor);
      if (!loaded)
        s = "Loading sounds...";
      else
      s = "Game Over - 'S' to Start";
      offGraphics.setColor(bgColor);
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2 + 1, 2 * height / 3 + 1);
      offGraphics.setColor(fgColor);
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, 2 * height / 3);
    }
    if (gameState == LEVEL) {
      s = "Advancing to Level " + (levelTotal + 1);
      offGraphics.setColor(bgColor);
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2 + 1, height / 2 + 1);
      offGraphics.setColor(fgColor);
      offGraphics.drawString(s, (width - fm.stringWidth(s)) / 2, height / 2);
    }

    // Copy the off screen buffer to the screen.

    offGraphics.translate(-xOff, -yOff);
    g.drawImage(offImage, 0, 0, this);
  }
}
