Description
Company: Databricks
Problem Statement
Create a Tic-Tac-Toe game class. It must follow these rules:
Requirements
Board Setup:
- The board size is M x N. It might not be a square.
- A player needs K marks in a row to win. K can be any number.
Main Functions:
- Create a
TicTacToeclass. The constructor takes the board size and the winning number K. - Create a
move(i, j)method. This method should:- Put the current player's mark at row
iand columnj. - Print the board.
- Print "Player X won" if someone wins.
- Return the winner (1 for Player 1, 2 for Player 2, or 0 if the game is still going).
- Put the current player's mark at row
Rules:
- Player 1 and Player 2 take turns.
- Assume all moves are valid (you do not need to check for errors).
- Check if someone won after every move.
- A player wins by connecting K marks in:
- A row
- A column
- The main diagonal (top-left to bottom-right)
- The anti-diagonal (bottom-left to top-right)
Usage Example
TicTacToe game = new TicTacToe(3, 4, 3); // 3x4 board, need 3 to win
game.move(0, 0); // Player 1
Board:
| X | | | |
| | | | |
| | | | |
game.move(0, 1); // Player 2
Board:
| X | O | | |
| | | | |
| | | | |
game.move(1, 0); // Player 1
Board:
| X | O | | |
| X | | | |
| | | | |
game.move(1, 1); // Player 2
Board:
| X | O | | |
| X | O | | |
| | | | |
game.move(2, 0); // Player 1
Board:
| X | O | | |
| X | O | | |
| X | | | |
Player 1 won
Input Limits
- M and N are between 2 and 100.
- K is at least 2 and cannot be bigger than the board size.
- Players are defined as 1 and 2.
iandjare valid coordinates inside the board.- Speed is not the main goal.
Solution Details
How It Works
- Board Data: Use a 2D array. 0 is empty, 1 is Player 1, and 2 is Player 2.
- Making a Move:
- Save the mark on the board.
- Show the board.
- Look in 4 directions from the new mark to see if the player won.
- If yes, return the winner.
- Checking for a Win: After a move at (
row,col), check for K connected marks in:- Horizontal (Left + Right)
- Vertical (Up + Down)
- Diagonal (Top-Left + Bottom-Right)
- Anti-Diagonal (Bottom-Left + Top-Right)
- Checking for a Draw: The game is a draw if the board is full and no one won.
- Note: You do not need to check if a win is mathematically impossible early on. Just check if the board is full.
Time Performance
- Constructor:
O(M × N)to build the empty board. move():O(max(M, N))to scan the lines for a winner.- Total:
O(M × N)for the whole game.
Space Usage
O(M × N) to store the board in memory.
Code Implementation
class TicTacToe:
def __init__(self, M: int, N: int, K: int):
"""
Setup M x N board. Need K marks to win.
"""
self.rows = M
self.cols = N
self.k = K
self.board = [[0] * N for _ in range(M)]
self.current_player = 1 # Player 1 goes first
self.game_over = False
def move(self, i: int, j: int) -> int:
"""
Place mark at (i, j). Print board and check winner.
Returns: 0 (no winner), 1 (player 1 wins), 2 (player 2 wins)
"""
if self.game_over:
print("Game is already over!")
return 0
# Place the mark
self.board[i][j] = self.current_player
# Print the board
self._print_board()
# Check for winner
if self._check_win(i, j):
print(f"Player {self.current_player} won")
self.game_over = True
return self.current_player
# Switch player
self.current_player = 3 - self.current_player # Toggle between 1 and 2
return 0
def _print_board(self):
"""Print the board visually."""
symbols = {0: ' ', 1: ' X ', 2: ' O '}
print("\nBoard:")
for row in self.board:
print('|' + '|'.join(symbols[cell] for cell in row) + '|')
print()
def _check_win(self, row: int, col: int) -> bool:
"""
Check if the last move at (row, col) caused a win.
"""
player = self.current_player
# Check all 4 directions
directions = [
(0, 1), # Horizontal
(1, 0), # Vertical
(1, 1), # Main diagonal
(1, -1) # Anti-diagonal
]
for dr, dc in directions:
count = 1 # Count the current spot
# Count going forward
count += self._count_consecutive(row, col, dr, dc, player)
# Count going backward
count += self._count_consecutive(row, col, -dr, -dc, player)
if count >= self.k:
return True
return False
def _count_consecutive(self, row: int, col: int, dr: int, dc: int, player: int) -> int:
"""
Count how many marks match in one direction.
"""
count = 0
r, c = row + dr, col + dc
while 0 <= r < self.rows and 0 <= c < self.cols and self.board[r][c] == player:
count += 1
r += dr
c += dc
return count
# Example usage
if __name__ == "__main__":
game = TicTacToe(3, 4, 3)
game.move(0, 0) # Player 1
game.move(0, 1) # Player 2
game.move(1, 0) # Player 1
game.move(1, 1) # Player 2
game.move(2, 0) # Player 1 wins
Extension: Adding an AI
Add an AI player that plays automatically.
New Requirements
Update Constructor:
- Add a parameter
isAI. - Signature:
TicTacToe(int M, int N, int K, boolean isAI).
AI Logic:
- If
isAIis true, Player 2 is the computer. - The AI picks a valid empty spot.
- The game ends when someone wins or draws.
Update move():
- When a human plays (Player 1), the AI should immediately play afterwards (Player 2).
Draws:
- If the board is full and no one wins, print "Draw".
AI Logic
The AI decides where to move in this order:
- Win: If the AI can win right now, take that spot.
- Block: If the human can win on their next turn, block that spot.
- Random: If neither of the above, pick a random empty spot.
Code Implementation
import random
from typing import Optional, Tuple
class TicTacToeWithAI:
def __init__(self, M: int, N: int, K: int, isAI: bool = False):
"""
Setup board. If isAI is True, Player 2 is computer.
"""
self.rows = M
self.cols = N
self.k = K
self.board = [[0] * N for _ in range(M)]
self.current_player = 1 # Player 1 starts (human)
self.game_over = False
self.is_ai_enabled = isAI
self.ai_player = 2 # AI is Player 2
def move(self, i: int, j: int) -> int:
"""
Human move at (i, j). If AI is on, AI moves immediately after.
"""
if self.game_over:
print("Game is already over!")
return 0
# Place the mark
result = self._make_move(i, j)
if result != 0: # Game ended (win or draw)
return result
# If AI is enabled and it's now AI's turn, make AI move
if self.is_ai_enabled and self.current_player == self.ai_player:
return self._ai_move()
return 0
def _make_move(self, i: int, j: int) -> int:
"""
Helper to place mark and check result.
"""
# Place the mark
self.board[i][j] = self.current_player
# Print the board
self._print_board()
# Check for winner
if self._check_win(i, j):
print(f"Player {self.current_player} won")
self.game_over = True
return self.current_player
# Check for draw
if self._is_board_full():
print("Draw")
self.game_over = True
return -1
# Switch player
self.current_player = 3 - self.current_player
return 0
def _ai_move(self) -> int:
"""
AI logic: Win -> Block -> Random.
"""
print(f"AI (Player {self.ai_player}) is thinking...")
# Strategy 1: Try to win
move = self._find_winning_move(self.ai_player)
if move:
print(f"AI plays at ({move[0]}, {move[1]})")
return self._make_move(move[0], move[1])
# Strategy 2: Block opponent from winning
opponent = 3 - self.ai_player
move = self._find_winning_move(opponent)
if move:
print(f"AI blocks at ({move[0]}, {move[1]})")
return self._make_move(move[0], move[1])
# Strategy 3: Random move
move = self._get_random_empty_cell()
if move:
print(f"AI plays at ({move[0]}, {move[1]})")
return self._make_move(move[0], move[1])
return 0
def _find_winning_move(self, player: int) -> Optional[Tuple[int, int]]:
"""
Look for a move that wins immediately for 'player'.
"""
for i in range(self.rows):
for j in range(self.cols):
if self.board[i][j] == 0: # Empty cell
# Try this move
self.board[i][j] = player
wins = self._check_win(i, j, player)
self.board[i][j] = 0 # Undo
if wins:
return (i, j)
return None
def _get_random_empty_cell(self) -> Optional[Tuple[int, int]]:
"""
Pick any empty spot.
"""
empty_cells = []
for i in range(self.rows):
for j in range(self.cols):
if self.board[i][j] == 0:
empty_cells.append((i, j))
return random.choice(empty_cells) if empty_cells else None
def _is_board_full(self) -> bool:
"""Check if board is full."""
for row in self.board:
if 0 in row:
return False
return True
def _print_board(self):
"""Print the board."""
symbols = {0: ' ', 1: ' X ', 2: ' O '}
print("\nBoard:")
for row in self.board:
print('|' + '|'.join(symbols[cell] for cell in row) + '|')
print()
def _check_win(self, row: int, col: int, player: Optional[int] = None) -> bool:
"""
Check if 'player' won after a move at (row, col).
"""
if player is None:
player = self.current_player
# Check all 4 directions
directions = [
(0, 1), # Horizontal
(1, 0), # Vertical
(1, 1), # Main diagonal
(1, -1) # Anti-diagonal
]
for dr, dc in directions:
count = 1 # Count the current position
# Count in positive direction
count += self._count_consecutive(row, col, dr, dc, player)
# Count in negative direction
count += self._count_consecutive(row, col, -dr, -dc, player)
if count >= self.k:
return True
return False
def _count_consecutive(self, row: int, col: int, dr: int, dc: int, player: int) -> int:
"""
Count consecutive marks in one direction.
"""
count = 0
r, c = row + dr, col + dc
while 0 <= r < self.rows and 0 <= c < self.cols and self.board[r][c] == player:
count += 1
r += dr
c += dc
return count
# Example usage
if __name__ == "__main__":
print("=== Game with AI ===")
game = TicTacToeWithAI(3, 3, 3, isAI=True)
# Human player makes moves, AI responds automatically
game.move(0, 0) # Human (Player 1)
# AI automatically responds
game.move(0, 1) # Human (Player 1)
# AI automatically responds
game.move(0, 2) # Human (Player 1) tries to win
# Game ends
Example Output
=== Game with AI ===
Board:
| X | | |
| | | |
| | | |
AI (Player 2) is thinking...
AI plays at (1, 1)
Board:
| X | | |
| | O | |
| | | |
Board:
| X | X | |
| | O | |
| | | |
AI (Player 2) is thinking...
AI blocks at (0, 2)
Board:
| X | X | O |
| | O | |
| | | |
Performance Overview
- Basic Game
- Time:
O(M × N)maximum. Each move takesO(max(M, N)). - Space:
O(M × N)to hold the grid.
- Time:
- With AI
- Time: The AI is slower. It might take
O(M × N × K)because it checks every empty spot to see if it's a winning move. - Space: Same as above,
O(M × N).
- Time: The AI is slower. It might take
Special Cases
- Full board (Draw): Return -1 or print "Draw" correctly.
- Small games: If K=2 and the board is 2x2, the game ends very fast.
- Impossible K: If K is larger than the board rows or columns, no one can win.
- Non-square boards: If M and N are different sizes, diagonals might be shorter.
- AI trapped: If the AI has no moves, handle the draw logic carefully.
Important Takeaways
Evaluation Points
- The Basics:
- Did you set up the board correctly?
- Do players switch turns correctly?
- Do you check all 4 directions for a win?
- Win Logic:
- You must check from the last move.
- Count marks going "forward" and "backward" from that spot.
- Common mistake: Only looking one way (like only looking right, but forgetting left).
- Code Style:
- Keep logic separate (one function for moves, one for checking wins).
- Use clear names for variables.
Common Errors
- Bad Win Check: Not checking the negative direction (e.g., checking up but not down).
- Counting Errors: Forgetting to include the piece you just placed in the count.
- Board Data: Storing strings like "X" and "O" is okay, but numbers (1, 2) are often easier to manage.
- AI Loops: If the board is full, the AI might loop forever if you don't check for a draw first.
Discussion Topics
- Detecting Draws Early: Can we tell it's a draw before the board is full?
- Yes, but it is hard to code. You would need to check if enough empty spots exist in a line to make K. For a simple interview, checking for a full board is enough.
- Large Boards: How to handle a 1000x1000 board?
- Don't use a 2D array. Use a HashMap to store only the moves made.
- When checking for a win, only look at spots near the last move.
- Smarter AI:
- Use the Minimax algorithm. It looks ahead at future turns to find the perfect move.
Loading editor…
OUTPUTLast run results appear here.
No output yet. Click "Run Code" to see results.