Explain the connect4 Java game?

483    Asked by MintaAnkney in Java , Asked on Oct 11, 2022

Here is a simple text-based version of connect four I made. I have been building this in an attempt to improve my Java skills (and possibly mention on my resume).

My goals with this project are as follows:

Clean, optimise, and restructure: code, classes, and game logic based on feedback.

Transform this program from text-based to graphics based using JavaFX

Add computer AI logic to face a challenging opponent (assuming this is feasible to implement at my current skill level)

Answered by Miura Baba

Separate the game model from the game UI

You have a text-based game. You want a JavaFX based game. The text-based game is outputting messages to the console. When you have a JavaFX based game, you won't have a console, but will want to present game feedback in an entirely different way. Finally, if you have an AI which explores the game-space by playing a number of fake games starting at the current state of the board, you don't want any visible feedback - just an indication of whether the series of moves the AI has made results in winning or losing.

Separating the model from the UI will allow you to have both a text-based game and a JavaFX game. It will allow you to write an AI which can play the game. It will do this because the game model will just maintain the state of the game, and determine when a player makes a winning move.

Maybe:

interface ConnectFour {
    enum Player { PLAYER_ONE, PLAYER_TWO }
    enum GameState { IN_PROGRESS, DRAW, WIN }
    void reset();
    GameState makeMove(Player player, int col) throws IllegalMoveException;
    List getColumn(int col);
}

Notice the interface doesn't have player names, nor does it assign colours to the players. That is a UI detail.

It also doesn't use weird numbers for status results, instead an enum is used. Any move is assumed to be valid; we don't need to return a boolean to indicate that the move was valid, so we can use the return to indicate if the player made the winning move. If PLAYER_ONE made the winning move, PLAYER_ONE wins. If PLAYER_TWO made the winning move, PLAYER_TWO wins. No need for separate enum values to distinguish the two at the game model. If the move is not valid, throw an exception. If the UI doesn't want to handle exceptions, then it should ensure the move is legal before asking the model to perform it.

Finally, we provide a function which will allow the UI to query the game board, so it can display it to the user. Perhaps as text. Perhaps as JavaFX elements. Or perhaps just to an AI which will have to process the information algorithmically. I've shown each column as a list. If a column has only two tokens, the list for the column will be length 2. No need to coerce EMPTY as some kind of fake Player enum value; empty locations are indicated by the shorter-than-maximum list length.

Then, you can write your UI's.

class ConnectFourConsole {
    Connect4 Java game = new ConnectFourImpl();
    // ...
}
And,
class ConnectFourJavaFX {
    ConnectFour game = new ConnectFourImpl();
    // ...
}

Neither UI need worry about whether a player gets four-in-a-row horizontally, vertically, or diagonally. The model handles that.

Finally, your model implementation.

class ConnectFourImpl implements ConnectFour {

    // ...

}

Implementation

Game Grid

The 2-d array for the game grid is fine, but I would use an enum for the data type. As an alternative, I like List> columns, where you can simply add Player tokens to the column's list as moves are made.

if (col < 0>= columns.size())
    throw new IllegalMove("Invalid column");
List column = columns.get(col);
// Row we are about to play into (0 = bottom)
int row = column.size();
if (row == ROWS)
    throw new IllegalMove("Column Full");
// Move is valid.
column.add(player);
// Check for win by player, or full board, return
// WIN, DRAW, or IN_PROGRESS respectively.

Checking for a Win

Adding 4 values to a Set and checking if the .size() is 1 is an interesting way of solving the "all 4 values match" problem. But it may be easier to simply check if all 4 values match the player who just played. And it avoids the "4-blanks-in-a-row is not a win" issue, too.

With 6 rows, and 7 columns, the number of 4-in-a-rows you can get horizontally, vertically, or diagonally is (I think) 69. There are a lot of combinations to check. However, the only way the player could have achieved 4-in-a-row vertically is if it happened in the column that the player just played in. At the top. Exactly one possibility.

// Four-in-a-row Vertically?
if (row >= 3 && column.stream()
                       .skip(row - 3)
                       .allMatch(token -> token == player))
     return WIN;

The only way the player can win horizontally is if the horizontal row is the row the player's piece landed in. At most 4 possibilities: the piece just added is at the start, 2nd, 3rd or last in the row of 4.

Diagonals are similarly constrained. The player's piece ended up at row,col. You just need to check row+i,col-i for i running from -3 to +3, as long as you don't fall off the game grid, which works out to at most 3 possible combinations. row-i,col+i gives at most other 3.

That reduces 69 four-in-a-row checks down to a maximum of 11, by only considering possibilities including the newly added piece.

Player.java

Your Player class has a final name, and a private static counter which is used to assign the player number when the Player is created. And comments indicate you return 1 if player one wins and 2 if player two wins.

What if you don't exit the game, but a new player wants to challenge the winner of the last match? Maybe this is the JavaFX version. You need a new Player(name) to allow the challenger to be named, which creates player.playerNumber == 3. Does your code still work? If so, your comments are unclear. If not, you've unnecessarily restricted your game to exactly two named players; if you want a different person to play, quit & restart the game!!!

Main.java

while(hasWon == false) {
   // Code to ask player 1 for move
   // Code to check for a winner, and exit
   // Code to draw board
   // Code to ask player 2 for move
   // Code to check for a winner, and exit
   // Code to draw board
}

Don't repeat yourself. There are two almost identical copies of the code in the while loop. Move the common code into a function.

while(hasWon == false) {
   processTurnFor(playerOne);
   // break if won
   processTurnFor(playerTwo);
}

Closer. But we are still explicitly handling playerOne and playerTwo. If we had a 4-player game, the code would still be ugly. Store the players in an array/list, and walk through the list, wrapping back to the start when you reach the end:

Player[] players = { playerOne, playerTwo };
player_index = 0;
while (hasWon == false) {
    processTurnFor(players[player_index]);
    player_index = (player_index + 1) % players.length;
}


Your Answer

Interviews

Parent Categories