A game of connect four with an AI to play against.
import numpy as np
import itertools
import pygame
import random
import math

from variables import ROW_COUNT, COLUMN_COUNT, size, colors, SQUARESIZE, RADIUS, height, width, PLAYER_PIECE, AI_PIECE

# Creating board
def create_board():
    return np.zeros((ROW_COUNT,COLUMN_COUNT))

# Creating the board
board = create_board()

# Checking if the top row of a selected column is empty or not
def is_valid_location(board, col):
    return board[ROW_COUNT-1][col] == 0

# Getting the lowest empty slot of the selected column
def get_next_open_row(board, col):
    for slot in range(ROW_COUNT):
        if board[slot][col] == 0:
            return slot

# Dropping the piece in the board
def drop_piece(board, row, col, piece):
    board[row][col] = piece

# Checking if the game is over
def game_over_check(board, piece):
    
    # Checking horizontal win
    for c, r in itertools.product(range(COLUMN_COUNT - 3), range(ROW_COUNT)):
        if board[r][c] == piece and board[r][c+1] == piece and board[r][c+2] == piece and board[r][c+3] == piece:
            return True

    # Checking vertical win
    for c, r in itertools.product(range(COLUMN_COUNT), range(ROW_COUNT - 3)):
        if board[r][c] == piece and board[r+1][c] == piece and board[r+2][c] == piece and board[r+3][c] == piece:
            return True

    # Checking positive slop diagonal win
    for c, r in itertools.product(range(COLUMN_COUNT - 3), range(ROW_COUNT - 3)):
        if board[r][c] == piece and board[r+1][c+1] == piece and board[r+2][c+2] == piece and board[r+3][c+3] == piece:
            return True
        
    # Checking negative slop diagonal win
    for c, r in itertools.product(range(COLUMN_COUNT - 3), range(3, ROW_COUNT)):
        if board[r][c] == piece and board[r-1][c+1] == piece and board[r-2][c+2] == piece and board[r-3][c+3] == piece:
            return True

# Setting screen size
screen = pygame.display.set_mode(size)

# Drawing board graphics
def draw_board(board):

    for c, r in itertools.product(range(COLUMN_COUNT), range(ROW_COUNT)):
        pygame.draw.rect(screen, colors["VERDIGRIS"], (c*SQUARESIZE, r*SQUARESIZE+SQUARESIZE, SQUARESIZE, SQUARESIZE))
        pygame.draw.circle(screen, colors["CHARCOAL"], (int(c*SQUARESIZE+SQUARESIZE/2), int(r*SQUARESIZE+SQUARESIZE+SQUARESIZE/2)), RADIUS)
        
    for c, r in itertools.product(range(COLUMN_COUNT), range(ROW_COUNT)):
        if board[r][c] == PLAYER_PIECE:
            pygame.draw.circle(screen, colors["CERISE"], (int(c*SQUARESIZE+SQUARESIZE/2), height-int(r*SQUARESIZE+SQUARESIZE/2)), RADIUS)
        elif board[r][c] == AI_PIECE: 
            pygame.draw.circle(screen, colors["ICTERINE"], (int(c*SQUARESIZE+SQUARESIZE/2), height-int(r*SQUARESIZE+SQUARESIZE/2)), RADIUS)

    pygame.display.update()
    
    

src/functions.py
gdsc-ipsacademy/Connect-Four-Game
import pygame
import sys
import math
import random

from variables import ROW_COUNT, COLUMN_COUNT, SQUARESIZE, size, RADIUS, colors, height, width, PLAYER, AI, \
    PLAYER_PIECE, AI_PIECE
from functions import create_board, is_valid_location, get_next_open_row, drop_piece, game_over_check, draw_board, \
    board, screen
from score_ai import pick_best_move
from minmax_ai import minimax
from ui_components import Button 
from ui_components import ai_move, self_move, ai_wins_sound, player_wins_sound 

class ConnectFour:
    def __init__(self):
        pygame.init()
        pygame.mixer.init() #added to initialize sound 
        self.game_over = False
        self.turn = random.randint(PLAYER, AI)
        self.board = create_board()
        self.myfont = pygame.font.SysFont("monospace", 80)
        button_width = 250
        button_height = 100
        padding = 20
        restart_button_y = height // 2
        quit_button_y = restart_button_y + button_height + padding
        center_x = width // 2 - button_width // 2
        self.quit_button = Button((255, 0, 0), center_x, quit_button_y, button_width, button_height, 'Quit')
        self.restart_button = Button((0, 255, 0), center_x, restart_button_y, button_width, button_height, 'Restart')
        pygame.display.set_caption("Connect Four")
        draw_board(self.board)
        pygame.display.update()

    def handle_mouse_motion(self, event):
        pygame.draw.rect(screen, colors["CHARCOAL"], (0, 0, width, SQUARESIZE))
        posx = event.pos[0]
        if self.turn == PLAYER:
            pygame.draw.circle(screen, colors["CERISE"], (posx, int(SQUARESIZE / 2)), RADIUS)
        pygame.display.update()

    def handle_mouse_button_down(self, event):
        pygame.draw.rect(screen, colors["CHARCOAL"], (0, 0, width, SQUARESIZE))
        posx = event.pos[0]
        if self.turn == PLAYER:
            col = int(math.floor(posx / SQUARESIZE))
            if is_valid_location(self.board, col):
                self._extracted_from_ai_move_7(col, PLAYER_PIECE, "You win!! ^_^")
                self.turn ^= 1
                self_move.play()
                self.render_thinking("Thinking...")
                draw_board(self.board)
        if self.game_over:
            if self.quit_button.is_over((posx, event.pos[1])):
                sys.exit()
            elif self.restart_button.is_over((posx, event.pos[1])):
                self.__init__()

    def ai_move(self):
        col, minimax_score = minimax(self.board, 6, -math.inf, math.inf, True)
        if is_valid_location(self.board, col):
            self.clear_label()
            self._extracted_from_ai_move_7(col, AI_PIECE, "AI wins!! :[")
            draw_board(self.board)
            self.turn ^= 1
            ai_move.play()


    # TODO Rename this here and in `handle_mouse_button_down` and `ai_move`
    def _extracted_from_ai_move_7(self, col, arg1, arg2):
        row = get_next_open_row(self.board, col)
        drop_piece(self.board, row, col, arg1)
        if game_over_check(self.board, arg1):
            self.display_winner(arg2)

    def display_winner(self, message):
        if message == "AI wins!! :[":
            ai_wins_sound.play()
        elif message == "You win!! ^_^":
            player_wins_sound.play()
        label = self.myfont.render(message, 1, colors["MISTYROSE"])
        screen.blit(label, (40, 10))
        self.game_over = True

    def handle_game_over(self):
        while self.game_over:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                if event.type == pygame.MOUSEBUTTONDOWN:
                    posx, posy = event.pos
                    if self.quit_button.is_over((posx, posy)):
                        sys.exit()
                    elif self.restart_button.is_over((posx, posy)):
                        self.__init__()
                        self.game_start()  # You need to call game_start again to restart the game loop

            self.quit_button.draw(screen, (0, 0, 0))
            self.restart_button.draw(screen, (0, 0, 0))
            pygame.display.update()

    def game_start(self):
        while not self.game_over:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    sys.exit()
                if event.type == pygame.MOUSEMOTION:
                    self.handle_mouse_motion(event)
                if event.type == pygame.MOUSEBUTTONDOWN:
                    self.handle_mouse_button_down(event)
            if self.turn == AI and not self.game_over:
                self.ai_move()
            if self.game_over:
                self.handle_game_over()
    
    def clear_label(self):
        pygame.draw.rect(screen, colors["CHARCOAL"], (0, 0, width, SQUARESIZE))


    def render_thinking(self, text):
        self.clear_label()
        label = pygame.font.SysFont("monospace", 60).render(text, 1, colors["MISTYROSE"])
        screen.blit(label, (40, 10))

if __name__ == "__main__":
    game = ConnectFour()
    game.game_start()

src/game.py
gdsc-ipsacademy/Connect-Four-Game
import math
import random

from variables import ROW_COUNT, COLUMN_COUNT, PLAYER_PIECE, AI_PIECE
from functions import is_valid_location, game_over_check, get_next_open_row, drop_piece
from score_ai import score_position

# Getting valid locations for AI
def get_valid_locations(board):
    return [c for c in range(COLUMN_COUNT) if is_valid_location(board, c)]

# Checking for terminal nodes
def is_terminal_node(board):
    return game_over_check(board, PLAYER_PIECE) or game_over_check(board, AI_PIECE) or len(get_valid_locations(board)) == 0

# Implimenting minimax algorithm
def minimax(board, depth, alpha, beta, maximizing_player):
    valid_locations = get_valid_locations(board)

    if isTerminal := is_terminal_node(board):
        if game_over_check(board, AI_PIECE):
            return (None, math.inf)
        elif game_over_check(board, PLAYER_PIECE):
            return (None, -math.inf)
        else: 
            return (None, 0)
    elif depth == 0:
        return (None, score_position(board, AI_PIECE))

    if maximizing_player:
        value = -math.inf
        column = random.choice(valid_locations)

        for c in valid_locations:
            r = get_next_open_row(board, c)
            temp_board = board.copy()
            drop_piece(temp_board, r, c, AI_PIECE)
            new_score = minimax(temp_board, depth - 1, alpha, beta, False)[1]

            if new_score > value:
                value = new_score
                column = c
            
            alpha = max(alpha, value)

            if alpha >= beta:
                break

    # Minimizing player
    else:
        value = math.inf
        column = random.choice(valid_locations)

        for c in valid_locations:
            r = get_next_open_row(board, c)
            temp_board = board.copy()
            drop_piece(temp_board, r, c, PLAYER_PIECE)
            new_score = minimax(temp_board, depth - 1, alpha, beta, True)[1]

            if new_score < value:
                value = new_score
                column = c

            beta = min(beta, value)

            if alpha >= beta:
                break
            
    return column, value
src/minmax_ai.py
gdsc-ipsacademy/Connect-Four-Game
import itertools

from variables import ROW_COUNT, COLUMN_COUNT, PLAYER_PIECE, AI_PIECE, WINDOW_LENGTH, EMPTY

# Evaluating scores of connections
def evaluate_window(window, piece):

    score = 0
    opponent_piece = PLAYER_PIECE
    if piece == PLAYER_PIECE:
        opponent_piece = AI_PIECE
    
    if window.count(piece) == 4:
        score += 100
    elif window.count(piece) == 3 and window.count(EMPTY) == 1:
        score += 50
    elif window.count(piece) == 2 and window.count(EMPTY) == 2:
        score += 10

    if window.count(opponent_piece) == 3 and window.count(EMPTY) == 1:
        score -= 75    
    elif window.count(opponent_piece) == 2 and window.count(EMPTY) == 2:
        score -= 25

    return score

# Getting scores for connections
def score_position(board, piece):

    score = 0

    # Prefer center column for increased probablity of win
    center_array = [int(i) for i in list(board[ : , COLUMN_COUNT//2])]
    center_count = center_array.count(piece)
    score += center_count*10

    # Horizontal score
    for r in range(ROW_COUNT):
        row_array = [int(i) for i in list(board[r , : ])]
        for c in range(COLUMN_COUNT - 3):
            window = row_array[c : c + WINDOW_LENGTH]
            score += evaluate_window(window, piece)

    # Vertical score
    for c in range(COLUMN_COUNT):
        col_array = [int(i) for i in list(board[ : , c])]
        for r in range(ROW_COUNT):
            window = col_array[r : r + WINDOW_LENGTH]
            score += evaluate_window(window, piece)

    # Positive slope diagonal score
    for r, c in itertools.product(range(ROW_COUNT - 3), range(COLUMN_COUNT - 3)):
        window = [board[r + i][c + i] for i in range(WINDOW_LENGTH)]
        score += evaluate_window(window, piece)

    # Negative slope diagonal score
    for r, c in itertools.product(range(ROW_COUNT - 3), range(COLUMN_COUNT - 3)):
        window = [board[r + 3 - i][c + i] for i in range(WINDOW_LENGTH)]
        score += evaluate_window(window, piece)

    return score

# Picking best moves based on scores
def pick_best_move(board, piece):

    valid_locations = get_valid_locations(board)

    best_score = -10000
    best_col = random.choice(valid_locations)

    for c in valid_locations:
        r = get_next_open_row(board, c)
        temp_board = board.copy()
        drop_piece(temp_board, r, c, piece)
        score = score_position(temp_board, piece)

        if score > best_score:
            best_score = score
            best_col = c

    return best_col
src/score_ai.py
gdsc-ipsacademy/Connect-Four-Game
#!/usr/bin/env python3
import pygame
import os
class Button:
    def __init__(self, color, x, y, width, height, text=''):
        self.color = color
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.text = text

    def draw(self, win, outline=None):
        # Call this method to draw the button on the screen
        if outline:
            pygame.draw.rect(win, outline, (self.x-2, self.y-2, self.width+4, self.height+4), 0)
        pygame.draw.rect(win, self.color, (self.x, self.y, self.width, self.height), 0)
        if self.text != '':
            font = pygame.font.SysFont('comicsans', 60)
            text = font.render(self.text, 1, (0, 0, 0))
            win.blit(text, (self.x + (self.width/2 - text.get_width()/2), self.y + (self.height/2 - text.get_height()/2)))

    def is_over(self, pos):
        # Pos is the mouse position or a tuple of (x,y) coordinates
        if self.x < pos[0] < self.x + self.width and self.y < pos[1] < self.y + self.height:
            return True
        
pygame.mixer.init()

ai_move = pygame.mixer.Sound(os.path.join('src\sound', 'AI_sound.ogg'))
self_move = pygame.mixer.Sound(os.path.join('src\sound','self_sound.ogg'))
ai_wins_sound = pygame.mixer.Sound(os.path.join('src\sound',"looser.ogg"))
player_wins_sound = pygame.mixer.Sound(os.path.join('src\sound',"winner.ogg"))
        
    

src/ui_components.py
gdsc-ipsacademy/Connect-Four-Game
# Connect four board has dimentions 6*7
ROW_COUNT = 6
COLUMN_COUNT = 7

# Global variables for pygame
SQUARESIZE = 100

width = COLUMN_COUNT * SQUARESIZE
height = (ROW_COUNT + 1) * SQUARESIZE
size = (width, height)

RADIUS = int(SQUARESIZE / 2 - 5)

# Dictionary for colors
colors = {
    "VERDIGRIS": (23, 190, 187),
    "CHARCOAL": (47, 72, 88),
    "CERISE": (236, 64, 103),
    "ICTERINE": (248, 242, 114),
    "MISTYROSE": (250, 216, 214),
}

# Values for player and AI turns
PLAYER = 0
AI = 1

# Values for player and AI piece
PLAYER_PIECE = 1
AI_PIECE = 2

WINDOW_LENGTH = 4
EMPTY = 0

src/variables.py
gdsc-ipsacademy/Connect-Four-Game