# Arup Guha
# 6/18/2024
# Connect Four Class for Day 12 of SI@UCF Camp (more complete)

import time
import pygame, sys
from pygame.locals import *

# This is to get the fonts to work.
pygame.init()

class connect4:

    # Used to keep track of dropping rate for visual.
    dropping = False
    dropC = -1
    dropR = -1
    dropX = 0
    dropY = 0
    
    # Will store where the win was so we can draw a black line.
    ptStart = (-1,-1)
    ptEnd = (-1,-1)

    # These are more like constants.
    # white means game is still going, black means it's a tie.
    PIECES = ["red","yellow","white","black"]
    DR = [0,1,1,1]
    DC = [1,-1,0,1]
    DY = 5
    COLS = 7
    ROWS = 6
    SCREEN_W = 625
    SCREEN_H = 625
    SQ_SIZE = 75
    font = pygame.font.SysFont(None, 36)
    quitbutton = pygame.Rect(500, 550, 100, 50)

    def __init__(self):

        # Set board to be empty board.
        self.board = []
        for i in range(self.COLS):
            self.board.append([])
            for j in range(self.ROWS):
                self.board[i].append(self.PIECES[2])

        # Set up whose turn it is and how many tokens in each column.
        self.turn = 0
        self.numused = []
        for i in range(self.COLS):
            self.numused.append(0)
        self.winner = self.PIECES[2]

        self.DISPLAYSURF = pygame.display.set_mode((self.SCREEN_W, self.SCREEN_H))
        
        
    # Return true if it's possible to move in column col.
    def isPossible(self,col):
        return self.numused[col] < self.ROWS

    # Assumes that it's valid to play in column col, executes the move.
    def executeTurn(self,col):

        # You can't do this, so we just return false without doing anything.
        if not self.isPossible(col):
            return False

        # Place the piece in column col, and it MUST GO in row numused[col].
        self.board[col][self.numused[col]] = self.PIECES[self.turn]

        # Only way I could think of executing a dropping piece.
        self.dropping = True
        tmp = self.mapCenter(col,self.ROWS)
        self.dropX = tmp[0]
        self.dropY = tmp[1]
        self.dropR = self.numused[col]
        self.dropC = col

        # Make it the other person's turn.
        self.turn = 1 - self.turn

        # One more piece is in this column.
        self.numused[col] += 1

        # Update the game status.
        self.winner = self.status()

        # We made the move.
        return True

    # Will draw this board onto its display surface.
    def drawgame(self):

        self.DISPLAYSURF.fill("blue")

        # Loops through the board structure.
        for col in range(self.COLS):

            # Trick to get pixel locations below board.
            tmppt = self.mapTopLeft(col,-1)
            self.draw(str(col+1), "black", tmppt[0]+30, tmppt[1])

            # Go through each slot in this column.
            for row in range(self.ROWS):

                # I just draw a bounding square and the appropriate circle.
                pt = self.mapTopLeft(col,row)
                pygame.draw.rect(self.DISPLAYSURF, "black", (pt[0], pt[1], self.SQ_SIZE, self.SQ_SIZE), 1)

                # Draw all pieces but a dropping piece.
                if not (self.dropping and col == self.dropC and row == self.dropR):
                    pygame.draw.circle(self.DISPLAYSURF, self.board[col][row], self.mapCenter(col,row), self.SQ_SIZE//2) 
                else:
                    pygame.draw.circle(self.DISPLAYSURF, self.PIECES[2], self.mapCenter(col,row), self.SQ_SIZE//2)

        # Now let's draw the dropping piece.
        if self.dropping:

            self.dropY += self.DY
            endPt = self.mapCenter(self.dropC, self.dropR)

            # Time to stop.
            if self.dropY >= endPt[1]:
                self.dropY = endPt[1]
                self.dropping = False
                self.dropC = -1
                self.dropR = -1

            pygame.draw.circle(self.DISPLAYSURF, self.board[self.dropC][self.dropR], (self.dropX, self.dropY), self.SQ_SIZE//2)
                    

    # We only draw this at the end of the game.
    def drawend(self):

        # End message.
        if self.winner == "red":
            self.draw("Red wins the game!!!", "red", 100, self.SCREEN_H-75)

        elif self.winner == "yellow":
            self.draw("Yellow wins the game!!!", "yellow", 100, self.SCREEN_H-75)

        else:
            self.draw("Game is a tie!!!", "black", 100, self.SCREEN_H-75)

        # Line showing a winning path.
        pygame.draw.line(self.DISPLAYSURF, "black", self.ptStart, self.ptEnd)

        # Quit rectangle.
        pygame.draw.rect(self.DISPLAYSURF, "white", self.quitbutton)
        self.draw("Quit", "black", 510, 560)

    # We only draw this at the end of the game.
    def drawerror(self):

        # Store 1-based valid places to move.
        validlist = []
        for i in range(self.COLS):
            if self.isPossible(i):
                validlist.append(i+1)

        # Build the string.
        s = "Invalid move. Choose one of these: "
        for i in range(len(validlist)-1):
            s = s + str(validlist[i])+", "
        s = s + str(validlist[-1])

        # Draw it.
        self.draw(s, "black", 25, self.SCREEN_H-75)

    # Function to put text onto the screen.
    def draw(self, text, color, x, y):
        
        # Draw text on a new Surface. Title, antialias and color are used.
        text = self.font.render(text, 1, color)

        # Returns a new rectangle covering the entire surface.
        # This rectangle will always start at (0, 0) with a width and height the same size as the image.
        textrect = text.get_rect()

        # Sets location of text.
        textrect.topleft = (x, y)

        # Go ahead and draw it to the surface.
        self.DISPLAYSURF.blit(text, textrect)

        
    # Returns true iff col c, row r is within the playing board.
    def inbounds(self,c,r):
        return c>=0 and c<self.COLS and r>=0 and r<self.ROWS

    # Returns "red" if team red has won, "yellow" if team yellow has won
    # or "white" if the game should continue, or "black" if it's a tie.
    def status(self):

        # Will set to true if we have a valid square to play in.
        hasBlank = False

        # Go through columns.
        for c in range(self.COLS):

            # There's a place to play.
            if self.numused[c] < self.ROWS:
                hasBlank = True

            # Go through rows in this column
            for r in range(self.ROWS):
                temp = self.winFrom(c,r)

                # See if there's a winner here.
                if temp != self.PIECES[2]:
                    return self.board[c][r]

        # Play on!
        if hasBlank:
            return self.PIECES[2]

        # It's a tie!
        else:
            return self.PIECES[3]
        
    # Returns the piece that wins from location col c, row r.
    # "white" is returned if there is no winner from this position.
    def winFrom(self,c,r):

        # Not a valid square to check from because it's empty.
        if self.board[c][r] == self.PIECES[2]:
            return self.PIECES[2]

        # Try each direction.
        for d in range(len(self.DC)):

            # Get the winner from col c, row r, in direction d.
            winner = self.winFromDir(c,r,d)

            # Winner winner chicken dinner.
            if winner != self.PIECES[2]:
                return winner

        # This means there's no winner from this square.
        return self.PIECES[2]

    # Returns the pixel point that is the top left of column c, row r of the board.
    def mapTopLeft(self,c,r):
        return (50+self.SQ_SIZE*c, self.SCREEN_H - 200 - self.SQ_SIZE*r)

    # Returns the pixel point that is the center of column c, row r of the board.
    def mapCenter(self,c,r):
        return (50+self.SQ_SIZE*c + self.SQ_SIZE//2, self.SCREEN_H - 200 - self.SQ_SIZE*r + self.SQ_SIZE//2)

    # Returns the piece that wins from location col c, row r in
    # direction d. "white" is returned if a player doesn't win.
    # Call this method only if there's a real piece at col c, row r.
    def winFromDir(self,c,r,d):

        # Check the next three pieces in this group.
        for i in range(1, 4):

            # Next coordinate.
            nC = c + i*self.DC[d]
            nR = r + i*self.DR[d]

            # Can't be a winner if we go off the board.
            if not self.inbounds(nC, nR):
                return self.PIECES[2]

            # Doesn't match first piece in run.
            if self.board[nC][nR] != self.board[c][r]:
                return self.PIECES[2]

        # Here is our winner! (Store it and return)
        self.ptStart = self.mapCenter(c,r)
        self.ptEnd = self.mapCenter(c+3*self.DC[d],r+3*self.DR[d])
        return self.board[c][r]
