Chris Pollett>Old Classes>PIC 20, Winter 2000>Hw4>Hw4 Solutions

Winter 2000 PIC 20 HW4 Solutions Page

// Program Name: FPuzzle.java
//
// Purpose:  This is the file that contains the applet part of a program
//	     to play the 2X2, 3X3, 4X4, version 5X5 of the Tile game
//
// Known Bugs:  Probably could break the init method into a nicer grouping
//		of methods.
//

import java.awt.*;
import javax.swing.*;
import java.awt.event.*;

public class FPuzzle extends JApplet
{
	
	Puzzle puzzle; // this is a JPanel where actual puzzle drawn

	int size = 3; // initial puzzle size.

	Container c; 

	JButton randomize, solve;
	JPanel buttonPanel, sizeAndLabel, directionGadget;	
	JLabel sizeLabel,title;

	
	// Name: init()
	//
	// Purpose: This method adds all the GUI components onto theapplet
	//
	// Known Bugs:  Should probably be split into smaller methods
	//

	public void init()
	{

		String sizeArray[] = new String[size+1]; // Strings for the
							//JComboBox
		for(int i=0; i <= size;i++)
		{ 
			sizeArray[i] = ""+(i+2);
		}


		//
		//set up the background layout and font of the applet
		//


		c = getContentPane();
		c.setBackground(Color.white);

		c.setLayout(new BorderLayout());
		c.setFont( new Font("Serif",Font.PLAIN,15));


		//
		// add title label
		//


		title = new JLabel("Tile Puzzle");
		title.setFont( new Font("Serif", Font.BOLD,30));
		c.add(title,BorderLayout.NORTH);
		
		//
		//add puzzle
		//
		
		puzzle = new Puzzle(size,c);
		puzzle.setBackground(Color.white);
		c.add(puzzle,BorderLayout.CENTER);

		//
		// add SizeSelector and set up its listener
		//

		sizeLabel = new JLabel("Size Selector");

		sizes = new JComboBox(sizeArray);
		sizes.setMaximumRowCount(3);
		sizes.setSelectedIndex(size-2);
		sizes.addItemListener(
			new ItemListener()
			{
			public void itemStateChanged(ItemEvent e)
				{
					int sel= sizes.getSelectedIndex();					 			
					if(sel+ 2 != size)
 					{  			
	 					size = sel + 2;
	 					puzzle.setPuzzleSize(size);
	 					c.repaint(); 
					}
 				}
	 		});

		//
		//Initalize and add listeners to buttons controlling
		//movement
		//

		setUpDirectionGadget();

		//		
		//Put sizeLabel JLabel, sizes JComboBox, and direction gadget
		// on a JPanel and add it to the applet 
		//

		sizeAndLabel = new JPanel();
		sizeAndLabel.setLayout(new GridLayout(3,1,5,5));
		sizeAndLabel.setBackground(Color.white);
		sizeAndLabel.add(sizeLabel);
		sizeAndLabel.add(sizes);
		sizeAndLabel.add(directionGadget);

		c.add(sizeAndLabel,BorderLayout.EAST);

		//
		//add the randomize and solve buttons and their listeners 
		//
	
		randomize =new JButton("Randomize");
		randomize.addActionListener(
				new ActionListener()
				{
				public void actionPerformed(ActionEvent e)
					{
						puzzle.randomize();
						c.repaint();
 					}
 				}
	 	 	);
		
		solve =new JButton("Solve");
		solve.addActionListener(
			new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
					{
					
					puzzle.solveTimer(FPuzzle.this); 
	 	 	 	 	} 	
	} 		);	

			
		buttonPanel = new JPanel();
		buttonPanel.setLayout(new GridLayout(1,2,5,5));
		buttonPanel.setBackground(Color.white);
		buttonPanel.add(randomize); 
		buttonPanel.add(solve);				
		c.add(buttonPanel, BorderLayout.SOUTH);
	}

	// Name: setUpDirectionGadget()
	//
	// Purpose: Puts the four direction control buttons on a JPanel
	//	    and adds their listeners. The movements should
	//	    be thought of moving the hole around 
	//
	// Known Bugs:  
	//

	void setUpDirectionGadget()
	{
		directionGadget = new JPanel();
		directionGadget.setLayout(new BorderLayout());
		directionGadget.setBackground(Color.white);

		JLabel move;
		JButton north,south,east,west;
		
		move = new JLabel("move");
		directionGadget.add(move,BorderLayout.CENTER);

		north = new JButton("^");
		north.addActionListener(
			new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
					{
					
					puzzle.move(Puzzle.NORTH); 
					FPuzzle.this.repaint();
	 	 	 	 	} 	
			} 		
		);	

		east = new JButton(">");
		east.addActionListener(
			new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
					{
					
					puzzle.move(Puzzle.EAST); 
					FPuzzle.this.repaint();
	 	 	 	 	} 	
			} 		
		);

		west = new JButton("<");
		west.addActionListener(
			new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
					{
					
					puzzle.move(Puzzle.WEST); 
					FPuzzle.this.repaint();
	 	 	 	 	} 	
			} 		
		);

		south = new JButton("v");
		south.addActionListener(
			new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
					{
					
					puzzle.move(Puzzle.SOUTH); 
					FPuzzle.this.repaint();
	 	 	 	 	} 	
			} 		
		);							

		directionGadget.add(north,BorderLayout.NORTH);
		directionGadget.add(east,BorderLayout.EAST);
		directionGadget.add(west,BorderLayout.WEST);
		directionGadget.add(south,BorderLayout.SOUTH);	
	}

}

// Program Name: Puzzle.java
//
// Purpose: This class extends JPanel and is used to draw the tile puzzle
//	    It has a method move(i) which given a direction will
//	    will move the whole in the puzzle in that direction. it also
//	    has a public method randomize() to randomize the puzzle and
//	    a method solveTimer() which  solves the puzzle and
//	    and starts a timer of the moves to do in carrying out the
//	    solution.
//
//
// Known Bugs: The directions for NORTH SOUTH EAST WEST have been modified
//		to improve speed of the move method. It would not be too
//	        hard to change move(i) to map the spec values to these values 
//              then do move(i, puzzleArray) but would probably make the code
//		less clear


import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;

public class Puzzle extends JPanel
{
	public static final int NORTH=-1, SOUTH=1, WEST=-2, EAST=2;
			//integer values correspond to moving
			// moving a tile. Not exactly as in the spec
			// see above. Notice NORTH = -SOUTH
			// and WEST = -EAST. Will use this to save
			// copying boards in minToConsider method
 
	public int searchDelay=10; // time between moves when showing
				//solution to puzzle

	public int moveDelay=10; //timer delay bewteen 2 pixel increments
				//in moving a piece

	public int pieceSizeX =40, pieceSizeY=40; // x and y dimension of
						// a tile

	public int slideX, slideY,xOff, yOff, mDir;
			//x and y in matrix of puzzle of piece being moved
			//xOff and yOff are how many pixels the tile has
			//currently been moved
			//mdir is the movement direction
 	
	public boolean moving=false, wasRunning =false;
		//booleans used in drawing the puzzle if a piece is moving
		//and for the timers

	public int ranMoves=200; // hom many random moves to insert
				//in randomizing the board	
	 	
	Container c; //container of the applet

	JApplet test; //name of applet that puzzle lives on so
			// we can call showStatus

	Rectangle tile; //tile shape
	GradientPaint tColor; //gradient paint for the tile
 	Rectangle border; // big rectangle for perimeter of puzzle

	int puzzleArray[][]; //array which stores the puzzle

	int tmp[][]; // temporary array used in solving puzzle

	int less[][]; // temporary array use to store
			// puzzle of current lowest value
			// while solving the puzzle
	
	Point blankLocation =new Point(0,0); //used in solving the
					//puzzle to store the x,y location
					// of the hole on the current board
					//under consideration

	int puzzleSize,colSize,rowSize; // respectively
			// the number of tiles in a row or column, the
			// number of pixels in a column, and the number
			// of pixels in a row

	int totalNumPieces;//total number of squares in the puzzle

	Timer puzzleTimer, moveTimer;

	Hashtable active;
	HashSet inActive;

	// Name: Puzzle
	//
	// Purpose: constructor for the Puzzle. It sets the container
	//	    whose repaint method should be called during a move
	//	    then initializes the puzzle array. 
	//
	// Known Bugs:  
	//


	public Puzzle(int pSize, Container parent)
	{
		c=parent;

		setPuzzleSize(pSize);
	 }

	// Name: setPuzzleSize
	//
	// Purpose: Initializes the puzzleArray as well as any global
	// 	    variables that depend on puzzleSize.
	//
	// Known Bugs:  
	//


	public void setPuzzleSize(int pSize)
	{
		if (puzzleTimer != null) puzzleTimer.stop();
		if (moveTimer != null) moveTimer.stop();

		puzzleSize = pSize;
		totalNumPieces = puzzleSize;

		puzzleArray = new int[puzzleSize][puzzleSize]; 
						//elts initialized to zero

 		tmp=new int[puzzleSize][puzzleSize];
                less =new int[puzzleSize][puzzleSize];

		tile=new Rectangle(0,0,pieceSizeX,pieceSizeY);
		tColor = new GradientPaint( 0,0,Color.white, 	 
	  	 	 	 pieceSizeX, pieceSizeY,Color.blue,false);

		colSize = pieceSizeY*puzzleSize;
		rowSize = pieceSizeX*puzzleSize;
		border = new Rectangle(5,5,10+rowSize,10+colSize);

		randomize();

	}

	// Name: randomize
	//
	// Purpose: Inserts ranMoves many move into the board to randomize it
	//
	// Known Bugs:  
	//


	public void randomize()
	{
		int tmp =0;

		// make sure timers don't go off

		if (puzzleTimer != null) puzzleTimer.stop(); 
		if (moveTimer != null) {moveTimer.stop();
					moving = false;}

		for(int i =0; i< puzzleSize; i++)
		{
			for(int j=0; j< puzzleSize; j++)
			{ 
				puzzleArray[i][j] = i+j*puzzleSize;
			}
		}

		for(int i= 0; i< ranMoves; i++)
		{
			tmp = (int)(Math.random()*4.99)-2;
			if(tmp==0) continue;
 			move( tmp,puzzleArray);
 		}

	}

	// Name: solveTimer
	//
	// Purpose: Solves the puzzle and sets up a Timer to execute the
	//	    solution moves
	//
	// Known Bugs:  
	//

	public void solveTimer(JApplet par)
	{
		test=par;

		if(puzzleTimer != null) puzzleTimer.stop();

		puzzleTimer = new Timer(searchDelay, new ActionListener()
			{
				Stack s =new Stack();

				public void actionPerformed(ActionEvent e)
				{
					if(s.isEmpty()){
						puzzleTimer.stop();	  
 					        s=Puzzle.this.solve();   
	 					if(!s.isEmpty())
	 	 	 	 		 puzzleTimer.restart(); 
	 	   			}    	 	
					else 			
		 			{move(((Integer)s.pop()).intValue());}
     	  	 	 	}   

	 	 	}  );

   	 	puzzleTimer.start();

	}

	// Name: getEmpty
	//
	// Purpose: Put into the global variable blankLocation
	//	    the x,y value of the blank in board[][]
	//
	// Known Bugs:  
	//

	void  getEmpty(int board[][])
	{
		for(int i=0; i < puzzleSize;i++)
		{
			for(int j=0; j < puzzleSize; j++)
			{
			  if(board[i][j]==0)
			    {blankLocation.setLocation(i,j); return;}	  
			}   	
		} 
	}

	// Name: valuePuzzle
	//
	// Purpose: Computes the value of a board according to
	//	    the sum of the distance each piece is from their
	//	    correct position 
	//
	// Known Bugs:  
	//


	public int valuePuzzle(int board[][])
	{
		int sum =0;
		int tmp;

		for(int i=0; i < puzzleSize; i++)
		{
			for(int j=0; j < puzzleSize; j++)
			{
				tmp = board[i][j];
				sum += Math.abs(i-tmp%puzzleSize) 
						+ Math.abs(j-tmp/puzzleSize); 
			}
		} 

		return sum;
	}

	// Name: move(int i)
	//
	// Purpose: public method used by FPuzzle and solveTimer
	//	    to move the puzzle pieces
	//
	// Known Bugs:  
	//


	public void move(int i)
	{
		getEmpty(puzzleArray);

		xOff =0;
		yOff =0;

		if(!move(i,puzzleArray)) return;

		slideX = blankLocation.x;
		slideY = blankLocation.y;  

		mDir = (i >0)? 1 : -1;

		if(Math.abs(i) == 1) yOff=pieceSizeY;
		else xOff=pieceSizeX;

		moving =true;
		wasRunning = false;

		moveTimer = new Timer(moveDelay, new ActionListener()
			{
				public void actionPerformed(ActionEvent e)
				{       c.repaint();
					if(xOff == 0 && yOff == 0)
					{ moving = false;
					  moveTimer.stop();
					if(wasRunning) puzzleTimer.restart();
					}					
					
					if( xOff> 0) xOff-=2;	
					if( yOff> 0) yOff-=2;
				} 			
			});
		
		if(puzzleTimer != null && puzzleTimer.isRunning())
		{
			puzzleTimer.stop();
			wasRunning=true;
		}

		moveTimer.start();
	
	}

	// Name: move(int i, int board[][])
	//
	// Purpose: changes board[][] to the puzzle that
	//	    that would result if move i was applied
	//	    If move can be applied true is returned 
	//	    If move not valid false returned
	//
	// Known Bugs:  
	//


	boolean move(int i, int board[][])
	{
		getEmpty(board);

		int xLoc = blankLocation.x;
		int xMov = xLoc + i/2;
		int yLoc = blankLocation.y;
		int yMov = yLoc + i%2;

		if(xMov < 0 || xMov >= puzzleSize || 
				yMov < 0 || yMov >= puzzleSize)
						return false;

		board[xLoc][yLoc] = board[xMov][yMov];
		board[xMov][yMov] = 0;

		return true;		
	 }


	// Name: solve()
	//
	// Purpose: solves the puzzle returns a stack with the moves for
	//	    the solution. Uses the A* algorithm
	//
	// Known Bugs:  
	//


	public Stack solve()
	{
		active = new Hashtable(); // Hashtable is in java.util
					// entries are key value pair
					// and given a key the method
					// to look up the associated value
					// is very fast
		
		inActive = new HashSet(); //Hashset is in java.util
					  //It contains collection of objects
 					  //and the method to see if an
					//object is already stored is
					//very fast	

		Stack ansStack = new Stack();
		
		BoardNode b;

		Integer hash;

		int min =valuePuzzle(puzzleArray);
		if(min == 0 ) return ansStack;

		b =new BoardNode();
		b.setBoard(puzzleArray,null, min, 0,0);

	 	active.put(hashBoard(puzzleArray),b);


		while(!active.isEmpty())
		{
			b=minToConsider();
  

			if(b.value == 0) return b.path();

			//check if any node should be moved to
			//the inActive Hashset

			if(nodeDone(b.parent)) 
			{
				hash=hashBoard(b.parent.board);
				inActive.add(hash);
				active.remove( hash );
			} 

			if(nodeDone(b)) 
				{
				hash=hashBoard(b.board);
				inActive.add(hash);
				active.remove(hash); 
			 	}
 			else {
 				active.put(hashBoard(b.board),b);
				}
	 	}

		return ansStack;
	}

	// Name: minToConsider()
	//
	// Purpose: Searches the children of the active
	//          Hashtable BoardNodes and returns the
	//	    least child that has not yet been considered
	//
	// Known Bugs:  
	//


	public BoardNode minToConsider()
	{


		BoardNode b, least=new BoardNode();
		int min = 1000;
		int val,depth,heu;
		Integer hash;

		Enumeration e=active.elements(); //an Enumeration
					// is a class in java.util
					// If a class has a method
					// to returns an enumeration
					// then the Enumeration
					// can be used to list out the
					// data stored in the class.
					// In this case the active's
					// element() method returns an
					// Enumeration class for the values
					// stored in the Hashtable.
					// The Enumeration methods
					// nextElement()
 				 	// and hasMoreElements() can be used to
		 			// list out these values. 
					// Like an Iterator in C++ or
					// Java 1.2	

		while(e.hasMoreElements())
		{
			b = (BoardNode)e.nextElement();

			copyBoard(b.board,tmp);
			depth = b.depth+1;
	
			for(int j=-2; j <= 2; j++)
			{
				if(j == 0) j++;
				if(!move(j,tmp)) continue;

			        val=valuePuzzle(tmp);
				hash=hashBoard(tmp);
				heu = val+depth; // this is the A* heuristic
	 	
	 			 if(heu < min&&!active.containsKey(hash)
    	 			    &&!inActive.contains(hash))
	 	 		  { 	 
				   min = heu; 	
	   	 		   copyBoard(tmp,less);  	
	   	 		   least.setBoard(less,b,val,j,depth);
 	 			 }

				move(-j,tmp); //undoes the just tried move

			}
 		}
		
		test.showStatus("depth="+least.depth+"val="+least.value);
		

  		return least;
	}

	// Name: hashBoard
	//
	// Purpose: returns a unique integer object corresponding
	//	    to a board. To check if two key object
	//	    are equal in a Hashtable the equals method of
	// 	    method of the object is called. The default
	//	    equals in the Object class just compares
	//	    references but that of the Integer class
	//	    actually checks if the numbers stored in the objects
	//          are equal. This was the reason this function was written 
	//
	// Known Bugs: For the 5X5 board there may be conflicting
	//	       keys so would probably be better off 
	//	       returning Long but that would be slower.


	public Integer hashBoard( int board[][])
	{
		int hash=0;

		for(int i =0 ; i < puzzleSize; i++)
		{
			for(int j=0; j < puzzleSize; j++)
			{
			 hash += board[i][j];
			 hash *= totalNumPieces;			 	
			}
		}

		return new Integer(hash);
	}

	// Name: nodeDone
	//
	// Purpose: Returns true if all the children of the
	//          BoardNode b either appear in active or inActive
	//
	// Known Bugs:  
	//


	public boolean nodeDone (BoardNode b)
	{
		Integer hash;		

		copyBoard(b.board,tmp);

		for(int i=-2; i <= 2; i++)
		{
			if(i==0) i++;
			if(!move(i,tmp)) continue;

			hash=hashBoard(tmp);

			if(!active.containsKey(hash) &&
					!inActive.contains(hash)) 
				{
				move(-i,tmp); //undo just tried move
				return false;
				}
			move(-i,tmp); //undoes the just tried move
		}
		return true;
	}

	// Name: copyBoard
	//
	// Purpose: copies the puzzle in board to board2
	//
	// Known Bugs:  
	//


	void copyBoard(int board[][], int board2[][])
	{
		for(int i =0 ; i< puzzleSize; i++)
		{
			for(int j =0 ; j < puzzleSize; j++)
				{ board2[i][j]=board[i][j];}
		}
	}

	// Name: paint
	//
	// Purpose: draws the puzzle
	//
	// Known Bugs:  
	//


	public void paint( Graphics g)
	{

		Graphics2D g2d = (Graphics2D) g;

		AffineTransform t;
		
		int pieceij;
		int xOffDir = xOff*mDir;
		int yOffDir = yOff*mDir;

		g2d.setPaint(Color.black);
		g2d.fill( border);


		g2d.translate(10-pieceSizeX,10+colSize-pieceSizeY);

		for(int i=0; i < puzzleSize; i++)
		{
			g2d.translate(pieceSizeX,-colSize);

			for(int j=0; j < puzzleSize;j++)
			{
				pieceij = puzzleArray[i][j];

				g2d.translate(0,pieceSizeY);
	 	

				if(pieceij != 0 && 
				(!moving || slideX !=i || slideY !=j))
  				{ 
					g2d.setPaint(tColor); 
 		 	 		g2d.fill( tile); 	
				 
				 	g2d.setColor(Color.black);
			 	 	g2d.drawString( ""+pieceij,15,15);
  		 		}

   				else if(pieceij==0 && moving)
 				{ 				
				t= g2d.getTransform();
					
				g2d.translate(pieceSizeX*(slideX-i),
						pieceSizeY*(slideY-j));

				g2d.setPaint( Color.black);
	 	 	 	g2d.fill( tile); 

				g2d.translate(xOffDir,yOffDir);
	 			g2d.setPaint( tColor);   

 	 	 		g2d.fill( tile); 
			 		 
	 			g2d.setPaint(Color.black);
	 	 	 		
				g2d.drawString(""+ puzzleArray[slideX][slideY],
					15, 15);
					
				g2d.setTransform(t);
					
	  		 	} 	

 			}
		}
	}

	// Name: class BoardNode
	//
	// Purpose: This class is used by solve() to store
	//	   data on board that it is considering as part of the
	//	   solution process. The data that is stored is the
	//	   board itself, the parent BoardNode, the value of the
	//	   board, the move that changed the parent to this board,
	//	   and depth of the branch from puzzleArray to this board.
	//
	// Known Bugs:  
	//


	class BoardNode
	{
		public int board[][] =new int[puzzleSize][puzzleSize];
		public BoardNode parent;
		public int value, move, depth;

		// Name: BoardNode
		//
		// Purpose: Constructor. It does nothing since for
		//	    speed considerations was faster to do things in
		//	    setBoard.
 		//
		// Known Bugs:  
		//


		public BoardNode() {}
		


		// Name: setBoard
		//
		// Purpose: set the values of all the parameters of
		//	    the BoardNode
		//
		// Known Bugs:  
		//
		public void setBoard(int b[][],BoardNode par, 
				int val,int mv,int d)
  		{
			copyBoard(b,board);
			parent =par;
			value =val;
			depth =d;
			move =mv;
		}
	


		// Name: path
		//
		// Purpose: Returns a Stack with the path of moves 
		//	    from puzzleArray to this board
		//
		// Known Bugs:  
		//

		public Stack path()
		{
			Stack s =new Stack();
			BoardNode tmp = this;		

			do{
				s.push(new Integer(tmp.move));
				tmp = tmp.parent;
			} while (tmp != null);

			s.pop(); //top element move doesn't count

			return s;
		}
	}

}