// Craig Duttweiler       CS 193i      June, 1996

// MazeApplet is a java applet that implements a maze through which
// three monsters move randomly and one player moves by keyboard input

import java.awt.*;
import java.applet.*;
import java.lang.Math;
import java.awt.image.*;
import java.net.*;
import RandomBag;
import Coordinates;


//--------------------------------------------------------------------------


// Keep track of what's going on in one square of the maze.

class Room {
  // array over the four directions: N, E, S, W
  boolean[] walls = {true, true, true, true};
  byte state = Maze.FarFromMaze;
  Player inhabitant = null;
};


//--------------------------------------------------------------------------


class Maze extends Canvas {

  // constants
  public static final byte FarFromMaze = 0;
  public static final byte NearMaze = 1;
  public static final byte InMaze = 2;

  int xRooms, yRooms, xRoomSize, yRoomSize;
  Room[][] rooms;	


  public Maze(int XRooms, int YRooms, int XRoomSize, int YRoomSize) {

    int i,j;
    xRooms = XRooms;
    yRooms = YRooms;
    xRoomSize = XRoomSize;
    yRoomSize = YRoomSize;

    // allocate the rooms
    rooms = new Room[xRooms][yRooms];
    for (i=0; i<xRooms; i++)
      for (j=0; j<yRooms; j++)
    	rooms[i][j] = new Room();

    setBackground(Color.white);
    resize(xRooms*xRoomSize+3, yRooms*yRoomSize+3);
    build();
  }


  // Given a valid maze coordinate, add that room
  // to the maze by setting its state to InMaze and by
  // trying to remove one wall to connect it to the rest
  // of the maze. Then, look around the newly added
  // room to add new rooms to the near set.

  private void addToMaze(RandomBag near, Coordinates coord)
  {
    Room room = rooms[coord.x][coord.y];
    boolean wallKnockedOut = false;
    byte dir = (byte)(Math.random()*4); 
    
    room.state = InMaze;
    
    for(int i=0; i<4; i++) {
      dir = Coordinates.clockwise(dir);	// rotate one dir clockwise
      Coordinates adj = coord.adjacent(xRooms, yRooms, dir);
      if (adj == null) continue;
      
      Room adjRoom = rooms[adj.x][adj.y];
      if(adjRoom.state == InMaze && !wallKnockedOut) {
	wallKnockedOut = true;
	// Knock out the wall in room and adjRoom
	adjRoom.walls[Coordinates.opposite(dir)] = false;
	room.walls[dir] = false;
      }
      else {
	if(adjRoom.state == FarFromMaze){
	  near.add(adj);
	  adjRoom.state = NearMaze;
	}
      }
    }
  }


  // Given a "solid" maze (all walls) and a start point, this knocks out
  // walls to make a maze. The middle point of the whole maze makes
  // a good start point.
  private void carve(int startX, int startY)
  {
    RandomBag near = new RandomBag(xRooms * yRooms);
    Coordinates start = new Coordinates(startX, startY);
    
    // Put the seed in the bag to prime the pump
    near.add(start);
    
    // Keep adding squares which are near the maze until there are none left.
    while(near.getLength() > 0){
      //Attach to the maze one randomly chosen adjacent square
      addToMaze(near, (Coordinates)near.randomRemove());
    }
  }


  private void build() {
    byte seedX = (byte)(Math.random()*xRooms); 
    byte seedY = (byte)(Math.random()*yRooms); 
    carve(seedX, seedY);
  }
 

  // clear out the old maze, rebuild it, and repaint it

  public void rebuild() {
    int i,j, wallnum;

    for (i=0; i<xRooms; i++) {
      for (j=0; j<yRooms; j++) {
	for (wallnum=0; wallnum<4; wallnum++) {
	  rooms[i][j].walls[wallnum] = true;
	}
	rooms[i][j].state = FarFromMaze;
	rooms[i][j].inhabitant = null;
      }
    }
    build();
    repaint();
  }


  public void paint(Graphics g) {
    int i,j;
    Dimension dim = size();

    // first draw the left and bottom borders
    g.drawLine(1, 1, 1, dim.height-2);
    g.drawLine(1, dim.height-2, dim.width-2, dim.height-2);

    for (i=0; i<xRooms; i++) {
      for (j=0; j<yRooms; j++) {
	// draw top wall if present
	if (rooms[i][j].walls[Coordinates.North] == true)
	  g.drawLine(xRoomSize*i+1, yRoomSize*j+1, 
		     xRoomSize*(i+1)+1, yRoomSize*j+1);
	// draw right wall if present
	if (rooms[i][j].walls[Coordinates.East] == true)
	  g.drawLine(xRoomSize*(i+1)+1, yRoomSize*j+1, 
		     xRoomSize*(i+1)+1, yRoomSize*(j+1)+1);
	// draw player if present
	if (rooms[i][j].inhabitant != null)
	  rooms[i][j].inhabitant.drawSelf(g, xRoomSize*i+2, yRoomSize*j+2,
					  xRoomSize-1, yRoomSize-1);
      }
    }

  }


  public void addPlayer(Player p) {
    Coordinates location = p.getLocation();
    rooms[location.x][location.y].inhabitant = p;
    repaint();
  }


  // check to see whether player p can move in direction dir
  // without hitting a wall / another player / etc. 
  // if so, return true; if not return false

  public boolean checkDirection(Player p, byte dir) {
    Coordinates location = p.getLocation();

    // if there's a wall in the way, return false
    if (rooms[location.x][location.y].walls[dir] == true) 
      return(false);

    // otherwise just make sure the room is unoccupied
    Coordinates adjacentCoords = location.adjacent(xRooms,yRooms,dir);
    if (rooms[adjacentCoords.x][adjacentCoords.y].inhabitant == null)
      return(true);

    return(false);
  }


  // move a player and redraw as necessary

  public synchronized Coordinates movePlayer(Player p, byte dir) {
    Coordinates location = p.getLocation();
    boolean ok = checkDirection(p,dir);
    if (!ok) return(location);

    Coordinates newLoc = location.adjacent(xRooms,yRooms,dir);
    rooms[newLoc.x][newLoc.y].inhabitant = p;
    rooms[location.x][location.y].inhabitant = null;

    // clear the player's current position, redraw it in the new...
    Graphics g = getGraphics();
    Color savedColor = g.getColor();
    g.setColor(getBackground());
    g.fillRect(xRoomSize*location.x+2, yRoomSize*location.y+2,
	       xRoomSize-1, yRoomSize-1);
    g.setColor(savedColor);
    p.drawSelf(g, xRoomSize*newLoc.x+2, yRoomSize*newLoc.y+2,
	       xRoomSize-1, yRoomSize-1);

    return(newLoc);
  }


  // Keyboard input stuff

  // Buffer up to 10 keystrokes
  int kqLen = 10;
  int keyQueue[] = new int[kqLen];
  int kqNextIn = 0;
  int kqNextOut = 0;


  // Queue a keystroke
  void putKey(int key) {
    keyQueue[kqNextIn++] = key;
    if(kqNextIn == kqLen)
      kqNextIn = 0;
  }


  // Dequeue a keystroke or return -1 if there is none.
  synchronized int getKey() {
    int result;
    if(kqNextOut == kqNextIn)
      return -1;
    result = keyQueue[kqNextOut++];
    if(kqNextOut == kqLen)
      kqNextOut = 0;
    return result;
  }

};  // Class Maze


//------------------------------------------------------------------------


abstract class Player extends Thread {

  protected Maze myMaze;
  private Color myColor;
  private Coordinates myLocation = null;
  private int mySpeed;


  public Player(Maze maze, int startX, int startY, Color color, int speed) {
    myMaze = maze;
    myLocation = new Coordinates(startX, startY);
    myColor = color;
    mySpeed = speed;
  }


  public void run() {
    byte desiredDir;
    while (true) {
      desiredDir = pickDirection();
      if (desiredDir != -1) myLocation = myMaze.movePlayer(this, desiredDir);
      try {
	sleep(mySpeed);
      }
      catch(java.lang.InterruptedException e) {
	stop();
      }
    }
  }


  public Coordinates getLocation() {
    return(myLocation);
  }


  protected abstract byte pickDirection();


  // draw self into a square whose upper-left coordinate is x,y
  // and whose size is given by xSideLen and ySideLen
  // note: this assumes that it may draw on all pixels 
  // INCLUDING (x,y) and (x+xSideLen-1, y+ySideLen-1)

  public void drawSelf(Graphics g, int x, int y, int xSideLen, int ySideLen) {
    Color savedColor = g.getColor();
    g.setColor(myColor);

    // this just draws an X
    //g.drawLine(x+1,y+1,x+xSideLen-1-1,y+ySideLen-1-1);
    //g.drawLine(x+1,y+ySideLen-1-1,x+xSideLen-1-1,y+1);
    
    // this draws a rectangle one pixel in from the limit
    g.fillRect(x+1,y+1,xSideLen-2,ySideLen-2);

    g.setColor(savedColor);
  }

};  // class Player


//------------------------------------------------------------------------


class Human extends Player {

  private static final Color humanColor = Color.pink;
  private static final int humanSpeed = 100;


  public Human(Maze maze, int startX, int startY) {
    super(maze, startX, startY, humanColor, humanSpeed);
  }


  // get keyboard input

  protected byte pickDirection() {
    int key = myMaze.getKey();
    switch (key) {
      case 'j':
      case Event.LEFT:
	return(Coordinates.West);
      case 'i':
      case Event.UP:
	return(Coordinates.North);
      case 'l':
      case Event.RIGHT:
	return(Coordinates.East);
      case 'k':
      case Event.DOWN:
	return(Coordinates.South);
      default:
	return -1;
    }
  }

};  // class Human


//------------------------------------------------------------------------


class Monster extends Player {

  private static final Color monsterColor = Color.blue;
  private static final int monsterSpeed = 250;


  public Monster(Maze maze, int startX, int startY) {
    super(maze, startX, startY, monsterColor, monsterSpeed);
  }


  // monsters choose their direction randomly

  protected byte pickDirection() {
    boolean valid = false;
    byte dir = 0;
    while (!valid) {
      dir = (byte)(Math.random()*4);
      valid = myMaze.checkDirection(this, dir);
    }
    return(dir);
  }

};  // class Monster


//------------------------------------------------------------------------


public class MazeApplet extends Applet {

  private final int xRooms = 15;
  private final int yRooms = 15;
  private final int roomWidth = 25;
  private final int roomHeight = 25;
  private final int numMonsters = 3;

  private Button newMazeButton = null;
  private Maze maze = null;
  private Player[] players = null;


  public void init() {
    setBackground(Color.gray);

    newMazeButton = new Button("New Maze");
    add(newMazeButton);

    maze = new Maze(xRooms,yRooms,roomWidth,roomHeight);
    add("south", maze);

    createPlayers();
  }


  private void createPlayers() {
    int i, j;
    byte xx, yy;
    Coordinates startPos;
    boolean overlap;

    players = new Player[numMonsters+1];
    
    // create the human
    players[0] = new Human(maze,0,0);

    // create monsters, making sure none of them end up in the same place
    for (i=1; i<=numMonsters; i++) {
      do {
	xx = (byte)(Math.random()*xRooms);
	yy = (byte)(Math.random()*yRooms);
	startPos = new Coordinates(xx,yy);
	overlap = false;
	for (j=0; j<i; j++) {
	  if (Coordinates.equal(players[j].getLocation(), startPos))
	    overlap = true;
	}
      } while (overlap == true);
      players[i] = new Monster(maze,xx,yy);
    }

    // add them to the maze and set them going!
    for (i=0; i<=numMonsters; i++) {
      maze.addPlayer(players[i]);
      players[i].start();
    }

  }


  private void stopPlayers() {
    for (int i=0; i<=numMonsters; i++) 
      players[i].stop();
  }


  public boolean action(Event event, Object arg) {
    if (arg.equals("New Maze")) {
      stopPlayers();
      maze.rebuild();
      createPlayers();
      return(true);
    }
    return(false);
  }


  public boolean keyDown(Event e, int key) {
    maze.putKey(key);
    return(true);
  }

}
