/* Ataxx playing java applet/application 
   Copyright &copy; 1998  By Danny Sadinoff 
   Released under GNU Public License version 2

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

    or go to http://www.gnu.org
    
    */
import java.applet.*;
import java.awt.Frame;
import java.awt.*;
import java.util.*;
import java.io.*;
import java.awt.event.*;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.Math;

class AtaxxHistory implements Serializable, Observer{
   Board startingBoard;
   Vector moveVector = new Vector();
   String filename = "AtaxxHistory.jso";
   AtaxxHistory(Board board){
      board.addObserver(this);
   }
   AtaxxHistory(Board board, String fname_arg){
      board.addObserver(this);
      filename = fname_arg;
   }
   public void update( Observable o, Object closure){
      Move m = (Move)closure;
      Board b = (Board)o;
      if( m==null){
	 startingBoard = (Board)(b.clone());
	 System.out.print(startingBoard);
      }
      else{
	 add(m);
      }
   }
   void add(Move m){
      moveVector.addElement((Object)m);
      //      write();
   }
   public String toString(){
      return startingBoard.toString() + moveVector.toString();
   }
   void write(){
      try{
	 FileOutputStream ostream = new FileOutputStream(filename);
	 ObjectOutputStream oos = new ObjectOutputStream(ostream);
	 oos.writeObject(this);
	 oos.flush();
	 ostream.flush();
	 oos.close();
	 ostream.close();
      }catch( IOException ioe){
	 ioe.printStackTrace();
      }
   }
}


/* Board notifications will be either notifyObservers() or 
   notifyObservers(Move M), where m is the move just made.  
   We want a world here is that the history of the game can be rebuilt
   from the start state and the sequence of moves.  This necessitates "passes" as a Move. */
class Board extends Observable implements Serializable{
   static public final int FIRST_PLAYER = 0;
   static public final int EMPTY = -1;
   static public final int BLOCK = -2;

   int playerToGo = FIRST_PLAYER;
   int numPlayers = 2;
   int width;
   int height;
   int emptySquares;
   private int [][]field;
   int playerCount[];
   boolean moovedYet = false;

   public Object clone(){ return this;}
   
   Board(int width_arg, int height_arg){
      width = width_arg;
      height= height_arg;
      field = new int[width][height];
      for( int r = 0; r < height; r++)
	 for( int c = 0; c < width; c++)
	    field[c][r] = EMPTY;
      playerCount = new int[numPlayers];
      emptySquares = height*width;
      set(0,0,FIRST_PLAYER);
      set(width-1,height-1,FIRST_PLAYER);
      set(width-1,0,FIRST_PLAYER+1);
      set(0,height-1,FIRST_PLAYER+1);
      setBlock(width/2,height/2);
   }

   public String toString(){
      return ("{ Board:\n\tplayerToGo = "+playerToGo+
	      "\n\tnumPlayers = "+numPlayers+
	      "\n\twidth = "+width+
	      "\n\theight = "+height+
	      "\n\temptySquares = "+emptySquares+
	      "\n\tfield= "+field+
	      "\n\tplayerCount= "+playerCount+"\n}\n");

   }

   void set(int col, int row, int newValue){
      int oldValue = field[col][row];
      if ( oldValue == newValue)
	 return;
      switch(oldValue)
      {
	case BLOCK:
	   break;
	case EMPTY:
	   emptySquares--;
	   break;
	default:
	   playerCount[oldValue]--;
      }
      switch(newValue){
	case BLOCK:
	   break;
	case EMPTY:
	   emptySquares++;
	   break;
	default:
	   playerCount[newValue]++;
      }
      field[col][row] = newValue;
      setChanged();
   }

   void setBlock(int col, int row){
      set(col, row, BLOCK);
   }
   
   void clear(int col, int row){
      set(col, row, EMPTY);
   }

   private boolean isAnOpponent(int player){
      return (player >= FIRST_PLAYER && 
	      player != playerToGo);
   }

   boolean gameOver(){
      if( 0 == emptySquares)
	 return true;
      
      boolean oneColorInPlay = false;
      for( int pl = FIRST_PLAYER;
	   pl < numPlayers;
	   pl++)
	 if( playerCount[pl] > 0 ){
	    if( oneColorInPlay )
	       return false;
	    oneColorInPlay = true;
	 }
      return true;
   }


   String winnerString(){
      int max = -1;
      int maxingPlayersCount =0;
      int [] maxingPlayers = new int[numPlayers];
      for( int pl = FIRST_PLAYER ;
	   pl < numPlayers;
	   pl++)
      {
	 if( playerCount[pl] > max ){
	    max = playerCount[pl];
	    maxingPlayers[0] = pl;
	    maxingPlayersCount =1;
	 }
	 else if( playerCount[pl] == max ){
	    maxingPlayers[maxingPlayersCount++] = pl;
	 }
      }
      if( maxingPlayersCount == 1){
	 return BoardCanvas.names[maxingPlayers[0]] + " Won";
      }
      else if (maxingPlayersCount >1){
	 StringBuffer buf = new StringBuffer("It's a "+maxingPlayers+"-way tie, between ");
	 for( int pl = 0; pl < maxingPlayersCount-1; pl++){
	    buf.append(BoardCanvas.names[maxingPlayers[pl]]+", ");
	 }
	 buf.append("and "+BoardCanvas.names[maxingPlayers[maxingPlayersCount-1]]);
	 return buf.toString();

      }else 
	 throw new IllegalArgumentException("Aiee.  < 1 winners!\n");
   }

   private void advancePlayer(){
      playerToGo = (1+playerToGo) % numPlayers;
   }

   boolean mustPass(){
      for( int mt_row = 0; mt_row < height; mt_row++)
	 for( int mt_col = 0; mt_col < width; mt_col++)
	 {
	    if( field[mt_col][mt_row] != EMPTY )
	       continue;
	    
	    for( int h = -2; h <= 2; h++)
	       for( int v = -2; v <= 2; v++){
		  int col = mt_col + h;
		  int row = mt_row + v;
		  if( col < 0 || col >= width ||
		      row < 0 || row >= height)
		     continue;
		  if( playerToGo == field[col][row])
		     return false;
	       }
	 }	  

      return true;
   }

   void applyMove(Move m) throws IllegalMoveException{
      if( !moovedYet ){		// only send the board-setup notification once
	 notifyObservers();
	 moovedYet = true;
      }
      if( m.player != playerToGo )
	 throw new IllegalMoveException(
	    "It's "+BoardCanvas.names[playerToGo].toString()+"'s turn.");

      if( m.isPass)
      {
	 if( !mustPass())
	    throw new IllegalMoveException(
	       "Player must have no legal moves to pass.");
      }
      else{
	 int player = field[m.from_col][m.from_row];
	 if( player != playerToGo )
	    throw new IllegalMoveException(
	       "It's "+BoardCanvas.names[playerToGo].toString()+"'s turn.");
	 if( field[m.to_col][m.to_row] != EMPTY )
	    throw new IllegalMoveException(
	       "You can only move to a clear space");

	 int dist = Math.max(Math.abs(m.to_col - m.from_col),
			     Math.abs(m.to_row - m.from_row));
	 
	 if( dist > 2 )
	    throw new IllegalMoveException(
	       "You're trying to jump too far");
	 
	 if( dist == 2 )
	    clear(m.from_col,m.from_row);
	 
	 set(m.to_col,m.to_row,player);
	 for( int h = -1; h <= 1; h++)
	    for( int v = -1; v <= 1; v++){
	       int col = m.to_col + h;
	       int row = m.to_row + v;
	       if( col < 0 || col >= width ||
		   row < 0 || row >= height)
		  continue;
	       if( isAnOpponent(field[col][row]))
		  set(col,row,player);
	    }
      }
      advancePlayer();
      notifyObservers(m);
   }

   int getVal(int col, int row ){
      return field[col][row];
   }
}

class IllegalMoveException extends Exception{
   IllegalMoveException(){super();}
   IllegalMoveException(String s){super(s);}
}

class Move implements Serializable{
   int player;
   boolean isPass;
   int from_col, from_row, to_col, to_row;
   Move(int player_arg ){	// pass
      player = player_arg;
      isPass = true;
   }
   Move(int player_arg, int fc, int fr, int tc, int tr){
      player = player_arg;
      isPass = false;
      from_col = fc;
      from_row = fr;
      to_col = tc;
      to_row = tr;
   }
   
   public String toString(){
      if( isPass )
	 return "Player #"+player+" passes.";
      else
	 return ("Player #"+player+" moves ("+from_col+", "+from_row+
		 ") -> ("+to_col+", "+to_row+")");
   }
}

interface StatusReporter{
   void setStatus(String s);
}

interface BoardDragReporter{
   void dragged(Move m, Board board);
}

class BoardCanvas extends Canvas implements Observer{
   Board board;
   static final int boardMargin =5;
   static final double squareMargin = 0.1;
   Dimension dim;
   StatusReporter sr = null;
   BoardDragReporter bdr = null;
   static final Color[] colors = {
      Color.red,
      Color.blue,
      Color.green,
      Color.orange,
   };
   static final String[] names = {
      "Red",
      "Blue",
      "Green",
      "Orange",
   };
   
   BoardCanvas(Board b) {
      board = b;
      board.addObserver(this);
 
      MouseAdapter mAdapter = new MouseAdapter(){
	 boolean dragging = false;
	 int dragStartCol, dragStartRow;
// 	 public void mouseClicked(MouseEvent e){
// 	    if ( null == sr )
// 	       return;
// 	    int size = (int)((float)Math.min(dim.width - 2*boardMargin, dim.height - 2*boardMargin) /
// 			     (float)Math.max(board.width,board.height)) ;
// 	    int col = (e.getX() - boardMargin)/ size;
// 	    int row = (e.getY() - boardMargin)/ size;
// 	    sr.setStatus("click at ("+col + ", "+row+")");
// 	 }
	 public void mousePressed(MouseEvent e){
	    int size = (int)((float)Math.min(dim.width - 2*boardMargin, dim.height - 2*boardMargin) /
			     (float)Math.max(board.width,board.height)) ;
	    dragStartCol = (e.getX() - boardMargin)/ size;
	    dragStartRow = (e.getY() - boardMargin)/ size;
	    if( dragStartCol >=0 && dragStartCol < board.width &&
		dragStartRow >=0 && dragStartRow < board.height)
	       dragging = true;
	 }

	 public void mouseReleased(MouseEvent e){
	    int size = (int)((float)Math.min(dim.width - 2*boardMargin, dim.height - 2*boardMargin) /
			     (float)Math.max(board.width,board.height)) ;
	    if ( !dragging )
	       return;
	    if ( null == bdr )
	       return;

	    int dragEndCol = (e.getX() - boardMargin)/ size;
	    int dragEndRow = (e.getY() - boardMargin)/ size;
	    if( dragEndCol >=0 && dragEndCol < board.width &&
		dragEndRow >=0 && dragEndRow < board.height)
	       bdr.dragged(new Move(board.playerToGo, 
				    dragStartCol, dragStartRow,
				    dragEndCol, dragEndRow),
			   board);
	    dragging = false;
	 }
      };
      
      addMouseListener(mAdapter);
   }


   /* the board has changed, so we have to repaint it. */
   /* perhaps someday we'll refine this so that only the 
      affected areas are repainted */
   public void update( Observable o, Object closure ){ 
      repaint();
   }
  
   void setStatusReporter( StatusReporter sr_arg ) {
      sr = sr_arg;
   }

   void setBoardDragReporter( BoardDragReporter bdr_arg ) {
      bdr = bdr_arg;
   }

   public void paint( Graphics g){
      dim = getSize();
      g.setColor(Color.black);

      int size = (int)((float)Math.min(dim.width - 2*boardMargin, dim.height - 2*boardMargin) /
		       (float)Math.max(board.width,board.height)) ;
      int right = boardMargin + size* board.width;
      int bottom = boardMargin + size* board.height;

      for( int c = 0; c < board.width; c++) {
	 for( int r = 0; r < board.height; r++) {
	    int val;
	    switch(val = board.getVal(c,r)){
	      case Board.EMPTY:
		 break;
	      case Board.BLOCK:
		 g.setColor(Color.darkGray);
		 g.fillRoundRect((int)(boardMargin+ size *( squareMargin + c)),
				 (int)(boardMargin+ size *( squareMargin + r)),
				 (int)(size - 2*squareMargin*size),
				 (int)(size - 2*squareMargin*size),
				 (int)(2*squareMargin*size),
				 (int)(2*squareMargin*size));
		 break;
	      default:
		 g.setColor(colors[val]);
		 g.fillOval((int)(boardMargin + size*(squareMargin + c)),
			    (int)(boardMargin + size*(squareMargin + r)),
			    (int)(size - 2*squareMargin*size),
			    (int)(size - 2*squareMargin*size));

	    }
	 }
      }
      
      g.setColor(Color.black);
      //draw border 
      g.drawRect(boardMargin,boardMargin, size* board.width, size* board.height);

      // draw gridlines
      for( int c = 0; c < board.width; c++) {
	 g.drawLine(boardMargin + c *size, boardMargin,
		    boardMargin + c *size, bottom);
      }
      for( int r = 0; r < board.height; r++) {
	 g.drawLine(boardMargin, boardMargin + r *size,
		    right, boardMargin + r *size);
      }
   }
}

public class Ataxx extends Applet{
   BoardCanvas bCanvas;
   AtaxxHistory history;
   TextField status;
   public Ataxx(){this(6);}
   public Ataxx( int boardDim ){
      Board b = new Board(boardDim, boardDim);
      history = new AtaxxHistory(b);
      bCanvas = new BoardCanvas(b);
      bCanvas.setVisible(true);

      status = new TextField(
	 "It's "+BoardCanvas.names[bCanvas.board.playerToGo].toString()+"'s turn.");
    
      bCanvas.setStatusReporter(new StatusReporter(){
	 public void setStatus(String s){
	    status.setText(s);
	 }});

      bCanvas.setBoardDragReporter(new BoardDragReporter(){
	 public void dragged(Move move, 
				Board board){
	    if( board.gameOver())
	       return;
	    status.setText("Trying move "+move.toString());
	    try{
	       board.applyMove(move);
	       if( board.gameOver()){
		  status.setText(board.winnerString());
	       }
	       else 
	       {
		  StringBuffer buf = new StringBuffer();
		  if( board.mustPass()){
		      buf.append(BoardCanvas.names[board.playerToGo].toString()+" must pass.  ");
		     board.applyMove(new Move(board.playerToGo)); // pass
		  }
		  buf.append("It's "+BoardCanvas.names[board.playerToGo].toString()+"'s turn.");
		  status.setText(buf.toString());
	       }
	    } catch( IllegalMoveException ime ){
	       status.setText( ime.getMessage());
	    }
	 }});
    
      Button quitBut = new Button("quit");
      quitBut.addActionListener(new ActionListener(){
	 public void actionPerformed(ActionEvent e){
	    System.exit(0);
	 }
      });
    
   }
   public void init(){
      setLayout(new GridLayout(0,1));
      add( bCanvas );
      add( status );
      setVisible(true);
   }

   public static void main(String [] argv){
      final Frame f = new Frame("Ataxx");
      f.setBounds(10,10,300,600);
      f.addWindowListener( new WindowAdapter(){
	 public void windowClosed(WindowEvent e){
	    System.exit(0);
	 }
	 public void windowClosing(WindowEvent e){
	    f.dispose();
	 }
      });
      Ataxx a = new Ataxx(6);
      f.add(a);
      a.init();
      f.setVisible(true);
   }
}



