import copy
import solutions
Project 1 - Connect 4
In this project, we will implement a Connect 4 game with a rather basic graphical interface. To achieve this, we will use the fundamental objects of Python.
Game rules
The goal of Connect 4 is to align a sequence of 4 pieces of the same color on a grid with 6 rows and 7 columns. Each player has 21 pieces of one color (usually yellow or red by convention). The two players take turns placing a piece in the column of their choice, and the piece slides down to the lowest possible position in that column, after which it is the opponent’s turn to play. The winner is the player who first aligns (horizontally, vertically, or diagonally) at least four consecutive pieces of their color. If, when all the grid cells are filled, neither player has achieved such an alignment, the game is declared a draw.
To simplify the code for this project, we will assume that winning alignments can only be horizontal or vertical. Diagonals will not be considered (but they are an interesting exercise to go further!).
Project plan
We will break down the construction of the game into different parts:
initialization of the grid
representation of the grid
game function
detection of a victory (horizontal)
detection of a victory (vertical)
end of game
Initialization of the grid
The objective of this part is to initialize a Python object that represents a Connect 4 grid. The choice we will make is to represent the grid as a list of lists. It will be a 6x7 matrix: we will therefore have a list of 6 elements (which will represent the rows of the grid), each of which will be a list containing 7 elements (which will represent the pieces).
Each element of the grid will be represented by a string, which can take three values:
’ ’ : if it is an empty cell
‘R’ : if it is a red piece.
‘Y’ : if it is a yellow piece.
In the grid initialization function, each element will therefore be initialized as a string containing a space.
Note: Make sure that the rows are independent objects, in other words, modifying one of the lists does not affect the others.
Expected result
= solutions.initialize_grid()
grid_solution grid_solution
print(f'Number of rows: {len(grid_solution)}')
print(f'Number of columns: {len(grid_solution[0])}')
Your turn!
def initialize_grid():
# Your code here
return grid
# Checking the result
= initialize_grid()
grid grid
# Checking the result
print(f'Number of rows: {len(grid)}')
print(f'Number of columns: {len(grid[0])}')
Representation of the grid
Our grid is initialized, but its display is quite basic. The idea of this part is to provide a more visual representation of the game during a game.
To do this, we will create a function that takes the previously initialized grid as input and returns its representation (via the print
function). The columns will be separated by the | character (vertical bar).
Hint: A possible solution involves two concepts we have seen in previous exercises: string concatenation and the join
function, which “joins” the elements of a list by separating them with a certain character. Here is a reminder of an example using these two concepts:
= ["a", "b", "c", "d", "e"]
l = "START " + ", ".join(l) + " END"
l_join print(l_join)
Expected result
solutions.display_grid(grid_solution)
Your turn!
def display_grid():
# Your code here
# Checking the result
display_grid(grid)
Game function
Now that we can represent our grid, let’s focus on the core of Connect 4: the game. The objective of this part is to code a make_move
function that will modify the grid when a player takes their turn.
This function takes as input:
the grid
the column chosen by the player
the color of the piece (‘R’ for the red piece, and ‘Y’ for the yellow piece)
and returns the updated grid after the player’s turn.
If the chosen column is already full, return an error message.
Note: In Python, numbering starts at 0. The first column is therefore column 0 from the indexing point of view.
Optional: Return an error message if a player tries to play in a nonexistent column or with an unauthorized color.
Expected result
= solutions.initialize_grid() # Initialization
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="R") # 1st turn
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=5, disc_color="Y") # 2nd turn
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="R") # 3rd turn
grid_solution solutions.display_grid(grid_solution)
Your turn!
def make_move(grid, column_to_play, disc_color):
= copy.deepcopy(grid) # Avoid modifying the initial grid
new_grid # Your code here
return new_grid
# Checking the result
= initialize_grid() # Initialization
grid = make_move(grid=grid, column_to_play=2, disc_color="R") # 1st turn
grid = make_move(grid=grid, column_to_play=5, disc_color="Y") # 2nd turn
grid = make_move(grid=grid, column_to_play=2, disc_color="R") # 3rd turn
grid display_grid(grid)
Detection of a victory (horizontal)
Now that it is possible to actually play Connect 4, we need to detect a victory to end the current game. To do this, we will simplify the problem by breaking it down as much as possible.
First, we focus on detecting a horizontal victory. To do this, we will use two functions:
a
check_row_victory
function that takes a Connect 4 row as input (i.e., a list of size 7) and returnsTrue
if 4 consecutive pieces of the same color are found on the row, andFalse
otherwisea
check_horizontal_victory
function that takes a complete grid as input and returnsTrue
if any row in the grid meets the previous condition, andFalse
otherwise
Expected result
# Detection of a (horizontal) victory on a row
= [" ", "R", "R", "R", "Y", "Y", " "]
row1 = [" ", "R", "R", "R", "R", "Y", " "]
row2
print(solutions.check_row_victory(row1)) # Returns False
print() # New line
print(solutions.check_row_victory(row2)) # Returns True
# Detection of a (horizontal) victory on a grid
= solutions.initialize_grid() # Initialization
grid_solution print(solutions.check_horizontal_victory(grid_solution)) # Returns False
print() # New line
= solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="R")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=3, disc_color="R")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=4, disc_color="R")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=5, disc_color="R")
grid_solution
solutions.display_grid(grid_solution)print() # New line
print(solutions.check_horizontal_victory(grid_solution)) # Returns True
Your turn!
def check_row_victory(row):
# Your code here
# Checking the result
= [" ", "R", "R", "R", "Y", "R", " "]
row1 = [" ", "R", "R", "R", "R", "Y", " "]
row2
print(check_row_victory(row1)) # Returns False
print(check_row_victory(row2)) # Returns True
def check_horizontal_victory(grid):
# Your code here
# Checking the result
= initialize_grid() # Initialization
grid print(check_horizontal_victory(grid)) # Returns False
= make_move(grid=grid, column_to_play=2, disc_color="R")
grid = make_move(grid=grid, column_to_play=3, disc_color="R")
grid = make_move(grid=grid, column_to_play=4, disc_color="R")
grid = make_move(grid=grid, column_to_play=5, disc_color="R")
grid
display_grid(grid)print(check_horizontal_victory(grid)) # Returns True
Detection of a victory (vertical)
Now, we focus on detecting a vertical victory. Compared to the previous situation, the difficulty is that we cannot directly loop over the columns. We will therefore build a check_vertical_victory
function that, for each column:
retrieves the elements of the column in a list
applies the
check_row_victory
function to this list to check for 4 consecutive pieces of the same color in the considered column
Expected result
# Detection of a (vertical) victory on a grid
= solutions.initialize_grid() # Initialization
grid_solution print(solutions.check_vertical_victory(grid_solution)) # Returns False
print() # New line
= solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution = solutions.make_move(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution
solutions.display_grid(grid_solution)print() # New line
print(solutions.check_vertical_victory(grid_solution)) # Returns True
Your turn!
def check_vertical_victory(grid):
# Your code here
# Checking the result
= initialize_grid() # Initialization
grid print(check_vertical_victory(grid)) # Returns False
print() # New line
= make_move(grid=grid, column_to_play=2, disc_color="Y")
grid = make_move(grid=grid, column_to_play=2, disc_color="Y")
grid = make_move(grid=grid, column_to_play=2, disc_color="Y")
grid = make_move(grid=grid, column_to_play=2, disc_color="Y")
grid
display_grid(grid)print() # New line
print(check_vertical_victory(grid)) # Returns True
End of game
In our simplified version of Connect 4, we can now declare the end of the game: as soon as a horizontal or vertical victory is detected.
We will first create a check_victory
function that takes the grid as input and returns True
if a horizontal or vertical victory is detected, and False
otherwise.
Ideally, we would not want to manually test after each move if the game is over to limit code duplication. We will therefore create a make_move_and_check_victory
function that:
takes the same inputs as the
make_move
functioncalls the
make_move
function to make the movetests after the move if a victory is detected via the
check_victory
function. If a victory is detected, the function prints “END OF GAME”.
Expected result
= solutions.initialize_grid() # Initialization
grid_solution print("Turn 1")
= solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution print("Turn 2")
= solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution print("Turn 3")
= solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
grid_solution print("Turn 4")
= solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y") grid_solution
Your turn!
def check_victory(grid):
# Your code here
def make_move_and_check_victory(grid, column_to_play, disc_color):
= copy.deepcopy(grid)
grid # Your code here
return grid
# Checking the result
= initialize_grid() # Initialization
grid print("Turn 1")
= make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
grid print("Turn 2")
= make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
grid print("Turn 3")
= make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
grid print("Turn 4")
= make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y") grid