Java: Example - Five-in-a-row

Two players alternate making moves. The player who places five pieces in a row wins. This particular implementation starts with black already having made a move in the center position.

The source program consists of two source files: a graphical user interface and main program, and a logic class (the "model"). The model knows nothing about the GUI, and could just as easily be used with a text interface or by a web interface.

There's a lot of room for improvement. See the comments at the beginning of the first file, as well as at the end of this page, for suggested enhancements.

The applet, main program, and GUI

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 
127 
128 
129 
130 
131 
132 
133 
134 
135 
136 
137 
138 
139 
140 
141 
142 
143 
144 
145 
146 
147 
148 
149 
150 
151 
152 
153 
154 
155 
156 
157 
158 
159 
160 
161 
162 
163 
164 
165 
166 
167 
168 
169 
170 
171 
172 
173 
174 
175 
176 
177 
178 
179 
180 
181 
182 
183 
184 
185 
// File   : GUI/fiveinarow/Five.java
// Purpose: A Graphical User Interface for a Five-In-A-Row game.
//          This class implements the user interface (view and controller),
//          and the logic (model) is implemented in a separate class that
//          knows nothing about the user interface.
// Applet tag: <applet code="fiveinarow.Five.class" 
//                     archive="fiveinarow.jar" width="270" height="326"></applet>
// Enhancements: 
//          * The Undo button doesn't do anything.  Fix it in GUI and model.
//          * Use the Observer pattern to have the model notify any
//            interested parties when there is a change in the model.
//          * Clean up the status value.  Perhaps with an enum.
//          * Implement a machine player.  This isn't exactly trivial, but
//            a good start on that could be made by counting the number of
//            potential 5-in-a-rows that could be made with more weight give
//            to those that are nearer completion.
// Author : Fred Swartz - 2006-12-01 - Placed in public domain.

package fiveinarow;

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

///////////////////////////////////////////////////////////////////// class Five
public class Five extends JApplet {
    //================================================================ constants
    private static final int ROWS = 9;
    private static final int COLS = 9;
    private static final Color[]  PLAYER_COLOR = {null, Color.BLACK, Color.WHITE};
    private static final String[] PLAYER_NAME  = {null, "BLACK", "WHITE"};
    
    //======================================================= instance variables
    private GameBoard  _boardDisplay;
    private JTextField _statusField = new JTextField();
    private FiveModel  _gameLogic   = new FiveModel(ROWS, COLS);
    
    //============================================================== method main
    // If used as an applet, main will never be called.
    // If used as an application, this main will be called and it will use 
    //         the applet for the content pane.
    public static void main(String[] args) {
        JFrame window = new JFrame("Five in a Row");
        window.setContentPane(new Five());  // Make applet content pane.
        window.pack();                      // Do layout
        //System.out.println(window.getContentPane().getSize());
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setLocationRelativeTo(null); // Center window.
        window.setResizable(false);
        window.setVisible(true);            // Make window visible
    }
    
    //============================================================== constructor
    public Five() {
        //--- Create some buttons
        JButton newGameButton = new JButton("New Game");
        JButton undoButton    = new JButton("Undo");
        
        //--- Create control panel
        JPanel controlPanel = new JPanel();
        controlPanel.setLayout(new FlowLayout());
        controlPanel.add(newGameButton);
        controlPanel.add(undoButton);
        
        //--- Create component to display board.
        _boardDisplay = new GameBoard();
        
        //--- Set the layout and add the components
        this.setLayout(new BorderLayout());
        this.add(controlPanel , BorderLayout.NORTH);
        this.add(_boardDisplay, BorderLayout.CENTER);
        this.add(_statusField , BorderLayout.SOUTH);
        
        //-- Add action listener to New Game button.
        newGameButton.addActionListener(new NewGameAction());
    }
    
    ////////////////////////////////////////////////// inner class NewGameAction
    private class NewGameAction implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            _gameLogic.reset();
            _boardDisplay.repaint();
        }
    }
    
    ////////////////////////////////////////////////////// inner class GameBoard
    // This is defined inside outer class to use things from the outer class:
    //    * The logic (could be passed to the constructor).
    //    * The number of rows and cols (could be passed to constructor).
    //    * The status field - shouldn't really be managed here.
    //      See note on using Observer pattern in the model.
    class GameBoard extends JComponent implements MouseListener {
        //============================================================ constants
        private static final int CELL_SIZE = 30; // Pixels
        private static final int WIDTH  = COLS * CELL_SIZE;
        private static final int HEIGHT = ROWS * CELL_SIZE;
        
        //========================================================== constructor
        public GameBoard() {
            this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
            this.addMouseListener(this);  // Listen to own mouse events.
        }
        
        //======================================================= paintComponent
        @Override public void paintComponent(Graphics g) {
            Graphics2D g2 = (Graphics2D)g;
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            
            //... Paint background
            g2.setColor(Color.LIGHT_GRAY);
            g2.fillRect(0, 0, WIDTH, HEIGHT);
            
            //... Paint grid (could be done once and saved).
            g2.setColor(Color.BLACK);
            for (int r=1; r<ROWS; r++) {  // Horizontal lines
                g2.drawLine(0, r*CELL_SIZE, WIDTH, r*CELL_SIZE);
            }
            for (int c=1; c<COLS; c++) {
                g2.drawLine(c*CELL_SIZE, 0, c*CELL_SIZE, HEIGHT);
            }
            
            //... Draw player pieces.
            for (int r = 0; r < ROWS; r++) {
                for (int c = 0; c < COLS; c++) {
                    int x = c * CELL_SIZE;
                    int y = r * CELL_SIZE;
                    int who = _gameLogic.getPlayerAt(r, c);
                    if (who != _gameLogic.EMPTY) {
                        g2.setColor(PLAYER_COLOR[who]);
                        g2.fillOval(x+2, y+2, CELL_SIZE-4, CELL_SIZE-4);
                    }
                }
            }
        }
        
        //================================================ listener mousePressed
        // When the mouse is pressed (would released be better?),
        //       the coordinates are translanted into a row and column.
        // Setting the status field in here isn't really the right idea.
        //       Instead the model should notify those who have registered.
        public void mousePressed(MouseEvent e) {
            int col = e.getX() / CELL_SIZE;
            int row = e.getY() / CELL_SIZE;
            
            boolean gameOver = _gameLogic.getGameStatus() != 0;
            int currentOccupant = _gameLogic.getPlayerAt(row, col);
            if (!gameOver && currentOccupant == _gameLogic.EMPTY) {
                //... Make a move.
                _gameLogic.move(row, col);
                
                //... Report what happened in status field.
                switch (_gameLogic.getGameStatus()) {
                    case 1:
                        //... Player one wins.  Game over.
                        _statusField.setText("BLACK WINS");
                        break;
                    case 2:
                        //... Player two wins.  Game over.
                        _statusField.setText("WHITE WINS");
                        break;
                        
                    case FiveModel.TIE:  // Tie game.  Game over.
                        _statusField.setText("TIE GAME");
                        break;
                        
                    default:
                        _statusField.setText(PLAYER_NAME[_gameLogic.getNextPlayer()]
                                + " to play");
                }
                
            } else {  // Not legal - clicked non-empty location or game over.
                Toolkit.getDefaultToolkit().beep();
            }
            
            this.repaint();  // Show updated board
        }
        
        //================================================== ignore these events
        public void mouseClicked(MouseEvent e) {}
        public void mouseReleased(MouseEvent e) {}
        public void mouseEntered(MouseEvent e) {}
        public void mouseExited(MouseEvent e) {}
    }    
}

The Model

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 
124 
125 
126 
127 
128 
129 
130 
131 
132 
// File   : GUI/fiveinarow/FiveModel.java
// Purpose: Implements the logic (model) for the game of Five-In-A-Row.
// Author : Fred Swartz - December 1, 2006 - Placed in public domain.

package fiveinarow;

////////////////////////////////////////////////////////////////////// FiveModel
class FiveModel {
    
    //================================================================ constants
    public  static final int EMPTY   = 0;  // The cell is empty.
    private static final int PLAYER1 = 1;
    public  static final int TIE     = -1; // Game is a tie (draw).
    
    //=================================================================== fields
    private int     _maxRows;    // Number of rows. Set in constructor.
    private int     _maxCols;    // Number of columns.  Set in constructor.
    
    private int[][] _board;      // The board values.
    private int     _nextPlayer; // The player who moves next.
    private int     _moves = 0;  // Number of moves in the game.
    
    //============================================================== constructor
    public FiveModel(int rows, int cols) {
        _maxRows = rows;
        _maxCols = cols;
        _board = new int[_maxRows][_maxCols];
        reset();
    }
    
    //============================================================ getNextPlayer
    /** Returns the next player. */
    public int getNextPlayer() {
        return _nextPlayer;
    }
    
    //============================================================== getPlayerAt
    /** Returns player who has played at particular row and column. */
    public int getPlayerAt(int r, int c) {
        return _board[r][c];
    }
    
    //==================================================================== reset
    /** Clears board to initial state. Makes first move in center. */
    public void reset() {
        for (int r = 0; r < _maxRows; r++) {
            for (int c = 0; c < _maxCols; c++) {
                _board[r][c] = EMPTY;
            }
        }
        _moves = 0;  // No moves so far.
        _nextPlayer = PLAYER1;  
        //-- Make first move in center.
        move(_maxCols/2, _maxRows/2);  // First player moves to center
    }
    
    //===================================================================== move
    /** Play a marker on the board, record it, flip players. */
    public void move(int r, int c) {
        assert _board[r][c] == EMPTY;
        _board[r][c] = _nextPlayer;  // Record this move.
        _nextPlayer = 3-_nextPlayer; // Flip players
        _moves++;                    // Increment number of moves.
    }
    
    //=================================================== utility method _count5
    /**
     * The _count5 utility function returns true if there are five in
     *        a row starting at the specified r,c position and 
     *        continuing in the dr direction (+1, -1) and
     *        similarly for the column c.
     */
    private boolean _count5(int r, int dr, int c, int dc) {
        int player = _board[r][c];  // remember the player.
        for (int i = 1; i < 5; i++) {
            if (_board[r+dr*i][c+dc*i] != player) return false;
        }
        return true;  // There were 5 in a row!
    }

    //============================================================ getGameStatus
    /** -1 = game is tie, 
         0 = more to play, 
         1 = player1 wins,
         2 = player2 wins 
         What I don't like about this is mixing a couple of logical
         types: player number, empty board, and game status. */
    public int getGameStatus() {
        int row;
        int col;
        int n_up, n_right, n_up_right, n_up_left;

        boolean at_least_one_move;   // true if game isn't a tie

        for (row = 0; row < _maxRows; row++) {
            for (col = 0; col < _maxCols; col++) {
                int p = _board[row][col];
                if (p != EMPTY) {
                    // look at 4 kinds of rows of 5
                    //  1. a column going up
                    //  2. a row going to the right
                    //  3. a diagonal up and to the right
                    //  4. a diagonal up and to the left
    
                    if (row < _maxRows-4) // Look up
                        if (_count5(row, 1, col, 0)) return p;
    
                    if (col < _maxCols-4) { // row to right
                        if (_count5(row, 0, col, 1))  return p;
    
                        if (row < _maxRows-4) { // diagonal up to right
                            if (_count5(row, 1, col, 1)) return p;
                        }
                    }
    
                    if (col > 3 && row < _maxRows-4) { // diagonal up left
                        if (_count5(row, 1, col, -1)) return p;
                    }
                }//endif position wasn't empty
            }//endfor row
        }//endfor col

        //... Neither player has won, it's tie if there are empty positions.
        //    Game is finished if total moves equals number of positions.
        if (_moves == _maxRows*_maxCols) {
            return TIE; // Game tied.  No more possible moves.
        } else {
            return 0;  // More to play.
        }
    }

}

Exercises

  1. Make the Undo button on the Five-in-a-row program work. That means adding a button listener in the FiveGUI class, and adding an undo() method in the FiveModel class. The way undo generally works is by keeping a history of what was done in a LIFO (Last In, First Out) manner, implemented as an array (bounded number of undos) or ArrayList (unbounded). When an Undo is requested, the last thing is popped of the end of the history and "undone". That's pretty simple here -- whenever someone calls move(), just add that move to the end of a history array. Whe undo() is called, take the last element off the end of the history array and clear that position from the board.
  2. Observer pattern. Implement the observer pattern where the model keeps a list of observers to call whenever something happens in the model (eg, a move, an undo, or a reset). This might be easiest to do by using the predefined ChangeListener interface, in which the model would define an addChangeListener() method, and would call the stateChanged() method of each listener.