# Import Required Modules

import random, time, pygame, sys
from pygame.locals import *

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

FPS = 25

WINDOWWIDTH = 640

WINDOWHEIGHT = 480

BOXSIZE = 20

BOARDWIDTH = 10

BOARDHEIGHT = 20

BLANK = '.'

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

MOVESIDEWAYSFREQ = 0.15

MOVEDOWNFREQ = 0.1

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * BOXSIZE) / 2)

TOPMARGIN = WINDOWHEIGHT - (BOARDHEIGHT * BOXSIZE) - 5

#               R    G    B

WHITE       = (255, 255, 255)

GRAY        = (185, 185, 185)

BLACK       = (  0,   0,   0)

RED         = (155,   0,   0)

LIGHTRED    = (175,  20,  20)

GREEN       = (  0, 155,   0)

LIGHTGREEN  = ( 20, 175,  20)

BLUE        = (  0,   0, 155)

NAVY        = (  0,   0, 128)

LIGHTBLUE   = ( 20,  20, 175)

YELLOW      = (155, 155,   0)

LIGHTYELLOW = (175, 175,  20)

 

BORDERCOLOR = BLUE

BGCOLOR = BLACK

TEXTCOLOR = WHITE

TEXTSHADOWCOLOR = GRAY


COLORS      = (     BLUE,      GREEN,      RED,      YELLOW)

LIGHTCOLORS = (LIGHTBLUE, LIGHTGREEN, LIGHTRED, LIGHTYELLOW)

assert len(COLORS) == len(LIGHTCOLORS) # each color must have light color
 

TEMPLATEWIDTH = 5

TEMPLATEHEIGHT = 5

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# S - Shape Block

S_SHAPE_TEMPLATE = [['.....',  

                     '.....',

                     '..OO.',

                     '.OO..',

                     '.....'],

                    ['.....',

                     '..O..',

                     '..OO.',

                     '...O.',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# Z - Shape Block

Z_SHAPE_TEMPLATE = [['.....',

                     '.....',

                     '.OO..',

                     '..OO.',

                     '.....'],

                    ['.....',

                     '..O..',

                     '.OO..',

                     '.O...',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# I - Shape Block

I_SHAPE_TEMPLATE = [['..O..',

                     '..O..',

                     '..O..',

                     '..O..',

                     '.....'],

                    ['.....',

                     '.....',

                     'OOOO.',

                     '.....',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# O - Shape Block

O_SHAPE_TEMPLATE = [['.....',

                     '.....',

                     '.OO..',

                     '.OO..',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 


# J - Shape Block

J_SHAPE_TEMPLATE = [['.....',

                     '.O...',

                     '.OOO.',

                     '.....',

                     '.....'],

                    ['.....',

                     '..OO.',

                     '..O..',

                     '..O..',

                     '.....'],

                    ['.....',

                     '.....',

                     '.OOO.',

                     '...O.',

                     '.....'],

                    ['.....',

                     '..O..',

                     '..O..',

                     '.OO..',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# L - Shape Block

L_SHAPE_TEMPLATE = [['.....',

                     '...O.',

                     '.OOO.',

                     '.....',

                     '.....'],

                    ['.....',

                     '..O..',

                     '..O..',

                     '..OO.',

                     '.....'],

                    ['.....',

                     '.....',

                     '.OOO.',

                     '.O...',

                     '.....'],

                    ['.....',

                     '.OO..',

                     '..O..',

                     '..O..',

                     '.....']]

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

# T - Shape Block

T_SHAPE_TEMPLATE = [['.....',

                     '..O..',

                     '.OOO.',

                     '.....',

                     '.....'],

                    ['.....',

                     '..O..',

                     '..OO.',

                     '..O..',

                     '.....'],

                    ['.....',

                     '.....',

                     '.OOO.',

                     '..O..',

                     '.....'],

                    ['.....',

                     '..O..',

                     '.OO..',

                     '..O..',

                     '.....']]

#---------------------------------------------------------------------------------------------------------------------------------------------------------------


PIECES = {'S': S_SHAPE_TEMPLATE,

          'Z': Z_SHAPE_TEMPLATE,

          'J': J_SHAPE_TEMPLATE,

          'L': L_SHAPE_TEMPLATE,

          'I': I_SHAPE_TEMPLATE,

          'O': O_SHAPE_TEMPLATE,

          'T': T_SHAPE_TEMPLATE}

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def main():  # Main Function

    global FPSCLOCK, DISPLAYSURF, BASICFONT, BIGFONT

    pygame.init()

    FPSCLOCK = pygame.time.Clock()

    DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

    BASICFONT = pygame.font.Font('freesansbold.ttf', 18)

    BIGFONT = pygame.font.Font('freesansbold.ttf', 100)

    pygame.display.set_caption('Tetromino')

 

    showTextScreen('Tetromino')

    while True: # game loop

        if random.randint(0, 1) == 0:

            pygame.mixer.music.load('tetrisb.mid')

        else:

            pygame.mixer.music.load('tetrisc.mid')

        pygame.mixer.music.play(-1, 0.0)

        runGame()

        pygame.mixer.music.stop()

        showTextScreen('Game Over')

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def runGame():

    # setup variables for the start of the game

    board = getBlankBoard()

    lastMoveDownTime = time.time()

    lastMoveSidewaysTime = time.time()

    lastFallTime = time.time()

    movingDown = False # note: there is no movingUp variable

    movingLeft = False

    movingRight = False

    score = 0

    level, fallFreq = calculateLevelAndFallFreq(score)

 

    fallingPiece = getNewPiece()

    nextPiece = getNewPiece()

 

    while True: # game loop

        if fallingPiece == None:

            # No falling piece in play, so start a new piece at the top

            fallingPiece = nextPiece

            nextPiece = getNewPiece()

            lastFallTime = time.time() # reset lastFallTime

 

            if not isValidPosition(board, fallingPiece):

                return # can't fit a new piece on the board, so game over

        checkForQuit()

        for event in pygame.event.get(): # event handling loop

            if event.type == KEYUP:

                if (event.key == K_p):

                    # Pausing the game

                    DISPLAYSURF.fill(BGCOLOR)

                    pygame.mixer.music.stop()

                    showTextScreen('Paused') # pause until a key press

                    pygame.mixer.music.play(-1, 0.0)

                    lastFallTime = time.time()

                    lastMoveDownTime = time.time()

                    lastMoveSidewaysTime = time.time()

                elif (event.key == K_LEFT or event.key == K_a):

                    movingLeft = False

                elif (event.key == K_RIGHT or event.key == K_d):

                    movingRight = False

                elif (event.key == K_DOWN or event.key == K_s):

                    movingDown = False

 

            elif event.type == KEYDOWN:

                # moving the piece sideways

                if (event.key == K_LEFT or event.key == K_a) and isValidPosition(board, fallingPiece, adjX=-1):

                    fallingPiece['x'] -= 1

                    movingLeft = True

                    movingRight = False

                    lastMoveSidewaysTime = time.time()

 

                elif (event.key == K_RIGHT or event.key == K_d) and isValidPosition(board, fallingPiece, adjX=1):

                    fallingPiece['x'] += 1

                    movingRight = True

                    movingLeft = False

                    lastMoveSidewaysTime = time.time()

 

                # rotating the piece (if there is room to rotate)

                elif (event.key == K_UP or event.key == K_w):

                    fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

                    if not isValidPosition(board, fallingPiece):

                        fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])

                elif (event.key == K_q): # rotate the other direction

                    fallingPiece['rotation'] = (fallingPiece['rotation'] - 1) % len(PIECES[fallingPiece['shape']])

                    if not isValidPosition(board, fallingPiece):

                        fallingPiece['rotation'] = (fallingPiece['rotation'] + 1) % len(PIECES[fallingPiece['shape']])

 

                # making the piece fall faster with the down key

                elif (event.key == K_DOWN or event.key == K_s):

                    movingDown = True

                    if isValidPosition(board, fallingPiece, adjY=1):

                        fallingPiece['y'] += 1

                    lastMoveDownTime = time.time()

 

                # move the current piece all the way down

                elif event.key == K_SPACE:

                    movingDown = False

                    movingLeft = False

                    movingRight = False

                    for i in range(1, BOARDHEIGHT):

                        if not isValidPosition(board, fallingPiece, adjY=i):

                            break

                    fallingPiece['y'] += i - 1

        

        if (movingLeft or movingRight) and time.time() - lastMoveSidewaysTime > MOVESIDEWAYSFREQ: # handle moving the piece because of user input


            if movingLeft and isValidPosition(board, fallingPiece, adjX=-1):

                fallingPiece['x'] -= 1
                

            elif movingRight and isValidPosition(board, fallingPiece, adjX=1):

                fallingPiece['x'] += 1

            lastMoveSidewaysTime = time.time()

            

        if movingDown and time.time() - lastMoveDownTime > MOVEDOWNFREQ and isValidPosition(board, fallingPiece, adjY=1):

            fallingPiece['y'] += 1

            lastMoveDownTime = time.time()

 

        

        if time.time() - lastFallTime > fallFreq:  # let the piece fall if it is time to fall

           

            if not isValidPosition(board, fallingPiece, adjY=1):   # see if the piece has landed

               

                addToBoard(board, fallingPiece)   # falling piece has landed, set it on the board

                score += removeCompleteLines(board)

                level, fallFreq = calculateLevelAndFallFreq(score)

                fallingPiece = None
                

            else:  # piece did not land, just move the piece down

                fallingPiece['y'] += 1 

                lastFallTime = time.time()

        

        DISPLAYSURF.fill(BGCOLOR) # drawing everything on the screen

        drawBoard(board)

        drawStatus(score, level)

        drawNextPiece(nextPiece)

        if fallingPiece != None:

            drawPiece(fallingPiece)

 

        pygame.display.update()

        FPSCLOCK.tick(FPS)

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def makeTextObjs(text, font, color):

    surf = font.render(text, True, color)

    return surf, surf.get_rect()

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def terminate():

    pygame.quit()

    sys.exit()

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def checkForKeyPress():

    # Go through event queue looking for a KEYUP event.

    # Grab KEYDOWN events to remove them from the event queue.

    checkForQuit()

    for event in pygame.event.get([KEYDOWN, KEYUP]):

        if event.type == KEYDOWN:

            continue

        return event.key

    return None

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def showTextScreen(text):

    # This function displays large text in the

    # center of the screen until a key is pressed.

    # Draw the text drop shadow

    titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTSHADOWCOLOR)

    titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

    DISPLAYSURF.blit(titleSurf, titleRect)

    # Draw the text

    titleSurf, titleRect = makeTextObjs(text, BIGFONT, TEXTCOLOR)

    titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)

    DISPLAYSURF.blit(titleSurf, titleRect)

    # Draw the additional "Press a key to play." text.

    pressKeySurf, pressKeyRect = makeTextObjs('Press a key to play.', BASICFONT, TEXTCOLOR)

    pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)

    DISPLAYSURF.blit(pressKeySurf, pressKeyRect)

 

    while checkForKeyPress() == None:

        pygame.display.update()

        FPSCLOCK.tick()

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def checkForQuit():

    for event in pygame.event.get(QUIT): # get all the QUIT events

        terminate() # terminate if any QUIT events are present

    for event in pygame.event.get(KEYUP): # get all the KEYUP events

        if event.key == K_ESCAPE:

            terminate() # terminate if the KEYUP event was for the Esc key

        pygame.event.post(event) # put the other KEYUP event objects back

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def calculateLevelAndFallFreq(score):

    # Based on the score, return the level the player is on and

    # how many seconds pass until a falling piece falls one space.

    level = int(score / 10) + 1

    fallFreq = 0.27 - (level * 0.02)

    return level, fallFreq

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def getNewPiece():   # return a random new piece in a random rotation and color

   

    shape = random.choice(list(PIECES.keys()))

    newPiece = {'shape': shape,

                'rotation': random.randint(0, len(PIECES[shape]) - 1),

                'x': int(BOARDWIDTH / 2) - int(TEMPLATEWIDTH / 2),

                'y': -2, # start it above the board (i.e. less than 0)

                'color': random.randint(0, len(COLORS)-1)}

    return newPiece

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def addToBoard(board, piece): # fill in the board based on piece's location, shape, and rotation

    

    for x in range(TEMPLATEWIDTH):

        for y in range(TEMPLATEHEIGHT):

            if PIECES[piece['shape']][piece['rotation']][y][x] != BLANK:

                board[x + piece['x']][y + piece['y']] = piece['color']

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def getBlankBoard(): # create and return a new blank board data structure

    

    board = []

    for i in range(BOARDWIDTH):

        board.append([BLANK] * BOARDHEIGHT)

    return board

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def isOnBoard(x, y):

    return x >= 0 and x < BOARDWIDTH and y < BOARDHEIGHT

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def isValidPosition(board, piece, adjX=0, adjY=0): # Return True if the piece is within the board and not colliding
    

    for x in range(TEMPLATEWIDTH):

        for y in range(TEMPLATEHEIGHT):

            isAboveBoard = y + piece['y'] + adjY < 0

            if isAboveBoard or PIECES[piece['shape']][piece['rotation']][y][x] == BLANK:

                continue

            if not isOnBoard(x + piece['x'] + adjX, y + piece['y'] + adjY):

                return False

            if board[x + piece['x'] + adjX][y + piece['y'] + adjY] != BLANK:

                return False

    return True

#--------------------------------------------------------------------------------------------------------------------------------------------------------------- 

def isCompleteLine(board, y): # Return True if the line filled with boxes with no gaps.
    

    for x in range(BOARDWIDTH):

        if board[x][y] == BLANK:

            return False

    return True

 
#---------------------------------------------------------------------------------------------------------------------------------------------------------------
 

def removeCompleteLines(board):

    # Remove any completed lines on the board, move everything above them down, and return the number of complete lines.

    numLinesRemoved = 0

    y = BOARDHEIGHT - 1 # start y at the bottom of the board

    while y >= 0:

        if isCompleteLine(board, y):
            

            for pullDownY in range(y, 0, -1):   # Remove the line and pull boxes down by one line.

                for x in range(BOARDWIDTH):

                    board[x][pullDownY] = board[x][pullDownY-1]

            

            for x in range(BOARDWIDTH): # Set very top line to blank.

                board[x][0] = BLANK

            numLinesRemoved += 1

            

            # Note on the next iteration of the loop, y is the same.

            # This is so that if the line that was pulled down is also complete, it will be removed.

        else:

            y -= 1 # move on to check next row up

    return numLinesRemoved

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def convertToPixelCoords(boxx, boxy):

    # Convert the given xy coordinates of the board to xy coordinates of the location on the screen.

    return (XMARGIN + (boxx * BOXSIZE)), (TOPMARGIN + (boxy * BOXSIZE))

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def drawBox(boxx, boxy, color, pixelx=None, pixely=None): # draw a single box (each tetromino piece has four boxes)

    if color == BLANK:

        return

    if pixelx == None and pixely == None:

        pixelx, pixely = convertToPixelCoords(boxx, boxy)

    pygame.draw.rect(DISPLAYSURF, COLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 1, BOXSIZE - 1))

    pygame.draw.rect(DISPLAYSURF, LIGHTCOLORS[color], (pixelx + 1, pixely + 1, BOXSIZE - 4, BOXSIZE - 4))

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def drawBoard(board):

    

    pygame.draw.rect(DISPLAYSURF, BORDERCOLOR, (XMARGIN - 3, TOPMARGIN - 7, (BOARDWIDTH * BOXSIZE) + 8, (BOARDHEIGHT * BOXSIZE) + 8), 5) # draw the border around the board

    

    pygame.draw.rect(DISPLAYSURF, BGCOLOR, (XMARGIN, TOPMARGIN, BOXSIZE * BOARDWIDTH, BOXSIZE * BOARDHEIGHT)) # fill the background of the board

   

    for x in range(BOARDWIDTH):  # draw the individual boxes on the board

        for y in range(BOARDHEIGHT):

            drawBox(x, y, board[x][y])

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def drawStatus(score, level):

   

    scoreSurf = BASICFONT.render('Score: %s' % score, True, TEXTCOLOR)   # draw the score text

    scoreRect = scoreSurf.get_rect()

    scoreRect.topleft = (WINDOWWIDTH - 150, 20)

    DISPLAYSURF.blit(scoreSurf, scoreRect)

   

    levelSurf = BASICFONT.render('Level: %s' % level, True, TEXTCOLOR)   # draw the level text

    levelRect = levelSurf.get_rect()

    levelRect.topleft = (WINDOWWIDTH - 150, 50)

    DISPLAYSURF.blit(levelSurf, levelRect)

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def drawPiece(piece, pixelx=None, pixely=None):

    shapeToDraw = PIECES[piece['shape']][piece['rotation']]

    if pixelx == None and pixely == None:

        pixelx, pixely = convertToPixelCoords(piece['x'], piece['y']) # if pixelx & pixely hasn't been specified, use the location stored in the piece data structure
  

    for x in range(TEMPLATEWIDTH):   # draw each of the boxes that make up the piece

        for y in range(TEMPLATEHEIGHT):

            if shapeToDraw[y][x] != BLANK:

                drawBox(None, None, piece['color'], pixelx + (x * BOXSIZE), pixely + (y * BOXSIZE))

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

def drawNextPiece(piece):


    nextSurf = BASICFONT.render('Next:', True, TEXTCOLOR) # draw the "next" text
    nextRect = nextSurf.get_rect()
    nextRect.topleft = (WINDOWWIDTH - 120, 80)

    DISPLAYSURF.blit(nextSurf, nextRect)
    
    drawPiece(piece, pixelx=WINDOWWIDTH-120, pixely=100)  # draw the "next" piece

#---------------------------------------------------------------------------------------------------------------------------------------------------------------

if __name__ == '__main__':

    main()
