Alumpath
Columbia University Logo

Columbia University - COMS W1004 – Intro CS & Problem Solving in Java

Interactive Tic-Tac-Toe Game

Assignment Problem:

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:

  1. displayBoard() function: Prints the current state of the board to the console in a readable format.
  1. playerMove(char playerMark) function:
  1. Prompts the current player for their move (e.g., row and column, or a single number 1-9 mapped to cells).
  1. Validates the input: Is it within bounds (0-2 for row/col)? Is the chosen cell empty?
  1. If valid, updates the board array with the player's mark. If invalid, re-prompts.
  1. checkWin(char playerMark) function:
  1. Checks all 8 possible win conditions (3 rows, 3 columns, 2 diagonals).
  1. Returns true if playerMark has won, false otherwise.
  1. checkDraw() function:
  1. 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:

  1. Initialize the board.
  1. Keep track of the current player (e.g., 'X' starts).
  1. Loop until a win or draw occurs:
  1. Display the board.
  1. Call playerMove() for the current player.
  1. Call checkWin() for the current player. If true, announce winner and end game.
  1. Call checkDraw(). If true, announce draw and end game.
  1. 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

java
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 ArraysGame LogicInput/Output (Scanner)Loops (while, for)Conditional Statements (if/else)Methods/FunctionsObject-Oriented Programming (Basic Class Structure in Java)Input ValidationState 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.

Need Personalized Help with Your Assignment?

Our human experts provide tailored guidance for your specific course assignments, helping you understand concepts and succeed.