Columbia University - COMS W1004 – Intro CS & Problem Solving in Java
Interactive Tic-Tac-Toe Game
Develop an interactive Tic-Tac-Toe game in Java where two players can play against each other on the console. The game should display the board, take player inputs, check for valid moves, determine win/draw conditions, and allow for multiple games.
Our Suggested Approach
1. Game Board Representation:
- Use a 2D character array (e.g.,
char[][] board = new char[3][3];
) to represent the 3x3 Tic-Tac-Toe board.
- Initialize cells with a character representing an empty space (e.g., ' ' or '-'). Player marks can be 'X' and 'O'.
2. Game Logic Core:
displayBoard()
function: Prints the current state of the board to the console in a readable format.
playerMove(char playerMark)
function:
- Prompts the current player for their move (e.g., row and column, or a single number 1-9 mapped to cells).
- Validates the input: Is it within bounds (0-2 for row/col)? Is the chosen cell empty?
- If valid, updates the board array with the player's mark. If invalid, re-prompts.
checkWin(char playerMark)
function:
- Checks all 8 possible win conditions (3 rows, 3 columns, 2 diagonals).
- Returns
true
ifplayerMark
has won,false
otherwise.
checkDraw()
function:
- Checks if the board is full (no empty cells remaining) AND no player has won. Returns
true
if it's a draw,false
otherwise.
3. Main Game Loop:
- Initialize the board.
- Keep track of the current player (e.g., 'X' starts).
- Loop until a win or draw occurs:
- Display the board.
- Call
playerMove()
for the current player.
- Call
checkWin()
for the current player. If true, announce winner and end game.
- Call
checkDraw()
. If true, announce draw and end game.
- Switch to the other player.
4. Playing Multiple Games (Optional):
- After a game ends, ask players if they want to play again.
- If yes, reset the board and start a new game loop.
5. Input Handling (Scanner):
- Use
java.util.Scanner
to get input from the console.
Model-View-Controller (MVC) Separation (Conceptual):
- While not strictly required for a simple console game, thinking in MVC terms can be good practice.
- Model: The
board
array and game state variables (current player).
- View: The
displayBoard()
function.
- Controller: The main game loop and functions like
playerMove()
,checkWin()
,checkDraw()
that manage game flow and logic.
Detecting Wins in O(1) (Advanced, for discussion):
- The outline mentions 'detect wins in O(1)'. This typically means after each move, you only need to check the row, column, and diagonals affected by that specific move, rather than scanning the entire board. This can be achieved by keeping track of counts of 'X's and 'O's in each row, column, and diagonal. When a player places a mark, increment the relevant counts. If any count reaches 3 for that player, they've won. This is more efficient than iterating through all 8 win conditions from scratch every time.
Illustrative Code Snippet
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
import java.util.Scanner; import java.util.Arrays; public class TicTacToe { private char[][] board; private char currentPlayerMark; public TicTacToe() { board = new char[3][3]; currentPlayerMark = 'X'; initializeBoard(); } // Initialize or resets the board to empty public void initializeBoard() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { board[i][j] = '-'; } } } // Print the current board public void displayBoard() { System.out.println("-------------"); for (int i = 0; i < 3; i++) { System.out.print("| "); for (int j = 0; j < 3; j++) { System.out.print(board[i][j] + " | "); } System.out.println(); System.out.println("-------------"); } } // Check if board is full public boolean isBoardFull() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (board[i][j] == '-') { return false; } } } return true; } // Check for a win public boolean checkWin() { // Check rows for (int i = 0; i < 3; i++) { if (checkRowCol(board[i][0], board[i][1], board[i][2])) { return true; } } // Check columns for (int j = 0; j < 3; j++) { if (checkRowCol(board[0][j], board[1][j], board[2][j])) { return true; } } // Check diagonals if (checkRowCol(board[0][0], board[1][1], board[2][2]) || checkRowCol(board[0][2], board[1][1], board[2][0])) { return true; } return false; } // Helper for checkWin to see if all three chars are the same (and not empty) private boolean checkRowCol(char c1, char c2, char c3) { return ((c1 != '-') && (c1 == c2) && (c2 == c3)); } // Change player marks public void changePlayer() { if (currentPlayerMark == 'X') { currentPlayerMark = 'O'; } else { currentPlayerMark = 'X'; } } // Place mark on the board public boolean placeMark(int row, int col) { if ((row >= 0 && row < 3) && (col >= 0 && col < 3) && (board[row][col] == '-')) { board[row][col] = currentPlayerMark; return true; } return false; // Invalid move } public char getCurrentPlayerMark() { return currentPlayerMark; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); TicTacToe game = new TicTacToe(); boolean gameActive = true; while (gameActive) { System.out.println("Current board layout:"); game.displayBoard(); System.out.println("Player " + game.getCurrentPlayerMark() + ", enter your move (row[0-2] col[0-2]):"); int row, col; while (true) { try { row = scanner.nextInt(); col = scanner.nextInt(); if (game.placeMark(row, col)) { break; // Valid move } else { System.out.println("This move is not valid. Try again."); } } catch (Exception e) { System.out.println("Invalid input. Please enter numbers for row and col."); scanner.nextLine(); // Consume the invalid input } } if (game.checkWin()) { game.displayBoard(); System.out.println("Player " + game.getCurrentPlayerMark() + " wins!"); gameActive = false; } else if (game.isBoardFull()) { game.displayBoard(); System.out.println("It's a draw!"); gameActive = false; } else { game.changePlayer(); } if (!gameActive) { System.out.println("Play again? (yes/no)"); String playAgain = scanner.next(); if (playAgain.equalsIgnoreCase("yes")) { game.initializeBoard(); game.currentPlayerMark = 'X'; // Reset starting player gameActive = true; } else { System.out.println("Thanks for playing!"); } } } scanner.close(); } }
Explanation & Key Points
Board Representation and Initialization
A 2D character array (char[][]
) is a straightforward way to represent the Tic-Tac-Toe board. An initializeBoard()
method sets all cells to an empty state (e.g., '-') at the start of each game.
Displaying the Board
The displayBoard()
method iterates through the 2D array and prints it to the console in a user-friendly grid format, making it easy for players to see the current game state.
Player Moves and Input Validation
The game logic takes input from the current player for their desired move (row and column). It's crucial to validate this input to ensure the chosen cell is within the board's boundaries and is currently empty. If the move is invalid, the player is prompted again.
Checking for a Win
The checkWin()
method systematically checks all eight possible winning combinations: three rows, three columns, and two diagonals. It returns true if the current player has three of their marks in a line.
Checking for a Draw
A draw occurs if all cells on the board are filled and no player has achieved a win. The isBoardFull()
method checks for filled cells, and this is combined with checkWin()
to determine a draw.
Game Loop and Player Turns
The main game logic is controlled by a loop that continues as long as the game is active (no win or draw). In each iteration, the board is displayed, the current player makes a move, win/draw conditions are checked, and then play switches to the other player.
O(1) Win Detection (Advanced Concept)
The outline mentions O(1) win detection. This advanced technique avoids re-scanning the whole board. Instead, after a player makes a move at (row, col)
, you only need to check if that move completed a line for that specific row
, col
, and the two main diagonals (if applicable). This can be done by maintaining counts for each player in each row, column, and diagonal. When a player places a mark, update these counts. A win occurs if any count reaches 3.
Key Concepts to Master
2D Arrays
Game Logic
Input/Output (Scanner)
Loops (while
, for
)
Conditional Statements (if
/else
)
Methods/Functions
Object-Oriented Programming (Basic Class Structure in Java)
Input Validation
State Management (Current Player, Game Over)
How Our Tutors Elevate Your Learning
- Java Fundamentals: Helping with Java syntax, class structure, methods, and using the
Scanner
class for input. - Game Logic Design: Walking through the design of the game loop, player turn management, and the logic for checking win/draw conditions.
- Array Manipulation: Assisting with correctly indexing and updating the 2D array representing the board.
- Debugging: Helping to find and fix common errors, such as incorrect win detection, issues with input validation, or problems with player switching.
- Code Structure: Discussing how to organize the code into logical methods for better readability and maintainability.
- Advanced Win Detection: If desired, explaining and helping implement the O(1) win detection strategy using counters for rows, columns, and diagonals.