import pygame
import random
import sys

# Initialize Pygame
pygame.init()

# Get the current screen resolution for fullscreen
infoObject = pygame.display.Info()
SCREEN_WIDTH = infoObject.current_w
SCREEN_HEIGHT = infoObject.current_h

# Define block size and calculate rows based on screen height
BLOCK_SIZE = 30  # Size of the tetromino blocks
COLUMNS = 10     # Standard Tetris width
ROWS = SCREEN_HEIGHT // BLOCK_SIZE  # Max height based on screen size

# Define separate top-left positions for player and computer
PLAYER_TOP_LEFT_X = (SCREEN_WIDTH // 4) - (COLUMNS * BLOCK_SIZE) // 2
COMPUTER_TOP_LEFT_X = (3 * SCREEN_WIDTH // 4) - (COLUMNS * BLOCK_SIZE) // 2
TOP_LEFT_Y = 0

# Define screen
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.FULLSCREEN)
pygame.display.set_caption('Split-Screen Tetris')

# Colors
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)
BORDER_COLOR = (200, 200, 200)
WHITE = (255, 255, 255)

# Tetromino shapes (Using 5x5 matrices)
S = [['.....',
      '.....',
      '..OO.',
      '.OO..',
      '.....'],

     ['.....',
      '..O..',
      '..OO.',
      '...O.',
      '.....']]

Z = [['.....',
      '.....',
      '.OO..',
      '..OO.',
      '.....'],

     ['.....',
      '..O..',
      '.OO..',
      '.O...',
      '.....']]

I = [['..O..',
      '..O..',
      '..O..',
      '..O..',
      '.....'],

     ['.....',
      'OOOO.',
      '.....',
      '.....',
      '.....']]

O = [['.....',
      '.....',
      '.OO..',
      '.OO..',
      '.....']]

J = [['.....',
      '.O...',
      '.OOO.',
      '.....',
      '.....'],

     ['.....',
      '..OO.',
      '..O..',
      '..O..',
      '.....'],

     ['.....',
      '.....',
      '.OOO.',
      '...O.',
      '.....'],

     ['.....',
      '..O..',
      '..O..',
      '.OO..',
      '.....']]

L = [['.....',
      '...O.',
      '.OOO.',
      '.....',
      '.....'],

     ['.....',
      '..O..',
      '..O..',
      '..OO.',
      '.....'],

     ['.....',
      '.....',
      '.OOO.',
      '.O...',
      '.....'],

     ['.....',
      '.OO..',
      '..O..',
      '..O..',
      '.....']]

T = [['.....',
      '..O..',
      '.OOO.',
      '.....',
      '.....'],

     ['.....',
      '..O..',
      '..OO.',
      '..O..',
      '.....'],

     ['.....',
      '.....',
      '.OOO.',
      '..O..',
      '.....'],

     ['.....',
      '..O..',
      '.OO..',
      '..O..',
      '.....']]

SHAPES = [S, Z, I, O, J, L, T]
SHAPE_COLORS = [
    (0, 255, 0),    # S - Green
    (255, 0, 0),    # Z - Red
    (0, 255, 255),  # I - Cyan
    (255, 255, 0),  # O - Yellow
    (0, 0, 255),    # J - Blue
    (255, 165, 0),  # L - Orange
    (128, 0, 128)   # T - Purple
]

# Define the Piece class
class Piece:
    def __init__(self, column, row, shape):
        self.x = column  # Column position
        self.y = row     # Row position
        self.shape = shape
        self.color = SHAPE_COLORS[SHAPES.index(shape)]
        self.rotation = 0

def create_grid(locked_positions={}):
    grid = [[BLACK for _ in range(COLUMNS)] for _ in range(ROWS)]
    for (j, i) in locked_positions:
        if 0 <= i < ROWS and 0 <= j < COLUMNS:
            grid[i][j] = locked_positions[(j, i)]
    return grid

def convert_shape_format(piece):
    positions = []
    format = piece.shape[piece.rotation % len(piece.shape)]
    
    for i, line in enumerate(format):
        row = list(line)
        for j, column in enumerate(row):
            if column == 'O':
                positions.append((piece.x + j - 2, piece.y + i - 2))
    return positions

def valid_space(piece, grid):
    accepted_positions = [[(j, i) for j in range(COLUMNS) if grid[i][j] == BLACK] for i in range(ROWS)]
    accepted_positions = [j for sub in accepted_positions for j in sub]

    formatted = convert_shape_format(piece)

    for pos in formatted:
        x, y = pos
        if x < 0 or x >= COLUMNS:
            return False
        if y >= 0:
            if (x, y) not in accepted_positions:
                return False
    return True

def check_lost(locked_positions):
    # Only check locked positions
    for (x, y) in locked_positions:
        if y < 1:  # If any locked position is in row 0 or above
            return True
    return False

def get_shape():
    return Piece(5, -2, random.choice(SHAPES))  # Initialize y at -2

def draw_text_middle(text, size, color, surface, x_offset=0):
    font = pygame.font.SysFont('comicsans', size, bold=True)
    label = font.render(text, True, color)

    # Position text in the middle of the player's or computer's grid
    if x_offset == SCREEN_WIDTH // 2:
        sx = COMPUTER_TOP_LEFT_X + (COLUMNS * BLOCK_SIZE) // 2 - (label.get_width() // 2)
    else:
        sx = PLAYER_TOP_LEFT_X + (COLUMNS * BLOCK_SIZE) // 2 - (label.get_width() // 2)
    sy = TOP_LEFT_Y + (ROWS * BLOCK_SIZE) // 2 - (label.get_height() // 2)
    
    surface.blit(label, (sx + x_offset, sy))

def draw_grid(surface, grid, is_computer=False):
    if is_computer:
        sx = COMPUTER_TOP_LEFT_X
    else:
        sx = PLAYER_TOP_LEFT_X
    sy = TOP_LEFT_Y

    for i in range(ROWS):
        for j in range(COLUMNS):
            pygame.draw.rect(surface, grid[i][j],
                             (sx + j * BLOCK_SIZE, sy + i * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)

    # Draw Grid Lines
    for i in range(ROWS + 1):
        pygame.draw.line(surface, GRAY, (sx, sy + i * BLOCK_SIZE),
                         (sx + COLUMNS * BLOCK_SIZE, sy + i * BLOCK_SIZE))
    for j in range(COLUMNS + 1):
        pygame.draw.line(surface, GRAY, (sx + j * BLOCK_SIZE, sy),
                         (sx + j * BLOCK_SIZE, sy + ROWS * BLOCK_SIZE))

    # Draw Border
    pygame.draw.rect(surface, BORDER_COLOR, (sx, sy, COLUMNS * BLOCK_SIZE, ROWS * BLOCK_SIZE), 5)

def clear_rows(grid, locked):
    inc = 0
    for i in range(ROWS - 1, -1, -1):
        row = grid[i]
        if BLACK not in row:
            inc += 1
            # Add positions to remove
            ind = i
            for j in range(COLUMNS):
                try:
                    del locked[(j, i)]
                except:
                    continue
    if inc > 0:
        # Shift every row above down
        keys = sorted(list(locked), key=lambda x: x[1])
        for key in sorted(keys, key=lambda x: x[1], reverse=True):
            x, y = key
            if y < ind:
                newKey = (x, y + inc)
                if newKey in locked:
                    # Avoid overwriting existing blocks
                    continue
                locked[newKey] = locked.pop(key)
    return inc

def draw_next_shape(shape, surface, is_computer=False):
    font = pygame.font.SysFont('comicsans', 30)
    label = font.render('Next Shape', True, WHITE)

    if is_computer:
        sx = COMPUTER_TOP_LEFT_X + COLUMNS * BLOCK_SIZE + 50
        sy = TOP_LEFT_Y + 5 * BLOCK_SIZE
    else:
        sx = PLAYER_TOP_LEFT_X + COLUMNS * BLOCK_SIZE + 50
        sy = TOP_LEFT_Y + 5 * BLOCK_SIZE

    surface.blit(label, (sx + 10, sy - 30))

    format = shape.shape[shape.rotation % len(shape.shape)]

    for i, line in enumerate(format):
        row = list(line)
        for j, column in enumerate(row):
            if column == 'O':
                pygame.draw.rect(surface, shape.color,
                                 (sx + j * BLOCK_SIZE, sy + i * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)

def update_score(n, score):
    scores = [0, 40, 100, 300, 1200]  # Standard Tetris scoring
    return score + scores[n]

def simulate_computer_move(piece, grid):
    # AI algorithm to decide the best rotation and position
    best_score = None
    best_x = None
    best_rotation = None

    # Iterate over all possible rotations
    for rotation in range(len(piece.shape)):
        test_piece = Piece(piece.x, piece.y, piece.shape)
        test_piece.rotation = rotation

        # Get positions after rotation
        shape_positions = convert_shape_format(test_piece)
        positions_x = [pos[0] for pos in shape_positions]
        min_x = min(positions_x)
        max_x = max(positions_x)

        # Iterate over all possible horizontal positions
        for x_pos in range(-min_x, COLUMNS - max_x):
            test_piece.x = x_pos
            test_piece.y = 0

            # Simulate dropping piece down
            while valid_space(test_piece, grid) and test_piece.y < ROWS:
                test_piece.y += 1
            test_piece.y -= 1  # Step back to last valid position

            if not valid_space(test_piece, grid):
                continue

            # Evaluate position
            score = evaluate_position(test_piece, grid)

            # Update best score if current score is better
            if best_score is None or score > best_score:
                best_score = score
                best_x = x_pos
                best_rotation = rotation

    # Set the piece to the best found x and rotation
    if best_x is not None and best_rotation is not None:
        piece.x = best_x
        piece.rotation = best_rotation
        # Uncomment the line below for debugging AI moves
        # print(f"AI Move: Rotation={best_rotation}, X Position={best_x}")
    else:
        # If no valid move, do not set x and rotation
        pass  # Let the piece fall naturally

def evaluate_position(piece, grid):
    """
    Evaluation function to score the position. Higher is better.
    Factors considered:
    - Lines Cleared
    - Aggregate Height
    - Number of Holes
    - Bumpiness
    """
    temp_grid = [row[:] for row in grid]
    shape_positions = convert_shape_format(piece)
    for x, y in shape_positions:
        if 0 <= y < ROWS and 0 <= x < COLUMNS:
            temp_grid[y][x] = piece.color

    lines_cleared = clear_rows(temp_grid, {})

    aggregate_height = get_aggregate_height(temp_grid)
    number_of_holes = get_holes(temp_grid)
    bumpiness = get_bumpiness(temp_grid)

    # Weights for different factors (can be adjusted)
    score = (lines_cleared * 1.0) - (aggregate_height * 0.5) - (number_of_holes * 0.7) - (bumpiness * 0.5)
    return score

def get_aggregate_height(grid):
    total = 0
    for x in range(COLUMNS):
        column_height = 0
        for y in range(ROWS):
            if grid[y][x] != BLACK:
                column_height = ROWS - y
                break
        total += column_height
    return total

def get_holes(grid):
    holes = 0
    for x in range(COLUMNS):
        block_found = False
        for y in range(ROWS):
            if grid[y][x] != BLACK:
                block_found = True
            elif block_found:
                holes += 1
    return holes

def get_bumpiness(grid):
    heights = []
    for x in range(COLUMNS):
        column_height = 0
        for y in range(ROWS):
            if grid[y][x] != BLACK:
                column_height = ROWS - y
                break
        heights.append(column_height)
    bumpiness = 0
    for i in range(len(heights)-1):
        bumpiness += abs(heights[i] - heights[i+1])
    return bumpiness

def main():
    player_locked_positions = {}
    computer_locked_positions = {}

    player_grid = create_grid(player_locked_positions)
    computer_grid = create_grid(computer_locked_positions)

    change_player_piece = False
    change_computer_piece = False
    run = True
    player_current_piece = get_shape()
    computer_current_piece = get_shape()
    player_next_piece = get_shape()
    computer_next_piece = get_shape()
    clock = pygame.time.Clock()
    fall_time = 0
    fall_speed = 0.5  # Seconds per block
    player_score = 0
    computer_score = 0

    # Initial check for immediate loss
    if not valid_space(player_current_piece, player_grid):
        draw_text_middle("You Lost!", 80, WHITE, SCREEN)
        pygame.display.update()
        pygame.time.delay(2000)
        run = False
    if not valid_space(computer_current_piece, computer_grid):
        draw_text_middle("Computer Lost!", 80, WHITE, SCREEN, x_offset=(SCREEN_WIDTH // 2))
        pygame.display.update()
        pygame.time.delay(2000)
        run = False

    while run:
        fall_time += clock.get_rawtime()
        clock.tick()

        player_grid = create_grid(player_locked_positions)
        computer_grid = create_grid(computer_locked_positions)

        # Add current player's piece to the grid
        shape_pos = convert_shape_format(player_current_piece)
        for x, y in shape_pos:
            if 0 <= y < ROWS and 0 <= x < COLUMNS:
                player_grid[y][x] = player_current_piece.color

        # Add current computer's piece to the grid
        shape_pos = convert_shape_format(computer_current_piece)
        for x, y in shape_pos:
            if 0 <= y < ROWS and 0 <= x < COLUMNS:
                computer_grid[y][x] = computer_current_piece.color

        if fall_time / 1000 >= fall_speed:
            fall_time = 0

            # Player Piece Movement
            player_current_piece.y += 1
            if not valid_space(player_current_piece, player_grid) and player_current_piece.y > 0:
                player_current_piece.y -= 1
                change_player_piece = True

            # Computer Piece Movement
            computer_current_piece.y += 1
            if not valid_space(computer_current_piece, computer_grid) and computer_current_piece.y > 0:
                computer_current_piece.y -= 1
                change_computer_piece = True

        # Event handling
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                pygame.display.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    run = False
                    pygame.display.quit()
                    sys.exit()
                # Player controls
                if event.key == pygame.K_LEFT:
                    player_current_piece.x -= 1
                    if not valid_space(player_current_piece, player_grid):
                        player_current_piece.x += 1
                if event.key == pygame.K_RIGHT:
                    player_current_piece.x += 1
                    if not valid_space(player_current_piece, player_grid):
                        player_current_piece.x -= 1
                if event.key == pygame.K_DOWN:
                    player_current_piece.y += 1
                    if not valid_space(player_current_piece, player_grid):
                        player_current_piece.y -= 1
                if event.key == pygame.K_UP:
                    player_current_piece.rotation = (player_current_piece.rotation + 1) % len(player_current_piece.shape)
                    if not valid_space(player_current_piece, player_grid):
                        player_current_piece.rotation = (player_current_piece.rotation - 1) % len(player_current_piece.shape)
                if event.key == pygame.K_SPACE or event.key == pygame.K_RETURN:
                    # Hard drop
                    while valid_space(player_current_piece, player_grid):
                        player_current_piece.y += 1
                    player_current_piece.y -= 1
                    change_player_piece = True

        # AI move simulation
        if computer_current_piece.y == -2:
            # Only simulate move when piece is at the top (initial y position)
            simulate_computer_move(computer_current_piece, computer_grid)

        # Handle locking the player's piece and generating a new one
        shape_pos = convert_shape_format(player_current_piece)
        for x, y in shape_pos:
            if y > -1 and 0 <= x < COLUMNS and 0 <= y < ROWS:
                player_grid[y][x] = player_current_piece.color

        if change_player_piece:
            for pos in shape_pos:
                p = (pos[0], pos[1])
                if 0 <= pos[0] < COLUMNS and 0 <= pos[1] < ROWS:
                    player_locked_positions[p] = player_current_piece.color
            player_current_piece = player_next_piece
            player_next_piece = get_shape()
            change_player_piece = False

            # Check for loss after placing the new piece
            if not valid_space(player_current_piece, player_grid):
                draw_text_middle("You Lost!", 80, WHITE, SCREEN)
                pygame.display.update()
                pygame.time.delay(2000)
                run = False

            cleared = clear_rows(player_grid, player_locked_positions)
            player_score = update_score(cleared, player_score)

        # Handle locking the computer's piece and generating a new one
        shape_pos = convert_shape_format(computer_current_piece)
        for x, y in shape_pos:
            if y > -1 and 0 <= x < COLUMNS and 0 <= y < ROWS:
                computer_grid[y][x] = computer_current_piece.color

        if change_computer_piece:
            for pos in shape_pos:
                p = (pos[0], pos[1])
                if 0 <= pos[0] < COLUMNS and 0 <= pos[1] < ROWS:
                    computer_locked_positions[p] = computer_current_piece.color
            computer_current_piece = computer_next_piece
            computer_next_piece = get_shape()
            change_computer_piece = False

            # Check for loss after placing the new piece
            if not valid_space(computer_current_piece, computer_grid):
                draw_text_middle("Computer Lost!", 80, WHITE, SCREEN, x_offset=(SCREEN_WIDTH // 2))
                pygame.display.update()
                pygame.time.delay(2000)
                run = False

            cleared = clear_rows(computer_grid, computer_locked_positions)
            computer_score = update_score(cleared, computer_score)

        # Draw everything
        SCREEN.fill(BLACK)

        # Draw grids
        draw_grid(SCREEN, player_grid, is_computer=False)
        draw_grid(SCREEN, computer_grid, is_computer=True)

        # Draw current player's piece
        for pos in convert_shape_format(player_current_piece):
            x, y = pos
            if y > -1 and 0 <= x < COLUMNS and 0 <= y < ROWS:
                pygame.draw.rect(SCREEN, player_current_piece.color,
                                 (PLAYER_TOP_LEFT_X + x * BLOCK_SIZE, TOP_LEFT_Y + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE), 0)

        # Draw current computer's piece
        for pos in convert_shape_format(computer_current_piece):
            x, y = pos
            if y > -1 and 0 <= x < COLUMNS and 0 <= y < ROWS:
                pygame.draw.rect(SCREEN, computer_current_piece.color,
                                 (COMPUTER_TOP_LEFT_X + x * BLOCK_SIZE, TOP_LEFT_Y + y * BLOCK_SIZE, BLOCK_SIZE,
                                  BLOCK_SIZE), 0)

        # Draw next shapes
        draw_next_shape(player_next_piece, SCREEN, is_computer=False)
        draw_next_shape(computer_next_piece, SCREEN, is_computer=True)

        # Draw scores
        font = pygame.font.SysFont('comicsans', 30)
        player_label = font.render(f'Player Score: {player_score}', True, WHITE)
        computer_label = font.render(f'Computer Score: {computer_score}', True, WHITE)
        SCREEN.blit(player_label, (PLAYER_TOP_LEFT_X + COLUMNS * BLOCK_SIZE + 50, TOP_LEFT_Y + 20))
        SCREEN.blit(computer_label,
                    (COMPUTER_TOP_LEFT_X + COLUMNS * BLOCK_SIZE + 50, TOP_LEFT_Y + 20))

        pygame.display.update()

    pygame.display.quit()
    sys.exit()

if __name__ == '__main__':
    main()


Hello, above code contains a bug:
the pieces dont fall to the bottom, but instead get "stuck" near the top