// PuzzleApp.java import java.awt.*; import java.awt.event.*; import java.util.Vector; public class PuzzleApp extends Frame implements ActionListener, KeyListener, ItemListener, Runnable { public static int[] goalState; private static final int MINPATHS = 100, MAXPATHS = 100000, EAStar = 0, AStar = 1, BFS = 2, NO_ERROR = 0, INVALID_INT = 1, EXCEEDS_MAX_TILES = 2, NOT_ENOUGH_TILES = 3, MISSING_SPECIFIC_TILE = 4, RETRIEVE_ERROR = 5; private int expandCount, pathsToTraverse, error, numOfElements, dim, PUZZLESIZE = 9, puzzleStep; private PuzzleStarter ps; private boolean runsFromApplet, hasAnswer; private Puzzle puzzle; private TextField orderField, maxPathsField, movesField, pathsField, expandedField, statusField, timeField; private List dirList; private Button solve, contPause, reset, show; private CharArray dataLine; private int[] initState; private Thread runner = null, currentThread = null; private String pathStr = ""; private Checkbox cbEight, cbFifteen; private Choice algChoice; private String[] algStrings = {"Enhanced A*", "A*", "Breadth First"}; private Panel puzPanel; private Node bestNode; private Cursor defaultCursor, waitCursor; public PuzzleApp(PuzzleStarter _ps, boolean _runsFromApplet) { super("The 8-Puzzle Solver"); ps = _ps; runsFromApplet = _runsFromApplet; setBackground(SystemColor.control); dim = (int)Math.sqrt(PUZZLESIZE); goalState = Utility.makeGoalState(PUZZLESIZE); defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR); waitCursor = new Cursor(Cursor.WAIT_CURSOR); algChoice = new Choice(); for(int i = 0; i < algStrings.length; i++) algChoice.add(algStrings[i]); CheckboxGroup cbGroup = new CheckboxGroup(); cbEight = new Checkbox("8 tiles", cbGroup, true); cbEight.addItemListener(this); cbFifteen = new Checkbox("15 tiles", cbGroup, false); cbFifteen.addItemListener(this); Panel cbPanel = new Panel(); cbPanel.setLayout(new FlowLayout()); cbPanel.add(new Label("Puzzle configuration:", Label.RIGHT)); cbPanel.add(cbEight); cbPanel.add(cbFifteen); cbPanel.add(new Label("Heuristic:", Label.RIGHT)); cbPanel.add(algChoice); orderField = new TextField(37); orderField.addKeyListener(this); orderField.setBackground(Color.white); Panel orderPanel = new Panel(); orderPanel.setLayout(new FlowLayout()); orderPanel.add(new Label("Enter the order of the tiles:", Label.RIGHT)); orderPanel.add(orderField); solve = new Button(" Solve "); solve.addActionListener(this); reset = new Button(" Reset "); reset.addActionListener(this); show = new Button("Show Moves"); show.addActionListener(this); show.setEnabled(false); contPause = new Button(" Pause "); contPause.addActionListener(this); contPause.setEnabled(false); maxPathsField = new TextField(6); maxPathsField.setBackground(Color.white); Panel maxPathsPanel = new Panel(); maxPathsPanel.setLayout(new FlowLayout()); maxPathsPanel.add(new Label( "Number of paths to try (" + MINPATHS + "..." + MAXPATHS + "):", Label.RIGHT)); maxPathsPanel.add(maxPathsField); maxPathsPanel.add(solve); maxPathsPanel.add(reset); Panel userOptionsPanel = new Panel(); userOptionsPanel.setLayout(new GridLayout(3, 1)); userOptionsPanel.add(cbPanel); userOptionsPanel.add(orderPanel); userOptionsPanel.add(maxPathsPanel); Label title = new Label("The 8-Puzzle Solver", Label.CENTER); title.setFont(new Font("sansserif", Font.BOLD, 30)); Panel titlePanel = new Panel(); titlePanel.setLayout(new FlowLayout()); titlePanel.add(title); Panel topPanel = new Panel(); topPanel.setLayout(new BorderLayout()); topPanel.add("North", titlePanel); topPanel.add("South", new Box(userOptionsPanel, "User Input", Box.LEFT)); puzzle = new Puzzle(ps, PUZZLESIZE); Panel contPausePanel = new Panel(); contPausePanel.setLayout(new FlowLayout()); contPausePanel.add(contPause); Panel buttonPanel = new Panel(); buttonPanel.setLayout(new FlowLayout()); buttonPanel.add(show); buttonPanel.add(contPausePanel); puzPanel = new Panel(); puzPanel.setLayout(new FlowLayout()); puzPanel.add(new Label()); puzPanel.add(puzzle); puzPanel.add(new Label()); Panel puzzlePanel = new Panel(); puzzlePanel.setLayout(new BorderLayout()); puzzlePanel.add("North", puzPanel); puzzlePanel.add("South", buttonPanel); Panel puzzleButtonPanel = new Panel(); puzzleButtonPanel.setLayout(new BorderLayout()); puzzleButtonPanel.add("North", puzzlePanel); statusField = new TextField(27); statusField.setEditable(false); statusField.setBackground(Color.white); timeField = new TextField(8); timeField.setEditable(false); timeField.setBackground(Color.white); movesField = new TextField(2); movesField.setEditable(false); movesField.setBackground(Color.white); pathsField = new TextField(4); pathsField.setEditable(false); pathsField.setBackground(Color.white); expandedField = new TextField(4); expandedField.setEditable(false); expandedField.setBackground(Color.white); dirList = new List(7); dirList.setFont(new Font("Monospaced", Font.PLAIN, 12)); dirList.setBackground(Color.white); Panel infoPanel = new Panel(); infoPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(5, 0, 1, 10); gbc.anchor = GridBagConstraints.EAST; gbc.gridwidth = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; infoPanel.add(new Label("Status:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.fill = GridBagConstraints.HORIZONTAL; infoPanel.add(statusField, gbc); gbc.insets = new Insets(1, 0, 1, 10); gbc.anchor = GridBagConstraints.EAST; gbc.gridwidth = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; infoPanel.add(new Label("Elapsed time:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.fill = GridBagConstraints.HORIZONTAL; infoPanel.add(timeField, gbc); gbc.anchor = GridBagConstraints.EAST; gbc.gridwidth = 1; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE; infoPanel.add(new Label("Moves required:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.fill = GridBagConstraints.HORIZONTAL; infoPanel.add(movesField, gbc); gbc.anchor = GridBagConstraints.EAST; gbc.gridwidth = 1; gbc.weightx = 0; infoPanel.add(new Label("Paths visited:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.fill = GridBagConstraints.HORIZONTAL; infoPanel.add(pathsField, gbc); gbc.anchor = GridBagConstraints.EAST; gbc.gridwidth = 1; gbc.weightx = 0; infoPanel.add(new Label("Possibilities explored:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.fill = GridBagConstraints.HORIZONTAL; infoPanel.add(expandedField, gbc); gbc.insets = new Insets(1, 0, 10, 10); gbc.anchor = GridBagConstraints.NORTHEAST; gbc.gridwidth = 1; gbc.weightx = 0; infoPanel.add(new Label("Directions to move tiles:", Label.RIGHT), gbc); gbc.anchor = GridBagConstraints.CENTER; gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.gridheight = GridBagConstraints.REMAINDER; gbc.weightx = 100; gbc.weighty = 100; gbc.fill = GridBagConstraints.BOTH; infoPanel.add(dirList, gbc); Panel centerPanel = new Panel(); centerPanel.setLayout(new BorderLayout()); centerPanel.add("West", new Box(puzzleButtonPanel, "Puzzle", Box.LEFT)); centerPanel.add("Center", new Box(infoPanel, "Solution", Box.LEFT)); setLayout(new BorderLayout()); add("North", topPanel); add("Center", centerPanel); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { if (runsFromApplet) ps.close(); else System.exit(0); } }); validate(); pack(); setResizable(false); Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); Point p = new Point((int)((d.getWidth() - this.getWidth())/2), (int)((d.getHeight() - this.getHeight())/2)); setLocation(p); setVisible(true); } private boolean expandBFS(Node head, LinkedList open) { boolean done = false; Node left = head.moveLeft(), right = head.moveRight(), up = head.moveUp(), down = head.moveDown(); if (left != null) { expandCount++; if (left.isGoalState()) { done = true; bestNode = left; } else open.insertAtEnd(left); } if (right != null) { expandCount++; if (right.isGoalState()) { done = true; bestNode = right; } else open.insertAtEnd(right); } if (up != null) { expandCount++; if (up.isGoalState()) { done = true; bestNode = up; } else open.insertAtEnd(up); } if (down != null) { expandCount++; if (down.isGoalState()) { done = true; bestNode = down; } else open.insertAtEnd(down); } return done; } private LinkedList expandAStar(Node best, LinkedList closed) { LinkedList list = new LinkedList(); Node left = best.moveLeft(), right = best.moveRight(), up = best.moveUp(), down = best.moveDown(); if ((left != null) && !(closed.alreadyIncludes(left))) { list.insertAtFront(left); expandCount++; } if ((right != null) && !(closed.alreadyIncludes(right))) { list.insertAtFront(right); expandCount++; } if ((up != null) && !(closed.alreadyIncludes(up))) { list.insertAtFront(up); expandCount++; } if ((down != null) && !(closed.alreadyIncludes(down))) { list.insertAtFront(down); expandCount++; } return list; } private void expandEAStar(Node best, FibHeap open, AVLTree closed) { Node left = best.moveLeft(), right = best.moveRight(), up = best.moveUp(), down = best.moveDown(); if (left != null) { Node leftEl = closed.alreadyIncludes(left); boolean alreadyLeft = (leftEl != null); if ((!alreadyLeft) || ((alreadyLeft) && (left.getCost() < leftEl.getCost()))) { open.insert(left); expandCount++; } } if (right != null) { Node rightEl = closed.alreadyIncludes(right); boolean alreadyRight = (rightEl != null); if ((!alreadyRight) || ((alreadyRight) && (right.getCost() < rightEl.getCost()))) { open.insert(right); expandCount++; } } if (up != null) { Node upEl = closed.alreadyIncludes(up); boolean alreadyUp = (upEl != null); if ((!alreadyUp) || ((alreadyUp) && (up.getCost() < upEl.getCost()))) { open.insert(up); expandCount++; } } if (down != null) { Node downEl = closed.alreadyIncludes(down); boolean alreadyDown = (downEl != null); if ((!alreadyDown) || ((alreadyDown) && (down.getCost() < downEl.getCost()))) { open.insert(down); expandCount++; } } } private void initList(String path, Vector vector) { if (path.equals("")) dirList.add("Tiles already in order."); else { for(int i = 0; i < path.length(); i++) { Integer tileNum = (Integer)vector.elementAt(i); String pad = ""; if (i < 9) pad = " "; String s = pad + (i + 1) + ". Tile[" + tileNum.toString() + "] - "; if (path.charAt(i) == 'U') s += "down"; else if (path.charAt(i) == 'D') s += "up"; else if (path.charAt(i) == 'L') s += "right"; else s += "left"; dirList.add(s); } } } public void run() { /* If no answer, try to solve puzzle. Otherwise, puzzle has been solved. Go to else clause to show moves. */ currentThread = Thread.currentThread(); if (!hasAnswer) { puzzle.setOrder(initState); // Give thread some time to finish repainting image... try { runner.sleep(100); } catch(InterruptedException ie) { System.err.println("Error: " + ie); } setCursor(waitCursor); statusField.setText("Please wait. Thinking..."); movesField.setText(""); pathsField.setText(""); dirList.removeAll(); expandedField.setText(""); bestNode = new Node(PUZZLESIZE, initState); int numberVisited = 0; long startTime = System.currentTimeMillis(), time; if (algChoice.getSelectedIndex() == BFS) { LinkedList open = new LinkedList(); while ((runner == currentThread) && (numberVisited < pathsToTraverse)) { numberVisited++; time = System.currentTimeMillis(); if (numberVisited % 50 == 0) { timeField.setText((time - startTime)/1000f + " s"); if (numberVisited % 2000 == 0) { movesField.setText(Integer.toString(bestNode.getPath().length())); pathsField.setText(Integer.toString(numberVisited)); expandedField.setText(Integer.toString(expandCount)); } } if (bestNode.isGoalState()) { statusField.setText("Solution found."); break; } else if (!expandBFS(bestNode, open)) bestNode = open.removeHead(); } } else if (algChoice.getSelectedIndex() == AStar) { LinkedList open = new LinkedList(), closed = new LinkedList(); while ((runner == currentThread) && (numberVisited < pathsToTraverse)) { numberVisited++; time = System.currentTimeMillis(); if (numberVisited % 50 == 0) { timeField.setText((time - startTime)/1000f + " s"); if (numberVisited % 2000 == 0) { movesField.setText(bestNode.displayCost()); pathsField.setText(Integer.toString(numberVisited)); expandedField.setText(Integer.toString(expandCount)); } } if (bestNode.isGoalState()) { statusField.setText("Solution found."); break; } else open.merge(expandAStar(bestNode, closed)); closed.insertAtFront(bestNode); bestNode = open.removeHead(); } } else { FibHeap open = new FibHeap(); AVLTree closed = new AVLTree(); while ((runner == currentThread) && (numberVisited < pathsToTraverse)) { numberVisited++; time = System.currentTimeMillis(); if (numberVisited % 50 == 0) { timeField.setText((time - startTime)/1000f + " s"); if (numberVisited % 2000 == 0) { movesField.setText(bestNode.displayCost()); pathsField.setText(Integer.toString(numberVisited)); expandedField.setText(Integer.toString(expandCount)); } } if (bestNode.isGoalState()) { statusField.setText("Solution found."); break; } else expandEAStar(bestNode, open, closed); closed.insert(bestNode); bestNode = open.removeMin(); } } if (runner == currentThread) { time = System.currentTimeMillis(); timeField.setText((time - startTime)/1000f + " s"); if (bestNode == null) statusField.setText("There is no solution to this problem."); else if (numberVisited == pathsToTraverse) statusField.setText("No solution found in this test."); else { String path = bestNode.getPath(); Vector tileVector = Utility.makeTileVector(path, initState, PUZZLESIZE); initList(path, tileVector); if (bestNode.getCost() != 0) { hasAnswer = true; pathStr = bestNode.getPath(); show.setEnabled(true); } } movesField.setText(bestNode.displayCost()); pathsField.setText(Integer.toString(numberVisited)); expandedField.setText(Integer.toString(expandCount)); reset.setEnabled(true); setCursor(defaultCursor); puzzleStep = 0; stop(); } } else { while ((runner == currentThread) && (puzzleStep < pathStr.length())) { int p = Utility.posOfElement(0, initState, PUZZLESIZE); if (pathStr.charAt(puzzleStep) == 'U') Utility.swap(p, p - dim, initState); else if (pathStr.charAt(puzzleStep) == 'D') Utility.swap(p, p + dim, initState); else if (pathStr.charAt(puzzleStep) == 'L') Utility.swap(p, p - 1, initState); else Utility.swap(p, p + 1, initState); puzzle.setOrder(initState); dirList.select(puzzleStep); try { runner.sleep(1000); } catch(InterruptedException ie) { System.err.println("Error: " + ie); } puzzleStep += 1; } if (runner == currentThread) { dirList.deselect(pathStr.length() - 1); contPause.transferFocus(); reset.requestFocus(); contPause.setEnabled(false); stop(); } } } private int getArray() { boolean done = false; int i = 0, arrayPos = 0; initState = new int[PUZZLESIZE]; while (dataLine.chars[0] == ' ') dataLine.delete(0, 1); while (dataLine.chars[dataLine.length - 1] == ' ') dataLine.delete(dataLine.length - 1, 1); dataLine.addOn(' '); while ((error == NO_ERROR) && (i < dataLine.length)) { char ch = dataLine.chars[i]; if (!((ch >= '0') && (ch <= '9')) && (ch != '-') && (ch != ' ')) error = 1; else i += 1; } if (error == 1) { orderField.select(0, orderField.getText().length()); orderField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Cannot initialize puzzle: error in input.", "exclaim.gif"); } else { int lastP = 0; while ((!done) && (error == NO_ERROR)) { int p = dataLine.pos(' ', lastP); if (p > 0) { String temp = new String(dataLine.chars, lastP, p - lastP); if (arrayPos < PUZZLESIZE) try { initState[arrayPos] = Integer.parseInt(temp); } catch (NumberFormatException nfe) { error = INVALID_INT; } else error = EXCEEDS_MAX_TILES; lastP = p + 1; while ((lastP < dataLine.length) && (dataLine.chars[lastP] == ' ')) dataLine.delete(lastP, 1); arrayPos += 1; } else if (arrayPos == PUZZLESIZE) done = true; else error = NOT_ENOUGH_TILES; } if (error != NO_ERROR) { orderField.select(0, orderField.getText().length()); orderField.requestFocus(); if (error == INVALID_INT) { MessageBox mb = new MessageBox(ps, this, "Error", "Cannot initialize puzzle: error in input.", "exclaim.gif"); } else if (error == EXCEEDS_MAX_TILES) { MessageBox mb = new MessageBox(ps, this, "Error", "Input exceeds maximum of " + PUZZLESIZE + " tiles.", "exclaim.gif"); } else if (error == NOT_ENOUGH_TILES) { MessageBox mb = new MessageBox(ps, this, "Error", "Input contains only " + arrayPos + " of the " + PUZZLESIZE + " tiles.", "exclaim.gif"); } } } return arrayPos; } private int checkArray() { int missing = 0; for(int i = 0; i < PUZZLESIZE; i++) if (Utility.posOfElement(i, initState, PUZZLESIZE) == -1) { error = MISSING_SPECIFIC_TILE; missing = i; break; } return missing; } private void prepareToRun() { expandCount = 0; cbEight.setEnabled(false); cbFifteen.setEnabled(false); algChoice.setEnabled(false); orderField.setEnabled(false); maxPathsField.setEnabled(false); solve.transferFocus(); solve.setEnabled(false); } public void processCommand() { hasAnswer = false; error = NO_ERROR; String orderStr = orderField.getText().trim(); String maxPathsStr = maxPathsField.getText().trim(); orderField.setText(orderStr); maxPathsField.setText(maxPathsStr); if (orderStr.equals("")) { orderField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Cannot solve puzzle: no tile order entered.", "exclaim.gif"); error = RETRIEVE_ERROR; } else if (maxPathsStr.equals("")) { maxPathsField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Cannot solve puzzle: number of paths not entered.", "exclaim.gif"); error = RETRIEVE_ERROR; } else { try { pathsToTraverse = Integer.parseInt(maxPathsStr); if ((pathsToTraverse < MINPATHS) || (pathsToTraverse > MAXPATHS)) { maxPathsField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Cannot solve puzzle: path value out of bounds.", "exclaim.gif"); error = RETRIEVE_ERROR; } else { dataLine = new CharArray(orderStr); numOfElements = getArray(); if (error == NO_ERROR) { int missing = checkArray(); if (error == MISSING_SPECIFIC_TILE) { orderField.selectAll(); orderField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Tile " + missing + " is missing from the input.", "exclaim.gif"); } else { orderField.setText(dataLine.toString()); if (Utility.isValidPermutation(initState, goalState)) { prepareToRun(); start(); } else { orderField.selectAll(); orderField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Information", "This puzzle configuration is unsolvable.", "information.gif"); } } } } } catch (NumberFormatException nfe) { maxPathsField.requestFocus(); MessageBox mb = new MessageBox(ps, this, "Error", "Cannot solve puzzle: invalid path value entered.", "exclaim.gif"); error = RETRIEVE_ERROR; } } } public void start() { stop(); if (runner == null) { runner = new Thread(this); runner.start(); } } public void stop() { if (runner != null) { runner = null; } } public void actionPerformed(ActionEvent ae) { if (ae.getSource() == solve) processCommand(); else if (ae.getSource() == show) { contPause.setEnabled(true); contPause.requestFocus(); show.setEnabled(false); start(); } else if (ae.getSource() == reset) { stop(); puzzle.putInOrder(); cbEight.setEnabled(true); cbFifteen.setEnabled(true); algChoice.setEnabled(true); orderField.setEnabled(true); maxPathsField.setEnabled(true); solve.setEnabled(true); pathStr = ""; orderField.select(0, orderField.getText().length()); solve.setEnabled(true); statusField.setText(""); timeField.setText(""); movesField.setText(""); pathsField.setText(""); expandedField.setText(""); dirList.removeAll(); contPause.setLabel("Pause"); contPause.setEnabled(false); show.setEnabled(false); setCursor(defaultCursor); orderField.requestFocus(); } else if (ae.getSource() == contPause) if (contPause.getLabel().equals("Continue")) { contPause.setLabel("Pause"); start(); } else { contPause.setLabel("Continue"); stop(); } } public void itemStateChanged(ItemEvent ie) { setCursor(waitCursor); stop(); if (cbEight.getState()) PUZZLESIZE = 9; else PUZZLESIZE = 16; dim = (int)Math.sqrt(PUZZLESIZE); goalState = Utility.makeGoalState(PUZZLESIZE); puzzle = new Puzzle(ps, PUZZLESIZE); puzPanel.removeAll(); puzPanel.add(new Label()); puzPanel.add(puzzle); puzPanel.add(new Label()); puzPanel.validate(); setCursor(defaultCursor); } public void keyPressed(KeyEvent ke) { if (ke.getKeyText(ke.getKeyCode()).equals("Tab")) orderField.select(0, 0); } public void keyReleased(KeyEvent ke){} public void keyTyped(KeyEvent ke){} public static void main(String[] args) { PuzzleApp pa = new PuzzleApp(null, false); } }