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:
- Did I implement the majority of the advice correctly?
- 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).
- 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();
}
}