I hope you're enjoying learning about Java and programming in general. First, it's my belief that GUI programming is probably not the easiest way to learn how to program and I would suggest that you might want to consider learning more of the basics before digging too deeply into the GUI aspects.
As far as your code is concerned I don't think you're "passing objects around different classes too much" at all. In fact I think that you've done pretty well with regard to the general object oriented design of the classes.
When you said you would like to "know about better ways to do the checkWinningMove() method" my assumption is that you want to do it more generically rather than hardcoding all the possible outcomes. With that in mind I have modified your Grid and XOButton classes below. Most of my critiques are included in comments within the code.
You will notice that some of my comments refer to a link about access modifiers, you can find that right here. It would be a good idea to become familiar with these modifiers and use them appropriately to restrict access to the internal workings of your classes. Only make public what needs to be seen by code that is using your class.
BEGIN EDIT #1:
I realized after posting that you were using MouseListener
instead of ActionListener
in your button class. Please read this about why you should use ActionListener
. I updated the code below to use an ActionListener
.
One other thing that comes to mind - you could optimize the logic that performs the winning move check if you give your buttons additional fields to represent their row and column in the grid. If you do this then you could, for example, check only the row of the button that was clicked when looking for a horizontal win and check only the column of the button when searching for a vertical win. Since optimization was not part of your question and you said you're new to Java I did NOT implement this logic in the code below. If you want to try it out here's a code snippet to get you started:
Grid:
public void initialiseButtons() {
//renamed i and j to row and col for readability
for (int row = 0; row < buttonGrid.length; row++) {
for (int col = 0; col < buttonGrid[row].length; col++) {
//Changed this row to add row and col parameters in button constructor
buttonGrid[row][col] = new XOButton(this, row, col);
panel.add(buttonGrid[row][col]);
}
}
}
XOButton:
public XOButton(Grid grid, int row, int col) {
...
this.row = row;
this.col = col;
...
}
public int getRow(){...}
public int getCol(){...}
END EDIT #1
BEGIN EDIT #2
I wrote my original post and the edit late and night so I didn't have much chance to polish the code. As I thought about it more this morning I decided it might be best to illustrate one of the principles of Object Oriented Design - code reuse. Although perhaps in this instance it falls under modular design more than Object Oriented since the code is reused within the same class, however, I think it's relevant to your question and will help you to see how a design is polished. As you look at my original post, pay attention to the following methods: isHorizontalWin()
isVerticalWin()
isDiagonalWin()
and notice how the first two are almost identical and that the two parts of the third method are almost identical as well. This is a good indicator that it would be helpful to create a something more generic that can be reused. This is beneficial because when maintaining the code you can change only one place in order to fix bugs or add features that affect several methods. It also reduces the number of lines of code making it easier to read (if done well). Since you're new to programming let me explain my thinking - I thought that it would be easiest to implement a generic solution for the horizontal win check so I tackled that first. It then occurred to me that every win scenario involves a set of buttons all having the same "token" as their text. Therefore I wrote this method:
/**
* Determines whether all of the buttons in the specified array have the
* same text. Note that empty string is not considered valid text.
* @param lineToProcess an array of buttons representing a line in the grid
* @return true if all buttons in the array have the same non-empty text, false otherwise.
*/
private boolean isWin(JButton[] lineToProcess) {
String rowToken = null;
boolean foundWin = true;
for(JButton button: lineToProcess) {
if (rowToken == null) {
rowToken = button.getText();
} else if ("".equals(button.getText())) {
foundWin = false;
break;
} else if (!rowToken.equals(button.getText())) {
foundWin = false;
break;
}
}
return foundWin;
}
Implementing the horizontal win method using this new method above proved very easy:
/**
* Determines if there is a winner by checking each row for consecutive
* matching tokens.
* @return true if there is a winner horizontally, false otherwise.
*/
private boolean isHorizontalWin() {
for(int row = 0; row < buttonGrid.length; row++){
if(isWin(buttonGrid[row]))
return true;
}
return false;
}
The vertical win method was slightly more complex because I had to build an array containing all the buttons in a column of the grid:
/**
* Determines whether there is a winner by checking column for consecutive
* matching tokens.
* @return true if there is a vertical winner, false otherwise.
*/
private boolean isVerticalWin() {
JButton[] column = null;
for(int col = 0; col < columns; col++){
column = new JButton[rows];
for(int row = 0; row < rows; row++){
column[row] = buttonGrid[row][col];
}
if(isWin(column))
return true;
}
return false;
}
Finally I implemented the diagonal win method by creating an array containing references to all of the buttons on a diagonal then repeating that for the second diagonal:
/**
* Determines if there is a winner by checking each diagonal for consecutive
* matching tokens.
* @return true if a diagonal winner exists, false otherwise.
*/
private boolean isDiagonalWin() {
int row = 0, col = 0;
//Create a one-dimensional array to represent the diagonal. Use the lesser
// of the rows or columns to set its size. If the grid is rectangular then
// a diagonal will always be the size of the lesser of its two dimensions.
int size = columns < rows ? columns : rows;
JButton[] diagonal = new JButton[size];
//Since we know the grid is a square we really could just check one of
// these - either row or col, but I left both in here anyway.
while (row < rows && col < columns) {
diagonal[col] = buttonGrid[row][col];
row++;
col++;
}
if (isWin(diagonal)) {
return true;
}
row = rows - 1;
col = 0;
diagonal = new JButton[size];
while (row >=0 && col < columns) {
diagonal[col] = buttonGrid[row][col];
row--;
col++;
}
return isWin(diagonal);
}
END EDIT #2
Here is the Grid class:
import java.awt.GridLayout;
import javax.swing.*;
public class Grid {
//The fields of this class do not have any access level modifiers,
// it would probably be wise to look into this, see the link provided.
Player player1;
Player player2;
//Added this field to track the player whose turn it is currently.
Player currentPlayer;
int rows = 3;
int columns = 3;
JFrame frame;
JPanel panel = new JPanel();
JButton[][] buttonGrid = new XOButton[rows][columns];
// Removed this field as it is not needed any longer.
// String[] stringGrid = new String[rows * columns];
public Grid(Player p1, Player p2) {
this.player1 = p1;
this.player2 = p2;
currentPlayer = player1; //added a variable to track the current player
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
//It is generally not good design to have overridable methods in a
// constructor call, consider making this method final or private.
createGrid();
}
/**
* @return the Player whose turn it is currently.
*/
public Player getCurrentPlayer() {
return currentPlayer;
}
/**
* Yields control to the next player
*/
public void nextPlayer() {
if (currentPlayer.equals(player1)) {
System.out.println("It's player2's (" + player2.getToken() + ") turn now.");
currentPlayer = player2;
} else {
System.out.println("It's player1's (" + player1.getToken() + ") turn now.");
currentPlayer = player1;
}
}
public void createGrid() {
frame = new JFrame("Tic Tac Toe");
frame.setSize(300, 300);
frame.setVisible(true);
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setResizable(false);
//Changed this line to use the rows and columns variables
panel.setLayout(new GridLayout(rows, columns));
initialiseButtons();
// removed - no longer needed
// initialiseStringGrid();
frame.add(panel);
}
public void initialiseButtons() {
//renamed i and j to row and col for readability
for (int row = 0; row < buttonGrid.length; row++) {
for (int col = 0; col < buttonGrid[row].length; col++) {
buttonGrid[row][col] = new XOButton(this);
panel.add(buttonGrid[row][col]);
}
}
}
// removed - no longer needed
// public void initialiseStringGrid() {
// for (int i = 0; i < stringGrid.length; i++) {
// stringGrid[i] = "-1";
// }
// }
// removed - no longer needed
// public void updateStringGrid(int position, String token) {
// stringGrid[position] = token;
// }
public void checkIfWinningMove() {
if (isHorizontalWin() || isVerticalWin() || isDiagonalWin()) {
winningMove(currentPlayer.getToken());
}
}
/**
* Determines if there is a winner by checking each row for consecutive
* matching tokens.
* @return true if there is a winner horizontally, false otherwise.
*/
private boolean isHorizontalWin() {
for (int row = 0; row < buttonGrid.length; row++) {
String rowToken = null;
boolean foundWin = true;
for (int col = 0; col < buttonGrid[row].length; col++) {
if (rowToken == null) {
rowToken = buttonGrid[row][col].getText();
} else if ("".equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
} else if (!rowToken.equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
}
}
if (foundWin) {
return true;
}
}
return false;
}
/**
* Determines whether there is a winner by checking column for consecutive
* matching tokens.
* @return true if there is a vertical winner, false otherwise.
*/
private boolean isVerticalWin() {
for (int col = 0; col < columns; col++) {
String colToken = null;
boolean foundWin = true;
for (int row = 0; row < rows; row++) {
if (colToken == null) {
colToken = buttonGrid[row][col].getText();
} else if ("".equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
} else if (!colToken.equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
}
}
if (foundWin) {
return true;
}
}
return false;
}
/**
* Determines if there is a winner by checking each diagonal for consecutive
* matching tokens.
* @return true if a diagonal winner exists, false otherwise.
*/
private boolean isDiagonalWin() {
int row = 0, col = 0;
String diagToken = null;
boolean foundWin = true;
//Since we know the grid is a square we really could just check one of
// these - either row or col, but I left both in here anyway.
while (row < rows && col < columns) {
if (diagToken == null) {
diagToken = buttonGrid[row][col].getText();
} else if ("".equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
} else if (!diagToken.equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
}
row++;
col++;
}
if (foundWin) {
return true;
}
diagToken = null;
row = rows - 1;
col = 0;
foundWin = true;
while (row >=0 && col < columns) {
if (diagToken == null) {
diagToken = buttonGrid[row][col].getText();
} else if ("".equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
} else if (!diagToken.equals(buttonGrid[row][col].getText())) {
foundWin = false;
break;
}
row--;
col++;
}
return foundWin;
}
// removed - no longer needed
// public void checkIfWinningMove(int position, String token) {
// switch (position) {
// case 0:
//
// // check horizontal
// if (stringGrid[1].equals(token) && stringGrid[2].equals(token)) {
// winningMove(token);
// } // check diagonal
// else if (stringGrid[4].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[3].equals(token) && stringGrid[3].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 1:
//
// // check horizontal
// if (stringGrid[0].equals(token) && stringGrid[2].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[4].equals(token) && stringGrid[7].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 2:
//
// // check horizontal
// if (stringGrid[1].equals(token) && stringGrid[0].equals(token)) {
// winningMove(token);
// } // check diagonal
// else if (stringGrid[4].equals(token) && stringGrid[6].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[5].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 3:
// // check horizontal
// if (stringGrid[4].equals(token) && stringGrid[5].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[0].equals(token) && stringGrid[6].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 4:
// // check horizontal
// if (stringGrid[3].equals(token) && stringGrid[5].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[1].equals(token) && stringGrid[7].equals(token)) {
// winningMove(token);
// }
// // check diagonal 1
// if (stringGrid[0].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// } // check diagonal 2
// else if (stringGrid[2].equals(token) && stringGrid[6].equals(token)) {
// winningMove(token);
// }
// break;
// case 5:
// // check horizontal
// if (stringGrid[4].equals(token) && stringGrid[3].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[2].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 6:
//
// // check horizontal
// if (stringGrid[7].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// } // check diagonal
// else if (stringGrid[4].equals(token) && stringGrid[2].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[3].equals(token) && stringGrid[0].equals(token)) {
// winningMove(token);
// }
//
// break;
// case 7:
// // check horizontal
// if (stringGrid[6].equals(token) && stringGrid[8].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[4].equals(token) && stringGrid[1].equals(token)) {
// winningMove(token);
// }
// break;
// case 8:
//
// // check horizontal
// if (stringGrid[7].equals(token) && stringGrid[6].equals(token)) {
// winningMove(token);
// } // check diagonal
// else if (stringGrid[4].equals(token) && stringGrid[0].equals(token)) {
// winningMove(token);
// } // check vertical
// else if (stringGrid[5].equals(token) && stringGrid[2].equals(token)) {
// winningMove(token);
// }
//
// break;
//
// }
// }
public void winningMove(String token) {
if (token.equals(player1.getToken())) {
JOptionPane.showMessageDialog(frame, "Player 1 wins " + "(" + player1.getToken() + ")");
} else {
JOptionPane.showMessageDialog(frame, "Player 2 wins " + "(" + player2.getToken() + ")");
}
System.exit(0);
}
}
Here is the button class:
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
public class XOButton extends JButton {
//None of the fields have access level modifiers, see provided link
Grid g;
int status = 0;
static int clickCount = 0;
static int id = 0;
int buttonId;
public XOButton(Grid grid) {
g = grid;
buttonId += id;
id++;
setFont(new Font("Arial", Font.PLAIN, 60));
setText("");
//It is generally not considered good practice to call an
// overridable method in a constructor. Consider making this
// method final or private.
this.buildButton();
}
public void buildButton() {
this.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
XOButton button = (XOButton)e.getSource();
if ("".equals(button.getText())) {
button.setText(g.getCurrentPlayer().getToken());
g.checkIfWinningMove();
g.nextPlayer();
} else {
System.out.println("invalid move, space already occupied by: " + getText());
}
}
});
// removed - use action listener instead, and created methods in Grid class to handle whose turn it is.
// this.addMouseListener(new MouseAdapter() {
// @Override
// public void mouseClicked(MouseEvent e) {
// if(clickCount == 0){
// if(isEnabled()){
// setText(g.player1.getToken());
// g.updateStringGrid(buttonId, g.player1.getToken());
// g.checkIfWinningMove(buttonId, g.player1.getToken());
// }
// setEnabled(false);
//
// }else{
// if(isEnabled()){
// setText(g.player2.getToken());
// g.updateStringGrid(buttonId, g.player2.getToken());
// g.checkIfWinningMove(buttonId, g.player2.getToken());
// }
// setEnabled(false);
// }
//
// clickCount++;
// clickCount %= 2;
// }
// });
}
}