2
\$\begingroup\$

After my previous post, I decided to work on the project myself for a while (two weeks) to see how far I could get after reading a few of the really helpful answers to my previous posts. I read up on stuffs to learn a little more about the stuffs mentioned in the answers (MVC, interfaces, packages, etc) and read a lot of code on GitHub. I made it so the program sets actually X and O icons as well.

This is my last code review question for this project (since I don't want to spam?), and I'd really appreciate some final thoughts on my classes.

Java MVC TicTacToe - follow-up

Java Tic-Tac-Toe game (implemented through MVC)

Questions:

  1. Did I implement the majority of the advice correctly?
  2. Is there anything I did that is just downright wrong / nasty / not recommended? Sub-questions: Are my comments / documentation fine? I want to get better at writing clear code AND documentation. I want to make sure that I can write small projects with decent-good code quality (otherwise, how would I be able to write readable and maintainable code in bigger projects - is my thought process).
  3. Is there anything I could do that could improve the structure of the code?

ViewInterface.Java

package chautnguyen.com.github.tictactoe.view;

import chautnguyen.com.github.tictactoe.model.Field.Symbol;

import javax.swing.JButton;

interface ViewInterface
{
    /**
     * Changes a field to a user symbol.
     * 
     * @param symbol    the symbol of the current player.
     * @param button    the button that was clicked.
     */
    public void setFieldOwner(Symbol owner, JButton button);

    /**
     * Informs the user who won.
     * 
     * @param symbol    the symbol of the current player (=> winner).
     */
    public void informWin(Symbol userSymbol);

    /**
     * Informs the user of the tie.
     */
    public void informTie();
}

View.java

package chautnguyen.com.github.tictactoe.view;

import chautnguyen.com.github.tictactoe.model.Field.Symbol;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Image;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import javax.swing.JOptionPane;

public class View extends JFrame implements ViewInterface {
    private GridLayout grid = new GridLayout(3, 3);     // default grid-size for tic-tac-toe
    private JButton[] buttons;                  // an array containing the 9 buttons

    /**
     * Overloaded constructor.
     */
    public View() {
        super("tic-tac-toe");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        addComponentsToPane(getContentPane());
        pack();
        setVisible(true);

        getRootPane().setDefaultButton(buttons[4]);
        buttons[4].requestFocus();
    }

    /**
     * Adds the panel along with its buttons to the pane.
     */
    public void addComponentsToPane(final Container pane) {
        final JPanel panel = new JPanel();
        panel.setLayout(grid);        
        panel.setPreferredSize(new Dimension(300, 300));

        buttons = new JButton[9];

        for (int i = 0; i < buttons.length; i++) {
            buttons[i] = new JButton();
            buttons[i].getPreferredSize();
            panel.add(buttons[i]);
        }

        pane.add(panel);
    }

    @Override
    /**
     * Changes a field to a user symbol.
     * 
     * @param symbol    the symbol of the current player.
     * @param button    the button that was clicked.
     */
    public void setFieldOwner(Symbol userSymbol, JButton button) {
        if (userSymbol.toString() == "X") {
            try {
                Image icon = ImageIO.read(View.class.getResource("icons/X.png"));
                button.setIcon(new ImageIcon(icon));
            } catch (IOException ex) {
                System.out.println("icons/X.png not found.");
            }
        } else {
            try {
                Image icon = ImageIO.read(View.class.getResource("icons/O.png"));
                button.setIcon(new ImageIcon(icon));
            } catch (IOException ex) {
                System.out.println("icons/O.png not found.");
            }
        }
        button.setEnabled(false);
    }

    /**
     * Informs the user who won.
     * 
     * @param symbol    the symbol of the current player (=> winner).
     */
    @Override
    public void informWin(Symbol userSymbol) {
        for (int i = 0; i < buttons.length; i++) {
            buttons[i].setEnabled(false);
        }
        JOptionPane.showMessageDialog(null, "Player " + userSymbol.toString() + " has won!");
    }

    /**
     * Informs the user of the tie.
     */
    @Override
    public void informTie() {
        JOptionPane.showMessageDialog(null, "Tie!");
    }

    /**
     * Returns a button with a specific index.
     * 
     * @return a button with a specific index.
     */
    public JButton getButton(int index) {
        return buttons[index];
    }

    /**
     * Returns the size of the buttons[] array.
     * 
     * @return the size of the buttons[] array.
     */
    public int getNumberOfButtons() {
        return buttons.length;
    }
}

Field.java

package chautnguyen.com.github.tictactoe.model;

/**
 * One field of the game grid.
 */
public class Field {
    private Symbol owner;

    /**
     * Initializes the Field object with a Symbol.
     * 
     * @param symbol    a Symbol (X, O, or NONE)
     */
    private Field(Symbol symbol) {
        owner = symbol;
    }

    /**
     * Static factory, returns a Field object with Symbol.NONE.
     */
    public static Field getDefault() {
        return new Field(Symbol.NONE);
    }

    /**
     * Returns the owner of a field as a Symbol.
     * 
     * @return      the owner of a field as a Symbol.
     */
    public Symbol getOwner() {
        return owner;
    }

    /**
     * Sets the owner of a field.
     * 
     * @param owner     a Symbol (X, O, or NONE)
     */
    public void setOwner(Symbol owner) {
        this.owner = owner;
    }

    /**
     * Returns the Symbol (X, O, or NONE) as a String.
     * 
     * @return      the owner of a field as a String.
     */
    @Override
    public String toString() {
        return owner.toString();
    }

    /**
     * A representation of an owner of a field.
     */
    public enum Symbol {
        X, O, NONE
    }
}

Board.java

package chautnguyen.com.github.tictactoe.model;

import chautnguyen.com.github.tictactoe.model.Field;
import chautnguyen.com.github.tictactoe.model.Field.Symbol;

public class Board {
    private Field[][] gameGrid;
    private static final int BOARD_SIDE_LENGTH = 3;

    public Board() {
        gameGrid = new Field[BOARD_SIDE_LENGTH][BOARD_SIDE_LENGTH];

        // initializes the board with Symbol.None
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            for (int j = 0; j < BOARD_SIDE_LENGTH; j++) {
                gameGrid[i][j] = Field.getDefault();
            }
        }
    }

    /**
     * Evaluates the board, stores the values into an array, and returns it.
     * 
     * @return      an array containing the score of each possible win condition.
     */
    public int[] evaluateBoard() {
        int[] scores = new int[8];

        // evaluate the first row
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[0][i].getOwner() == Symbol.X) {
                scores[0]++;
            }
            if (gameGrid[0][i].getOwner() == Symbol.O) {
                scores[0]--;
            }
        }

        // evaluate the second row
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[1][i].getOwner() == Symbol.X) {
                scores[1]++;
            }
            if (gameGrid[1][i].getOwner() == Symbol.O) {
                scores[1]--;
            }
        }

        // evaluate the third row
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[2][i].getOwner() == Symbol.X) {
                scores[2]++;
            }
            if (gameGrid[2][i].getOwner() == Symbol.O) {
                scores[2]--;
            }
        }

        // evaluate the first column
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[i][0].getOwner() == Symbol.X) {
                scores[3]++;
            }
            if (gameGrid[i][0].getOwner() == Symbol.O) {
                scores[3]--;
            }
        }

        // evaluate the second column
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[i][1].getOwner() == Symbol.X) {
                scores[4]++;
            }
            if (gameGrid[i][1].getOwner() == Symbol.O) {
                scores[4]--;
            }
        }

        // evaluate the third column
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[i][2].getOwner() == Symbol.X) {
                scores[5]++;
            }
            if (gameGrid[i][2].getOwner() == Symbol.O) {
                scores[5]--;
            }
        }

        // evaluate the left-to-right diagonal
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            if (gameGrid[i][i].getOwner() == Symbol.X) {
                scores[6]++;
            }
            if (gameGrid[i][i].getOwner() == Symbol.O) {
                scores[6]--;
            }
        }

        // evaluate the right-to-left diagonal
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) { // rows
            for (int j = 0; j < BOARD_SIDE_LENGTH; j++) { // columns
                if (i + j == 2) { // right diagonal contain x's and y's that add up to 2
                    if (gameGrid[i][j].getOwner() == Symbol.X) {
                        scores[7]++;
                    }
                    if (gameGrid[i][j].getOwner() == Symbol.O) {
                        scores[7]--;
                    }
                }
            }
        }

        return scores;
    }

    /**
     * Sets the symbol of a specific field.
     * 
     * @param x     the value for the x-coordinate of the Field
     * @param y     the value for the y-coordinate of the Field
     */
    public void setFieldOwner(Symbol owner, int x, int y) {
        gameGrid[x][y].setOwner(owner);
    }

    /**
     * Returns the owner of a specific field.
     * 
     * @param x     the value for the x-coordinate of the Field
     * @param y     the value for the y-coordinate of the Field
     */
    public Symbol getFieldOwner(int x, int y) {
        return gameGrid[x][y].getOwner();
    }

    /**
     * Prints the field.
     * Note: I'm using this just for testing.
     */
    public void printField() {
        for (int i = 0; i < BOARD_SIDE_LENGTH; i++) {
            for (int j = 0; j < BOARD_SIDE_LENGTH; j++) {
                System.out.print(gameGrid[i][j].getOwner() + " ");
            }
            System.out.println();
        }
    }
}

Game.java

package chautnguyen.com.github.tictactoe.model;

import chautnguyen.com.github.tictactoe.model.Board;
import chautnguyen.com.github.tictactoe.model.Field.Symbol;

/**
 * The logic of the game.
 */
public class Game {
    private Board board;

    private int turnsCounter; // the number of turns since the start of the game
    private Symbol userSymbol; // the current Symbol of the player
    private boolean didSomeoneWin; // to check if a player won or if it was a tie

    /**
     * Default constructor.
     * 
     * Initializes the gameGrid array with 9 Field objects and the other
     * to their appropriate values.
     */
    public Game() {
        board = new Board();

        turnsCounter = 0;
        userSymbol = Symbol.NONE;
        didSomeoneWin = false;
    }

    /**
     * Sets the symbol of a specific field.
     * 
     * @param x     the value for the x-coordinate of the Field
     * @param y     the value for the y-coordinate of the Field
     */
    public void setFieldOwner(Symbol userSymbol, int x, int y) {
        board.setFieldOwner(userSymbol, x, y);
    }

    /**
     * Returns the owner of a specific field.
     * Note: I used this just for testing.
     * 
     * @param x     the value for the x-coordinate of the Field
     * @param y     the value for the y-coordinate of the Field
     * @return      the owner of the Field as a Symbol
     */
    public Symbol getFieldOwner(int x, int y) {
        return board.getFieldOwner(x, y);
    }

    /**
     * Prints the field.
     * Note: I used this just for testing.
     */
    public void printField() {
        System.out.println("---PRINTING FIELD---");
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.print(getFieldOwner(i, j) + " ");
            }
            System.out.println();
        }
    }

    /**
     * Evaluates the board to see if the game is over,
     * and then checks if the number of turns is maxed out at 9.
     * If both are not true, return false;
     * 
     * @return      returns true is game is over. Returns false if otherwise.
     */
    public boolean isGameOver() {
        int[] scores = board.evaluateBoard();

        for (int score : scores) {
            if (score == 3 || score == -3) {
                didSomeoneWin = true;
                return true;
            }
        }

        if (turnsCounter == 9) {
            return true;
        }
        // if both of the above conditions don't apply, game is not over
        return false;
    }

    /**
     * Increments the number of turns.
     */
    public void incrementTurnsCounter() {
        turnsCounter++;
    }

    /**
     * Returns the number of turns since the start of the game.
     * 
     * @return      the number of turns since the start of the game.
     */
    public int getTurnsCounter() {
        return turnsCounter;
    }

    /**
     * Sets the user symbol to a Symbol depending on whether the number of turns is even or odd.
     */
    public void setUserSymbol() {
        if (turnsCounter % 2 == 1) {
            userSymbol = Symbol.X;
        } else {
            userSymbol = Symbol.O;
        }
    }

    /**
     * Returns the current user symbol.
     * 
     * @return      the current user symbol.
     */
    public Symbol getUserSymbol() {
        return userSymbol;
    }

    /**
     * Returns true if a player has won the game. Otherwise, false.
     * 
     * @return      true if a player has won the game. Otherwise, return false.
     */
    public boolean getDidSomeoneWin() {
        return didSomeoneWin;
    }
}

Controller.java

package chautnguyen.com.github.tictactoe.controller;

import chautnguyen.com.github.tictactoe.model.Game;
import chautnguyen.com.github.tictactoe.view.View;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;

public class Controller implements ActionListener {
    private Game game;
    private View view;

    /**
     * Overloaded constructor. Initializes the game and view, and
     * adds the action listeners to the buttons in view.
     * 
     * @param   an instance of the Game class.
     * @param   an instance of the View class.
     */
    public Controller() {
        this.game = new Game();
        this.view = new View();
        addActionListeners();
    }

    /**
     * Adds an action listener to every button.
     */
    private void addActionListeners() {
        for (int i = 0; i < view.getNumberOfButtons(); i++) {
            view.getButton(i).addActionListener(this);
        }
    }

    /**
     * Increments the number of moves since the start of the game, and
     * sets the user symbol. It then finds out what x and y coordinates that button
     * corresponds to in the Game object.
     * Examples: button[0] would be Field[0][0]. button [1] would be Field[0][1].
     * button[5] would be Field[1][2].
     * 
     * It then sets the owner of the field in the Game object, and modifies the View buttons.
     * 
     * @param e     the action performed. In this game, it would be a mouse click.
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        if (game.isGameOver() == false) {
            game.incrementTurnsCounter();
            game.setUserSymbol();

            // The indices of the View JButton array is 0-8 while the
            // indices of the Game Field array
            // is a 2d 3x3 array, so I have to convert the index
            // into x- and y- coordinates.
            int indexOfViewButton = getJButtonIndex((JButton) e.getSource());
            int x = getX(indexOfViewButton); // row coordinate
            int y = getY(indexOfViewButton); // column coordinate

            game.setFieldOwner(game.getUserSymbol(), x, y);
            view.setFieldOwner(game.getUserSymbol(), (JButton) e.getSource());            
        }
    }

    /**
     * Returns the index of the current JButton.
     * 
     * @param button    the button that was clicked.
     * @return      the [0-8] index of the JButton clicked.
     */
    private int getJButtonIndex(JButton button) {
        int buttonIndex = 0;
        for (int i = 0; i < 9; i++) {
            if (button == view.getButton(i)) {
                buttonIndex = i;
            }
        }
        return buttonIndex;
    }

    /**
     * Returns the x-coordinate that corresponds to the index.
     * 
     * @param index     the index of of the JButton
     * @return      the x-coordinate of the 2d array that corresponds to the [0-8] index.
     */
    private int getX(int index) {
        if (0 <= index && index <= 2) {
            return 0;
        }
        if (3 <= index && index <= 5) {
            return 1;
        }
        if (6 <= index && index <= 8) {
            return 2;
        }
        return -1; // just to make sure all return paths work.
        // I did the above because I think it's more readable than its alternatives.
    }

    /**
     * Returns the y-coordinate that corresponds to the index.
     * 
     * @param index      the index of the JButton
     * @return      the y-coordinate of the 2d array that corresponds to the [0-8] index.
     */
    private int getY(int index) {
        if (index == 0 || index == 3 || index == 6) {
            return 0;
        }
        if (index == 1 || index == 4 || index == 7) {
            return 1;
        }
        if (index == 2 || index == 5 || index == 8) {
            return 2;
        }
        return -1; // just to make sure all return paths work.
        // I did the above because I think it's more readable than its alternatives.
    }

    /**
     * Informs the user of the outcome of the game.
     */
    public void informOutcome() {
        if (game.getDidSomeoneWin()) {
            view.informWin(game.getUserSymbol());
        } else {
            view.informTie();
        }
    }

    /**
     * Calls the isGameOver function in the Game class.
     * 
     * @return      returns true if game is over. Returns false if otherwise.
     */
    public boolean isGameOver() {
        return game.isGameOver();
    }
}

TicTacToe.java

package chautnguyen.com.github.tictactoe;

import chautnguyen.com.github.tictactoe.controller.Controller;

public class TicTacToe {
    public static void main(String[] args) {        
        Controller controller = new Controller();

        while (!controller.isGameOver()) {
            try {
                Thread.sleep(250);
            } catch(InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }

        controller.informOutcome();
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.