import copy
import solutionsProject 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
grid_solution = solutions.initialize_grid()
grid_solutionprint(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
grid = initialize_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:
l = ["a", "b", "c", "d", "e"]
l_join = "START " + ", ".join(l) + " END"
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
grid_solution = 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
solutions.display_grid(grid_solution)Your turn!
def make_move(grid, column_to_play, disc_color):
new_grid = copy.deepcopy(grid) # Avoid modifying the initial grid
# Your code here
return new_grid# Checking the result
grid = 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
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_victoryfunction that takes a Connect 4 row as input (i.e., a list of size 7) and returnsTrueif 4 consecutive pieces of the same color are found on the row, andFalseotherwisea
check_horizontal_victoryfunction that takes a complete grid as input and returnsTrueif any row in the grid meets the previous condition, andFalseotherwise
Expected result
# Detection of a (horizontal) victory on a row
row1 = [" ", "R", "R", "R", "Y", "Y", " "]
row2 = [" ", "R", "R", "R", "R", "Y", " "]
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
grid_solution = solutions.initialize_grid() # Initialization
print(solutions.check_horizontal_victory(grid_solution)) # Returns False
print() # New line
grid_solution = 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")
solutions.display_grid(grid_solution)
print() # New line
print(solutions.check_horizontal_victory(grid_solution)) # Returns TrueYour turn!
def check_row_victory(row):
# Your code here# Checking the result
row1 = [" ", "R", "R", "R", "Y", "R", " "]
row2 = [" ", "R", "R", "R", "R", "Y", " "]
print(check_row_victory(row1)) # Returns False
print(check_row_victory(row2)) # Returns Truedef check_horizontal_victory(grid):
# Your code here# Checking the result
grid = initialize_grid() # Initialization
print(check_horizontal_victory(grid)) # Returns False
grid = 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")
display_grid(grid)
print(check_horizontal_victory(grid)) # Returns TrueDetection 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_victoryfunction 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
grid_solution = solutions.initialize_grid() # Initialization
print(solutions.check_vertical_victory(grid_solution)) # Returns False
print() # New line
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.make_move(grid=grid_solution, column_to_play=2, disc_color="Y")
solutions.display_grid(grid_solution)
print() # New line
print(solutions.check_vertical_victory(grid_solution)) # Returns TrueYour turn!
def check_vertical_victory(grid):
# Your code here# Checking the result
grid = initialize_grid() # Initialization
print(check_vertical_victory(grid)) # Returns False
print() # New line
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 = make_move(grid=grid, column_to_play=2, disc_color="Y")
display_grid(grid)
print() # New line
print(check_vertical_victory(grid)) # Returns TrueEnd 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_movefunctioncalls the
make_movefunction to make the movetests after the move if a victory is detected via the
check_victoryfunction. If a victory is detected, the function prints “END OF GAME”.
Expected result
grid_solution = solutions.initialize_grid() # Initialization
print("Turn 1")
grid_solution = solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
print("Turn 2")
grid_solution = solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
print("Turn 3")
grid_solution = solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")
print("Turn 4")
grid_solution = solutions.make_move_and_check_victory(grid=grid_solution, column_to_play=2, disc_color="Y")Your turn!
def check_victory(grid):
# Your code heredef make_move_and_check_victory(grid, column_to_play, disc_color):
grid = copy.deepcopy(grid)
# Your code here
return grid# Checking the result
grid = initialize_grid() # Initialization
print("Turn 1")
grid = make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
print("Turn 2")
grid = make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
print("Turn 3")
grid = make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")
print("Turn 4")
grid = make_move_and_check_victory(grid=grid, column_to_play=2, disc_color="Y")