Editor

Four in a Row Game in Python

Intermediate · Games · about 90 minutes to build · 110 lines

Two players take turns dropping X and O pieces into a 6 by 7 grid, and the first to line up four across, down or diagonally wins.

The program

Press Run and answer its questions in the box that appears under the code.

four_in_a_row.py
"""Four in a row for two players: drop pieces into a 6 x 7 grid and line up four to win."""
ROWS = 6
COLS = 7
EMPTY = "."
PIECES = ("X", "O")
# The four lines to check, as (row step, column step): across, down and both diagonals.
DIRECTIONS = [(0, 1), (1, 0), (1, 1), (1, -1)]
def new_board():
"""Return an empty grid: a list of ROWS rows, each a list of COLS cells."""
return [[EMPTY] * COLS for _ in range(ROWS)]
def show_board(board):
"""Print the grid with the column numbers on top."""
print("\n " + " ".join(str(col + 1) for col in range(COLS)))
for row in board:
print("|" + " ".join(row) + "|")
def ask_column(board, piece):
"""Ask until the player picks a column that is not full; return its index."""
while True:
text = input(f"Player {piece}, choose a column (1-{COLS}): ").strip()
if not text.isdecimal() or not 1 <= int(text) <= COLS:
print(f" Please type a number from 1 to {COLS}.")
elif board[0][int(text) - 1] != EMPTY:
print(" That column is full. Pick another one.")
else:
return int(text) - 1
def drop_piece(board, col, piece):
"""Put the piece in the lowest empty cell of the column and return its row."""
for row in range(ROWS - 1, -1, -1):
if board[row][col] == EMPTY:
board[row][col] = piece
return row
return None # the column was full (ask_column prevents this)
def count_direction(board, row, col, d_row, d_col):
"""Count matching pieces next to (row, col), stepping by (d_row, d_col)."""
piece = board[row][col]
count = 0
row, col = row + d_row, col + d_col
while 0 <= row < ROWS and 0 <= col < COLS and board[row][col] == piece:
count += 1
row, col = row + d_row, col + d_col
return count
def is_winning_move(board, row, col):
"""Return True if the piece at (row, col) is part of four or more in a line."""
for d_row, d_col in DIRECTIONS:
# Count both ways along the line, plus the new piece itself.
ahead = count_direction(board, row, col, d_row, d_col)
behind = count_direction(board, row, col, -d_row, -d_col)
if 1 + ahead + behind >= 4:
return True
return False
def play_game():
"""Play one game and return the winning piece, or None for a draw."""
board = new_board()
for turn in range(ROWS * COLS):
piece = PIECES[turn % 2]
col = ask_column(board, piece)
row = drop_piece(board, col, piece)
show_board(board)
if is_winning_move(board, row, col):
return piece
return None
def ask_yes_no(prompt):
"""Ask a yes/no question and return True for yes."""
while True:
answer = input(prompt).strip().lower()
if answer in ("y", "yes"):
return True
if answer in ("n", "no"):
return False
print(" Please type y or n.")
def main():
"""Play games until the players stop, keeping score."""
print("=== Four in a Row ===")
print(f"Drop pieces into columns 1 to {COLS}. Four in a line in any direction wins.\n")
wins = {"X": 0, "O": 0}
while True:
winner = play_game()
if winner:
wins[winner] += 1
print(f"\nFour in a row. Player {winner} wins.")
else:
print("\nThe board is full. It's a draw.")
print(f"Score: X {wins['X']}, O {wins['O']}\n")
if not ask_yes_no("Play again? (y/n) "):
break
print()
print("Thanks for playing.")
if __name__ == "__main__":
main()

Open it in the full editor to save it, share it and keep building.

Sample run

What the program printed, and what was typed (in yellow).

=== Four in a Row ===
Drop pieces into columns 1 to 7. Four in a line in any direction wins.

Player X, choose a column (1-7): 4

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . X . . .|
Player O, choose a column (1-7): 4

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . O . . .|
|. . . X . . .|
Player X, choose a column (1-7): 3

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . O . . .|
|. . X X . . .|
Player O, choose a column (1-7): 8
  Please type a number from 1 to 7.
Player O, choose a column (1-7): 3

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . O O . . .|
|. . X X . . .|
Player X, choose a column (1-7): 5

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . O O . . .|
|. . X X X . .|
Player O, choose a column (1-7): 6

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . O O . . .|
|. . X X X O .|
Player X, choose a column (1-7): 2

 1 2 3 4 5 6 7
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . . . . . .|
|. . O O . . .|
|. X X X X O .|

Four in a row. Player X wins.
Score: X 1, O 0

Play again? (y/n) n
Thanks for playing.

How it is built

1. The grid

The board is a list of six rows, each a list of seven cells, so board[row][col] is one cell and row 0 is the top. DIRECTIONS holds one step for each kind of line.

ROWS = 6
COLS = 7
EMPTY = "."
PIECES = ("X", "O")
# The four lines to check, as (row step, column step): across, down and both diagonals.
DIRECTIONS = [(0, 1), (1, 0), (1, 1), (1, -1)]

2. Choosing a column

A column is full when its top cell is taken, so board[0][col] is the only cell the check needs.

def ask_column(board, piece):
    """Ask until the player picks a column that is not full; return its index."""
    while True:
        text = input(f"Player {piece}, choose a column (1-{COLS}): ").strip()
        if not text.isdecimal() or not 1 <= int(text) <= COLS:
            print(f"  Please type a number from 1 to {COLS}.")
        elif board[0][int(text) - 1] != EMPTY:
            print("  That column is full. Pick another one.")
        else:
            return int(text) - 1

3. Dropping a piece

The loop starts at the bottom row and climbs until it finds an empty cell, the way a real piece falls.

def drop_piece(board, col, piece):
    """Put the piece in the lowest empty cell of the column and return its row."""
    for row in range(ROWS - 1, -1, -1):
        if board[row][col] == EMPTY:
            board[row][col] = piece
            return row
    return None  # the column was full (ask_column prevents this)

4. Counting in one direction

From the new piece, the function keeps stepping by (d_row, d_col) while it stays on the board and finds the same piece. A step of (1, -1) walks down and to the left.

def count_direction(board, row, col, d_row, d_col):
    """Count matching pieces next to (row, col), stepping by (d_row, d_col)."""
    piece = board[row][col]
    count = 0
    row, col = row + d_row, col + d_col
    while 0 <= row < ROWS and 0 <= col < COLS and board[row][col] == piece:
        count += 1
        row, col = row + d_row, col + d_col
    return count

5. Four in a line

Only the newest piece can make a new line, so the check starts there. For each direction it counts matching pieces ahead and behind and adds one for the piece itself.

def is_winning_move(board, row, col):
    """Return True if the piece at (row, col) is part of four or more in a line."""
    for d_row, d_col in DIRECTIONS:
        # Count both ways along the line, plus the new piece itself.
        ahead = count_direction(board, row, col, d_row, d_col)
        behind = count_direction(board, row, col, -d_row, -d_col)
        if 1 + ahead + behind >= 4:
            return True
    return False

6. Taking turns

PIECES[turn % 2] gives X on even turns and O on odd ones. After 42 moves every cell is full and the game is a draw.

def play_game():
    """Play one game and return the winning piece, or None for a draw."""
    board = new_board()
    for turn in range(ROWS * COLS):
        piece = PIECES[turn % 2]
        col = ask_column(board, piece)
        row = drop_piece(board, col, piece)
        show_board(board)
        if is_winning_move(board, row, col):
            return piece
    return None

Make it bigger

  • Add a computer player that wins when it can, blocks when it must and otherwise prefers the middle column.
  • Mark the winning four by printing them in lowercase.
  • Let the players type their names and keep the score under those names.
  • Change ROWS and COLS to try a bigger board, and check that nothing else in the code needs to change.

What it practises