You're going to build two-player Tic-Tac-Toe in the terminal — the classic game where players take turns marking a 3×3 grid with X and O, and the first to claim three in a row (horizontally, vertically, or diagonally) wins. By the end you'll have a working game you can play with a friend, and you'll have written board display logic, win detection, and a turn-based game loop — all patterns that show up in bigger projects.
We're not racing. Each step is one idea. If a hint helps, take it — there's no penalty.
Before you start
Modern macOS ships Python 3 — try python3 --version. If it's older than 3.10, install via Homebrew: brew install python@3.12.
The walkthrough
Make the project file
In your terminal, inside the tic-tac-toe folder, create an empty file called tictactoe.py. Open it in your editor.
You can do this any way you like — touch tictactoe.py, code tictactoe.py, "File → New" in your editor, whatever's natural for you.
Represent the 3×3 board as a list
Create a board representation. A board has 9 cells (3 rows × 3 columns), and each cell can be empty, contain an X, or contain an O.
The simplest way to represent this is a list of 9 strings, starting all empty: [" ", " ", " ", " ", " ", " ", " ", " ", " "]. Cells will be indexed 0–8, left to right, top to bottom. (Later we'll let players use 1–9 for friendliness.)
In tictactoe.py, create that list. Store it in a variable called board. Then print it to confirm.
[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
Print the board as a readable grid
Right now board prints as a jumbled list. We need a function that displays it like an actual Tic-Tac-Toe board with rows, columns, and separators.
Write a function called display_board(board) that prints something like:
1 | 2 | 3
-----------
4 | 5 | 6
-----------
7 | 8 | 9
(The numbers above are placeholders — we'll show X and O soon. For now, cells 1–9 are just spaces because the board is empty.)
1 | 2 | 3 ----------- 4 | 5 | 6 ----------- 7 | 8 | 9
Write a function to place X or O on the board
When a player makes a move, we need to update the board. Write a function called place_mark(board, position, mark) that:
- Takes the board, a position (0–8), and a mark (either "X" or "O")
- Sets
board[position] = mark - Returns the modified board
Then test it: call the function to place an X at position 4 (the center), display the board, and verify the center cell now shows X.
1 | 2 | 3 ----------- 4 | X | 6 ----------- 7 | 8 | 9
Read player input — position 1–9
Players should say "I want cell 5" (1–9), not "I want cell 4" (0–8). Write a function called get_move(player) that:
- Takes the player ("X" or "O")
- Asks the player which cell they want (using
input()) - Converts the input from a string to an int
- Subtracts 1 (so the player's 1–9 input becomes the board's 0–8 index)
- Returns that 0-indexed position
Test it: call get_move("X"), type 5, and print the returned position. You should see 4.
X's turn. Pick a cell (1-9): 5 4
Reject out-of-range and occupied cells
Right now if a player types 15 or an already-taken cell, the game breaks. Write a function called is_valid_move(board, position) that:
- Returns
Trueif the position is in range (0–8) AND the cell is empty - Returns
Falseotherwise
Then wrap get_move() in a loop that keeps asking until the player picks a valid cell:
while True:
position = get_move(player)
if is_valid_move(board, position):
break
print("That cell is taken or out of range. Try again.")
Test it by trying to pick an invalid cell, then a valid one.
X's turn. Pick a cell (1-9): 10 That cell is taken or out of range. Try again. X's turn. Pick a cell (1-9): 5 4
Track whose turn it is
We need to alternate between X and O. Write a function called switch_player(current_player) that:
- Takes the current player ("X" or "O")
- Returns the other player
Also set up a variable current_player = "X" at the top of your game (X always goes first). Then, after each move, call current_player = switch_player(current_player) to swap turns.
Test it by playing two moves and confirming both X and O appear on the board.
X's turn. Pick a cell (1-9): 5 O's turn. Pick a cell (1-9): 3 1 | 2 | O ----------- 4 | X | 6 ----------- 7 | 8 | 9
Detect three in a row
A player wins if they claim three in a row horizontally. Write a function called check_win_rows(board, mark) that:
- Takes the board and a mark ("X" or "O")
- Checks each of the three rows (cells 0–2, 3–5, 6–8)
- Returns
Trueif any row has all three cells equal to the mark - Returns
Falseotherwise
Test it by filling a row with X and confirming it returns True.
True
Extend win detection to columns and diagonals
Extend the win-detection logic. Write a function called check_win(board, mark) that checks all winning configurations:
- Three in a row (horizontally) — cells 0–2, 3–5, 6–8 (reuse
check_win_rows()) - Three in a column (vertically) — cells 0–3–6, 1–4–7, 2–5–8
- Both diagonals — 0–4–8 and 2–4–6
Return True if any configuration matches the mark.
Test it by filling different winning patterns and confirming check_win() returns True for each.
True True True
Detect a full board with no winner
If the board fills up and no one has won, the game is a draw. Write a function called is_board_full(board) that:
- Returns
Trueif all 9 cells are filled (no spaces left) - Returns
Falseif any cell is still empty
Test it by filling the board and confirming it returns True.
True
What does `all(c != ' ' for c in board)` return?
Build the main game loop
Now tie it all together. Write the main game loop that:
- Displays the board
- Gets the current player's move (with validation)
- Places the mark
- Checks if that player won — if so, display the board, print a congratulations message, and break
- Checks if the board is full — if so, print a draw message and break
- Switch to the next player
- Repeat
Run the complete game and play through a few moves until someone wins or it's a draw.
1 | 2 | 3 ----------- 4 | 5 | 6 ----------- 7 | 8 | 9 X's turn. Pick a cell (1-9): 5 1 | 2 | 3 ----------- 4 | X | 6 ----------- 7 | 8 | 9 O's turn. Pick a cell (1-9): 1 O | 2 | 3 ----------- 4 | X | 6 ----------- 7 | 8 | 9 X's turn. Pick a cell (1-9): 4 O | 2 | 3 ----------- X | X | 6 ----------- 7 | 8 | 9 O's turn. Pick a cell (1-9): 7 O | 2 | 3 ----------- X | X | 6 ----------- O | 8 | 9 X wins!
Clear the screen and add a replay loop
Two small polish moves:
- At the top of the game loop, clear the terminal screen so the board doesn't scroll off:
import osand callos.system("clear")on macOS/Linux oros.system("cls")on Windows. A cross-platform approach is to checksys.platform. - After the game ends (win or draw), ask the player if they want to play again. If yes, reset the board and current_player to initial state and loop. If no, print "Thanks for playing!" and exit.
Run the game, play to completion, then choose to play again.
Play again? (yes/no): yes
You did it
Run python3 tictactoe.py and play through to victory (or a draw). Then play again. That game didn't exist a few hours ago — now you've got something you can challenge a friend with.
You wrote:
- a board representation and display function
- input parsing and validation
- turn-based game state management
- win-detection logic for rows, columns, and diagonals
- a full game loop with termination conditions
- cross-platform screen clearing
- a replay mechanic
Every one of those is a pattern you'll use again — in card games, word games, even 3-D game engines.