Tetris Player

上一篇 的基础上,这次用 Gemini 2.5 pro (0325)写了一个模仿玩家的程序来自动玩俄罗斯方块。

流程图

flowchart TD
    A[开始] --> B[初始化 Tetris AI Player]
    B --> C[启动 AI 循环]
    C --> D{游戏是否正在运行?}
    D -->|是| E[检查是否有最佳移动]
    E -->|否| F[计算最佳移动]
    F --> G[获取所有可能移动]
    G --> H[评估每个移动的得分]
    H --> I[选择最高得分的移动]
    I --> E
    E -->|是| J{当前旋转是否等于目标旋转?}
    J -->|否| K[旋转形状]
    K --> L{旋转是否成功?}
    L -->|否| M[重新计算最佳移动]
    M --> E
    L -->|是| J
    J -->|是| N{当前X位置是否等于目标X位置?}
    N -->|否| O[向目标方向移动]
    O --> N
    N -->|是| P[向下移动]
    P --> Q{是否放置了方块?}
    Q -->|是| R[放置形状并生成新形状]
    R --> S{游戏是否结束?}
    S -->|是| T[游戏结束]
    S -->|否| C
    Q -->|否| C
    D -->|否| T
    T --> U[结束]

代码

import tkinter as tk
from tetris import Tetris # Ensure tetris.py is in the same directory

class Player(Tetris):
    """
    An AI player that inherits from the Tetris class,
    It will automatically play the game to achieve a high score.
    """
    def __init__(self, master):
        super().__init__(master, auto_restart=True) # Pass auto_restart=True
        self.ai_move_time = 50  # Time interval between AI decisions (milliseconds)
        self.binds_enabled = False # Disable key bindings for human player
        self.master.title("Tetris AI Player") # Update window title

        # AI state variables
        self.best_move = None # Store the best move target {'rotation_target_state': int, 'x': int, 'score': float}
        self.current_rotation_count = 0 # Track the current rotation state of the block (0-3)

        # Start AI loop
        self.master.after(self.ai_move_time, self.ai_loop)

    # --- Override base class methods to adapt to AI ---

    def bind_keys(self):
        """Disable key bindings"""
        pass

    def new_shape(self):
        """Reset AI state when a new block appears"""
        super().new_shape()
        # Reset AI target and rotation count for new block
        self.best_move = None
        self.current_rotation_count = 0
        # Note: Do not call ai_move() immediately here, let ai_loop handle it to avoid potential performance issues

    def rotate_shape(self) -> None:
        """
        Rotate the current shape with wall kick attempts.
        Updates rotation count and handles collisions.

        Implements basic wall kick by trying to shift left/right when rotation fails.
        """
        if not self.game_running or not self.current_shape:
            return

        old_state = {
            'shape': [row[:] for row in self.current_shape],
            'x': self.current_x,
            'rotation': self.current_rotation_count
        }

        # Try rotation
        try:
            self.current_shape = [list(reversed(col)) for col in zip(*self.current_shape)]
        except IndexError:
            return

        # Check collision and attempt wall kicks
        if self.check_collision():
            for dx in [1, -2, 2, -1]:  # Try right, then left, then wider kicks
                self.current_x += dx
                if not self.check_collision():
                    self.current_rotation_count = (self.current_rotation_count + 1) % 4
                    return
                self.current_x -= dx  # Revert if kick failed

            # All kicks failed - restore original state
            self.current_shape = old_state['shape']
            self.current_x = old_state['x']
        else:
            # Rotation successful
            self.current_rotation_count = (self.current_rotation_count + 1) % 4

    def move_down(self) -> bool:
        """
        Move the current shape down one unit.

        Returns:
            bool: True if the block was placed (or game over), False otherwise
        """
        if not self.game_running:
            return False

        self.current_y += 1
        if not self.check_collision():
            return False  # Block not placed yet

        # Block hit something - place it
        self.current_y -= 1
        self.place_shape()

        if self.game_running:
            self.new_shape()
            if self.check_collision():
                self.game_over()

        return True  # Block was placed

    def drop_shape(self):
        """Override fast drop logic"""
        if not self.game_running: return

        # Continuously move down until collision
        while not self.check_collision():
            self.current_y += 1
        # Step back one step
        self.current_y -= 1
        self.place_shape()
        if self.game_running:
            self.new_shape() # Get new block and reset AI state
            if self.check_collision():
                self.game_over()
        # After fast drop, AI needs to immediately calculate target for new block
        self.best_move = None

    # --- AI Core Logic ---

    def ai_loop(self) -> None:
        """Optimized AI decision loop with better state management"""
        if not self.game_running:
            return

        current_piece = self.current_shape

        # Calculate move only if none exists or piece has changed
        if self.best_move is None or self.current_shape != current_piece:
            self.ai_move()

        # Execute best move if available
        if self.best_move:
            target_rot = self.best_move['rotation_target_state']
            target_x = self.best_move['x']

            # Rotation phase
            if self.current_rotation_count != target_rot:
                self.rotate_shape()
                # If rotation failed, recalculate
                if self.current_rotation_count != target_rot:
                    self.best_move = None

            # Movement phase
            elif self.current_x != target_x:
                direction = 1 if target_x > self.current_x else -1
                self.move_sideways(direction)

            # Drop phase
            else:
                placed = self.move_down()
                # If block placed, new_shape() was called which resets best_move

        # Fallback: force move down if no action taken
        elif self.game_running:
            self.move_down()

        # Schedule next iteration if game still running
        if self.game_running:
            self.master.after(self.ai_move_time, self.ai_loop)

    def ai_move(self) -> None:
        """
        Calculate and set the best move for the current block.

        Evaluates all possible moves and selects the one with highest score.
        If no valid moves found, sets best_move to None as fallback.
        """
        if not self.current_shape or not self.game_running:
            self.best_move = None
            return

        possible_moves = self.get_possible_moves()
        self.best_move = max(possible_moves, key=lambda m: m['score']) if possible_moves else None

    def get_possible_moves(self) -> list[dict]:
        """
        Generate all possible final placement positions for the current block and evaluate them.
        Optimized to reduce nested loops and improve performance by caching shape properties.

        Returns:
            list[dict]: Each element contains move information:
                {'rotation_target_state': int, 'x': int, 'score': float}
        """
        if not self.current_shape:
            return []

        possible_moves = []
        initial_rotation = self.current_rotation_count
        current_shape = self.current_shape

        # Pre-calculate shape properties for all rotations
        shape_props = self._precalculate_shape_properties(current_shape, initial_rotation)

        # Evaluate each possible move
        for prop in shape_props:
            rotated = prop['rotated']
            rot_state = prop['rot_state']

            for x_pos in range(prop['min_x'], prop['max_x'] + 1):
                temp_x = x_pos
                # Check if the starting position overlaps with existing blocks (at the top)
                start_blocked = self._check_start_position_blocked(rotated, temp_x)
                if start_blocked:
                    continue  # Starting position blocked, cannot place

                # Simulate block drop to find final y position
                sim_y = self._simulate_block_drop(rotated, temp_x)

                # Skip if the block would cause game over
                if self._check_game_over_potential(rotated, temp_x, sim_y):
                    continue

                # Create simulated board state after placement
                temp_grid = self._create_simulated_board(rotated, temp_x, sim_y)

                # Simulate clearing rows on the simulated board
                final_sim_grid, lines_cleared_in_sim = self._simulate_row_clearing(temp_grid)

                # Evaluate the score of the final simulated board
                score = self.evaluate_board_state(final_sim_grid, lines_cleared_in_sim)

                # Store this move and its score
                possible_moves.append({
                    'rotation_target_state': rot_state,
                    'x': x_pos,
                    'score': score
                })

        return possible_moves

    def _precalculate_shape_properties(self, current_shape: list[list[int]], initial_rotation: int) -> list[dict]:
        """Pre-calculate properties for all possible rotations of the current shape."""
        shape_props = []
        for rot_state in range(4):
            rotations = (rot_state - initial_rotation + 4) % 4
            try:
                rotated = current_shape
                for _ in range(rotations):
                    rotated = [list(reversed(col)) for col in zip(*rotated)]

                if not rotated or not rotated[0]:
                    continue

                # Calculate min/max x positions
                cells = [(x, y) for y, row in enumerate(rotated)
                        for x, cell in enumerate(row) if cell]
                if not cells:
                    continue

                min_x = min(x for x, _ in cells)
                max_x = max(x for x, _ in cells)
                min_grid_x = -min_x
                max_grid_x = self.grid_width - 1 - max_x

                shape_props.append({
                    'rotated': rotated,
                    'rot_state': rot_state,
                    'min_x': min_grid_x,
                    'max_x': max_grid_x
                })
            except IndexError:
                continue
        return shape_props

    def _check_start_position_blocked(self, rotated: list[list[int]], temp_x: int) -> bool:
        """Check if the starting position overlaps with existing blocks at the top."""
        for y, row in enumerate(rotated):
            for x, cell in enumerate(row):
                if cell:
                    check_x = temp_x + x
                    check_y = 0 + y  # Check position near the top
                    if 0 <= check_y < self.grid_height and 0 <= check_x < self.grid_width:
                        if self.grid[check_y][check_x]:
                            return True
            if True in [self.grid[0 + y][temp_x + x] for x, cell in enumerate(row) if cell
                       and 0 <= temp_x + x < self.grid_width]:
                return True
        return False

    def _simulate_block_drop(self, rotated: list[list[int]], temp_x: int) -> int:
        """Simulate dropping the block to find its final y position."""
        sim_y = 0
        while True:
            collision = False
            for y, row in enumerate(rotated):
                for x, cell in enumerate(row):
                    if cell:
                        next_x = temp_x + x
                        next_y = sim_y + y + 1  # Check position below
                        if (next_x < 0 or next_x >= self.grid_width or next_y >= self.grid_height or
                            (next_y >= 0 and self.grid[next_y][next_x])):
                            collision = True
                            break
                if collision:
                    break
            if collision:
                break  # sim_y is the final Y coordinate
            sim_y += 1
        return sim_y

    def _check_game_over_potential(self, rotated: list[list[int]], temp_x: int, sim_y: int) -> bool:
        """Check if placing the block would cause a game over."""
        game_over_potential = False
        for y, row in enumerate(rotated):
            for x, cell in enumerate(row):
                if cell:
                    place_x = temp_x + x
                    place_y = sim_y + y
                    if 0 <= place_y < self.grid_height and 0 <= place_x < self.grid_width:
                        if self.grid[place_y][place_x]:  # Overlap should not happen
                            game_over_potential = True
                            break
                    else:
                        # If the block is partially or fully out of bounds (only possible when sim_y < 0)
                        if place_y < 0:
                            game_over_potential = True
                            break
            if game_over_potential:
                break
        return game_over_potential

    def _create_simulated_board(self, rotated: list[list[int]], temp_x: int, sim_y: int) -> list[list[int]]:
        """Create a simulated board state after placing the block."""
        temp_grid = [row[:] for row in self.grid]
        for y, row in enumerate(rotated):
            for x, cell in enumerate(row):
                if cell:
                    place_x = temp_x + x
                    place_y = sim_y + y
                    if 0 <= place_y < self.grid_height and 0 <= place_x < self.grid_width:
                        temp_grid[place_y][place_x] = 1  # Use 1 to indicate block presence
        return temp_grid

    def _simulate_row_clearing(self, temp_grid: list[list[int]]) -> tuple[list[list[int]], int]:
        """Simulate clearing rows on the simulated board and return the updated grid and cleared lines count."""
        lines_cleared_in_sim = 0
        rows_to_keep = []
        for r in range(self.grid_height - 1, -1, -1):  # Check from bottom to top
            is_full = all(temp_grid[r])
            if not is_full:
                rows_to_keep.insert(0, temp_grid[r])
            else:
                lines_cleared_in_sim += 1

        # Create the final simulated board (add empty rows at the top)
        final_sim_grid = [[0] * self.grid_width for _ in range(lines_cleared_in_sim)] + rows_to_keep
        return final_sim_grid, lines_cleared_in_sim

    # --- Board Evaluation Function ---

    def evaluate_board_state(self, grid: list[list[int]], lines_cleared: int) -> float:
        """
        Evaluate the board state using optimized heuristic scoring.

        Args:
            grid: The board state matrix (1 = block, 0 = empty)
            lines_cleared: Number of lines cleared in this move

        Returns:
            float: Heuristic score (higher is better)
        """
        # Updated weights based on testing
        WEIGHTS = {
            'height': -0.7,      # Prefer lower stacks
            'lines': 4.0,        # Reward line clears
            'holes': -1.7,       # Penalize holes severely
            'bumpiness': -0.3,   # Slightly prefer smooth surfaces
            'wells': 0.2,        # Reward potential well formations
        }

        # Calculate metrics in single pass where possible
        heights = [0] * self.grid_width
        holes = 0
        well_depths = [0] * (self.grid_width - 2)

        for x in range(self.grid_width):
            col_height = 0
            found_block = False
            for y in range(self.grid_height):
                if y < len(grid) and x < len(grid[y]) and grid[y][x]:
                    col_height = self.grid_height - y
                    found_block = True
                    break
            heights[x] = col_height

            # Count holes in this column
            if found_block:
                for y in range(self.grid_height - col_height + 1, self.grid_height):
                    if y < len(grid) and x < len(grid[y]) and not grid[y][x]:
                        holes += 1

        # Calculate bumpiness and well depths
        bumpiness = 0
        for i in range(self.grid_width - 1):
            diff = abs(heights[i] - heights[i+1])
            bumpiness += diff

            # Detect wells (columns lower than both neighbors)
            if 0 < i < self.grid_width - 2:
                if heights[i] < heights[i-1] and heights[i] < heights[i+1]:
                    well_depths[i-1] = min(heights[i-1], heights[i+1]) - heights[i]

        agg_height = sum(heights)
        well_score = sum(well_depths)

        # Calculate final score
        score = (
            WEIGHTS['height'] * agg_height +
            WEIGHTS['lines'] * (lines_cleared ** 2) +
            WEIGHTS['holes'] * holes +
            WEIGHTS['bumpiness'] * bumpiness +
            WEIGHTS['wells'] * well_score
        )

        return score

    # --- Helper methods for evaluation metrics ---

    # Removed unused helper methods since evaluate_board_state() now calculates
    # all metrics in a single pass

    # --- Game start method ---
    @classmethod
    def run_game(cls):
        """Create Tkinter window and run the AI player"""
        root = tk.Tk()
        player = cls(root) # Create Player instance, it will start the game automatically
        root.mainloop()

# --- Main program entry ---
if __name__ == "__main__":
    Player.run_game()

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注