import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import javax.swing.text.html.*; /** * This is a modified version of the TicTacToe applet shipped with * Java 2 Software Development Kit. Instead of drawing the board and * pieces by way of overriding paint, this uses * JEditorPane with an html document. The html document is updated via * the HTMLDocument method setOuterHTML. */ public class TicTacToe extends MouseAdapter { private static final String computerHTML = ""; private static final String emptyMoveHTML = ""; private static final String userHTML = ""; private static final String yourMoveHTML = "Status: Your Move"; private static final String wonHTML = "Status: I WON!"; private static final String lostHTML = "Status: You WON!"; private static final String stalemateHTML = "Status: Stalemate"; /** * The squares in order of importance... */ private static final int moves[] = {4, 0, 2, 6, 8, 1, 3, 5, 7}; /** * The winning positions. */ private static boolean won[] = new boolean[1 << 9]; private static final int DONE = (1 << 9) - 1; private static final int OK = 0; private static final int WIN = 1; private static final int LOSE = 2; private static final int STALEMATE = 3; private int white; private int black; private JEditorPane jep; private HTMLDocument htmlDoc; private JFrame frame; /** * Mark all positions with these bits set as winning. */ static void isWon(int pos) { for (int i = 0 ; i < DONE ; i++) { if ((i & pos) == pos) { won[i] = true; } } } /** * Initialize all winning positions. */ static { isWon((1 << 0) | (1 << 1) | (1 << 2)); isWon((1 << 3) | (1 << 4) | (1 << 5)); isWon((1 << 6) | (1 << 7) | (1 << 8)); isWon((1 << 0) | (1 << 3) | (1 << 6)); isWon((1 << 1) | (1 << 4) | (1 << 7)); isWon((1 << 2) | (1 << 5) | (1 << 8)); isWon((1 << 0) | (1 << 4) | (1 << 8)); isWon((1 << 2) | (1 << 4) | (1 << 6)); } public TicTacToe() { new ImageIcon("circle2.gif"); new ImageIcon("cross2.gif"); frame = new JFrame("TicTacToe II"); jep = new JEditorPane(); jep.setEditable(false); jep.setEditorKit(new TicTacToeEditorKit()); try { jep.setPage(getClass().getResource("TicTacToe.html")); } catch (java.io.IOException ioe) {} htmlDoc = (HTMLDocument)jep.getDocument(); frame.getContentPane().add(new JScrollPane(jep)); jep.addMouseListener(this); frame.pack(); frame.setSize(new Dimension(600, 800)); frame.show(); } /** * Compute the best move for white. * @return the square to take */ private int bestMove(int white, int black) { int bestmove = -1; loop: for (int i = 0 ; i < 9 ; i++) { int mw = moves[i]; if (((white & (1 << mw)) == 0) && ((black & (1 << mw)) == 0)) { int pw = white | (1 << mw); if (won[pw]) { // white wins, take it! return mw; } for (int mb = 0 ; mb < 9 ; mb++) { if (((pw & (1 << mb)) == 0) && ((black & (1 << mb)) == 0)) { int pb = black | (1 << mb); if (won[pb]) { // black wins, take another continue loop; } } } // Neither white nor black can win in one move, this will do. if (bestmove == -1) { bestmove = mw; } } } if (bestmove != -1) { return bestmove; } // No move is totally satisfactory, try the first one that is open for (int i = 0 ; i < 9 ; i++) { int mw = moves[i]; if (((white & (1 << mw)) == 0) && ((black & (1 << mw)) == 0)) { return mw; } } // No more moves return -1; } /** * User move. This will invoke doYourMove to handle * updating the board if the move is legal. * * @return true if legal */ private boolean yourMove(int m) { if ((m < 0) || (m > 8)) { return false; } if (((black | white) & (1 << m)) != 0) { return false; } doYourMove(m); return true; } /** * Computer move, this will invoke doMyMove if * the move is legal. * * @return true if legal */ private boolean myMove() { if ((black | white) == DONE) { return false; } int best = bestMove(white, black); doMyMove(best); return true; } /** * Figure what the status of the game is. */ private int status() { if (won[white]) { return WIN; } if (won[black]) { return LOSE; } if ((black | white) == DONE) { return STALEMATE; } return OK; } /** * Resets the board to an empty state. */ public void newGame() { for (int counter = 0; counter < 9; counter++) { toggleBoardElementHTML(getBoardElement(counter), emptyMoveHTML); } toggleStatus(yourMoveHTML); black = white = 0; } /** * Invoked when the mouse is released. This will attempt to create a new * move for the user based on where the mouse is released. */ public void mouseReleased(MouseEvent me) { if (me.getClickCount() == 1) { int position; if (status() != OK || (position = getBoardPosition(me.getX(), me.getY())) == -1 || !yourMove(position)) { Toolkit.getDefaultToolkit().beep(); } } } /** * Invoked to do a move for the user. The html document is updated * as a result of the user moving to position position. * If no one has won, myMove is invoked to handle the * computers choice. */ private void doYourMove(int position) { black |= 1 << position; // Update the position Element e = getBoardElement(position); toggleBoardElementHTML(e, userHTML); // Check the current status switch(status()) { case WIN: won(); break; case LOSE: lost(); break; case STALEMATE: statelmate(); break; default: if (myMove()) { // We choose a spot, see if the game has ended. switch (status()) { case WIN: won(); break; case LOSE: lost(); break; case STALEMATE: statelmate(); break; default: break; } } break; } } /** * Updates the board in response to the computer choosing position * position. */ private void doMyMove(int position) { Element e = getBoardElement(position); toggleBoardElementHTML(e, computerHTML); white |= 1 << position; } /** * Invoked when the computer wins. */ private void won() { toggleStatus(wonHTML); } /** * Inovked when the computer loses. */ private void lost() { toggleStatus(lostHTML); } /** * Invoked when the game has resulted in a stalemate. */ private void statelmate() { toggleStatus(stalemateHTML); } /** * Togglets the status of game to show the passed in html string. */ private void toggleStatus(String newHTML) { Element e = htmlDoc.getElement("status"); try { htmlDoc.setInnerHTML(e, newHTML); } catch (BadLocationException ble) { System.out.println("BLE: " + ble); } catch (IOException ioe) { System.out.println("IOE: " + ioe); } } /** * Returns the element representing position position. */ private Element getBoardElement(int position) { return htmlDoc.getElement(Integer.toString(position)); } /** * Resets the html contents of the Element e to * the html string html. */ private void toggleBoardElementHTML(Element e, String html) { try { Object id = e.getAttributes().getAttribute(HTML.Attribute.ID); int insertIndex = html.indexOf(">"); html = html.substring(0, insertIndex) + " id=" + id + html.substring(insertIndex); htmlDoc.setOuterHTML(e, html); } catch (BadLocationException ble) { System.out.println("BLE: " + ble); } catch (java.io.IOException ioe) { System.out.println("IOE: " + ioe); } } /** * Returns the board position for the element at location x, * y. This will return -1 if the passed in location does not * represent a spot on the board. */ private int getBoardPosition(int x, int y) { // Determine the offset for the passed in x, y location Position.Bias bias[] = new Position.Bias[1]; int offset = jep.getUI().viewToModel(jep, new Point(x, y), bias); // A backward bias typically (at least in normal left to right text) // indicates an end of line condition. The passed in point was // beyond the visible region of the line. In which case the backward // bias indicates the location is at the end offset of the character // element. Since we will be using getCharacterElement followed by // a check of the bounds we subtract one from the offset so that // getCharacterElement returns the Element representing the end of // line and NOT the next line. if (offset > 0 && bias[0] == Position.Bias.Backward) { offset--; } // Get the character Element at that location, and find the // corresponding TD cell. Element e = htmlDoc.getCharacterElement(offset); while (e != null && e.getAttributes().getAttribute (StyleConstants.NameAttribute) != HTML.Tag.TD) { e = e.getParentElement(); } if (e != null) { // Check that the location is really inside the table cell. Rectangle bounds; try { bounds = jep.getUI().modelToView(jep, e.getStartOffset(), Position.Bias.Forward); bounds = bounds.union(jep.getUI().modelToView (jep, e.getEndOffset(), Position.Bias.Backward)); if (bounds.contains(x, y)) { // found it Object boardLocation = e.getAttributes().getAttribute (HTML.Attribute.ID); if (boardLocation != null) { try { return Integer.parseInt((String)boardLocation); } catch (NumberFormatException nfe) { } } } } catch (BadLocationException ble) { } } return -1; } /** * A subclass of HTMLEditorKit that returns a different ViewFactory. */ private class TicTacToeEditorKit extends HTMLEditorKit { public ViewFactory getViewFactory() { return new TicTacToeFactory(); } } /** * A subclass of the HTMLFactory that will create a * TicTacToeFormView for INPUT Elements. */ private class TicTacToeFactory extends HTMLEditorKit.HTMLFactory { public View create(Element e) { Object o = e.getAttributes().getAttribute (StyleConstants.NameAttribute); if (o == HTML.Tag.INPUT) { return new TicTacToeFormView(e); } return super.create(e); } } /** * A subclass of FormView that invokes newGame * when the action is performed (the user clicks on the button). */ private class TicTacToeFormView extends FormView { TicTacToeFormView(Element e) { super(e); } public void actionPerformed(ActionEvent ae) { newGame(); } } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new TicTacToe(); } }); } }