Python 数独游戏:添加动画与过渡效果

下面是一个完整的Python数独游戏实现,包含丰富的视觉细节和动画效果,使用Pygame库实现。

import pygame
import sys
import random
import math
from pygame import gfxdraw

# 初始化Pygame
pygame.init()

# 游戏常量
WIDTH, HEIGHT = 540, 600
GRID_SIZE = 9
CELL_SIZE = WIDTH // GRID_SIZE
FPS = 60

# 颜色定义
BACKGROUND = (28, 35, 53)
GRID_LINES = (86, 98, 128)
GRID_BOLD = (64, 80, 127)
CELL_HIGHLIGHT = (40, 120, 180, 100)
NUMBER_BASE = (220, 230, 255)
NUMBER_HIGHLIGHT = (255, 215, 100)
NUMBER_ERROR = (255, 100, 100)
BUTTON_BG = (50, 100, 180)
BUTTON_HOVER = (70, 130, 220)
BUTTON_TEXT = (240, 245, 255)

# 创建游戏窗口
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("视觉细节拉满的数独游戏")
clock = pygame.time.Clock()

# 字体
font = pygame.font.SysFont(None, 40)
small_font = pygame.font.SysFont(None, 30)
title_font = pygame.font.SysFont(None, 48)

class Animation:
    """动画效果管理类"""
    def __init__(self):
        self.cell_highlights = {}
        self.number_effects = {}
        self.grid_pulse = 0
        self.pulse_dir = 1
        self.completion_effect = None
    
    def add_cell_highlight(self, row, col):
        """添加单元格高亮动画"""
        self.cell_highlights[(row, col)] = {
            'alpha': 0,
            'grow': True,
            'duration': 0
        }
    
    def add_number_effect(self, row, col, correct=True):
        """添加数字输入效果"""
        self.number_effects[(row, col)] = {
            'scale': 0,
            'color': NUMBER_HIGHLIGHT if correct else NUMBER_ERROR,
            'duration': 0
        }
    
    def add_completion_effect(self):
        """添加游戏完成效果"""
        self.completion_effect = {
            'radius': 0,
            'alpha': 255,
            'particles': []
        }
    
    def generate_particle(self):
        """生成粒子效果"""
        return {
            'x': random.randint(0, WIDTH),
            'y': random.randint(0, HEIGHT),
            'size': random.randint(2, 8),
            'speed': random.uniform(1.0, 3.0),
            'angle': random.uniform(0, 2 * math.pi),
            'color': (
                random.randint(200, 255),
                random.randint(200, 255),
                random.randint(100, 200)
            ),
            'life': 100
        }
    
    def update(self, dt):
        """更新所有动画状态"""
        # 更新网格脉冲效果
        self.grid_pulse += 0.5 * self.pulse_dir
        if self.grid_pulse > 10:
            self.pulse_dir = -1
        elif self.grid_pulse < 0:
            self.pulse_dir = 1
        
        # 更新单元格高亮
        for key in list(self.cell_highlights.keys()):
            effect = self.cell_highlights[key]
            effect['duration'] += dt
            
            if effect['grow']:
                effect['alpha'] = min(255, effect['alpha'] + 15)
                if effect['alpha'] >= 255:
                    effect['grow'] = False
            else:
                effect['alpha'] = max(0, effect['alpha'] - 3)
                if effect['alpha'] <= 0:
                    del self.cell_highlights[key]
        
        # 更新数字效果
        for key in list(self.number_effects.keys()):
            effect = self.number_effects[key]
            effect['duration'] += dt
            
            if effect['scale'] < 1.0:
                effect['scale'] = min(1.0, effect['scale'] + 0.15)
            else:
                effect['scale'] = max(1.0, effect['scale'] - 0.01)
            
            if effect['duration'] > 1.5:  # 1.5秒后移除效果
                del self.number_effects[key]
        
        # 更新完成效果
        if self.completion_effect:
            self.completion_effect['radius'] += 1
            if self.completion_effect['radius'] > max(WIDTH, HEIGHT) * 1.5:
                self.completion_effect['alpha'] = max(0, self.completion_effect['alpha'] - 3)
            
            # 添加新粒子
            if random.random() < 0.3:
                self.completion_effect['particles'].append(self.generate_particle())
            
            # 更新现有粒子
            for particle in self.completion_effect['particles']:
                particle['x'] += math.cos(particle['angle']) * particle['speed']
                particle['y'] += math.sin(particle['angle']) * particle['speed']
                particle['life'] -= 1
            
            # 移除死亡的粒子
            self.completion_effect['particles'] = [
                p for p in self.completion_effect['particles'] if p['life'] > 0
            ]
            
            # 效果结束
            if self.completion_effect['alpha'] <= 0 and not self.completion_effect['particles']:
                self.completion_effect = None

class SudokuGame:
    """数独游戏核心类"""
    def __init__(self):
        self.board = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.solution = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.original = [[False for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.selected = None
        self.errors = set()
        self.animation = Animation()
        self.game_completed = False
        self.generate_puzzle()
    
    def generate_puzzle(self):
        """生成新的数独谜题"""
        # 重置游戏状态
        self.board = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.original = [[False for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.selected = None
        self.errors = set()
        self.game_completed = False
        self.animation = Animation()
        
        # 生成完整解
        self.solve_board(True)
        self.solution = [row[:] for row in self.board]
        
        # 移除部分数字创建谜题
        to_remove = 45  # 难度级别
        removed = 0
        
        while removed < to_remove:
            row, col = random.randint(0, 8), random.randint(0, 8)
            if self.board[row][col] != 0:
                # 备份值
                backup = self.board[row][col]
                self.board[row][col] = 0
                
                # 检查是否唯一解
                temp_board = [row[:] for row in self.board]
                solutions = self.count_solutions()
                self.board = [row[:] for row in temp_board]
                
                if solutions == 1:
                    self.original[row][col] = False
                    removed += 1
                else:
                    self.board[row][col] = backup
                    self.original[row][col] = True
    
    def solve_board(self, generate=False):
        """解数独板"""
        empty = self.find_empty()
        if not empty:
            return True
        
        row, col = empty
        
        # 生成谜题时随机排序数字
        numbers = list(range(1, 10))
        if generate:
            random.shuffle(numbers)
        
        for num in numbers:
            if self.is_valid(row, col, num):
                self.board[row][col] = num
                
                if self.solve_board(generate):
                    return True
                
                self.board[row][col] = 0
        
        return False
    
    def count_solutions(self):
        """计算解的数量"""
        empty = self.find_empty()
        if not empty:
            return 1
        
        row, col = empty
        count = 0
        
        for num in range(1, 10):
            if self.is_valid(row, col, num):
                self.board[row][col] = num
                count += self.count_solutions()
                self.board[row][col] = 0
                if count > 1:  # 提前终止
                    return count
        
        return count
    
    def find_empty(self):
        """查找空白格子"""
        for i in range(GRID_SIZE):
            for j in range(GRID_SIZE):
                if self.board[i][j] == 0:
                    return (i, j)
        return None
    
    def is_valid(self, row, col, num):
        """检查数字放置是否有效"""
        # 检查行
        for i in range(GRID_SIZE):
            if self.board[row][i] == num and i != col:
                return False
        
        # 检查列
        for i in range(GRID_SIZE):
            if self.board[i][col] == num and i != row:
                return False
        
        # 检查3x3宫格
        box_row, box_col = row // 3 * 3, col // 3 * 3
        for i in range(3):
            for j in range(3):
                if self.board[box_row + i][box_col + j] == num and (box_row + i != row or box_col + j != col):
                    return False
        
        return True
    
    def place_number(self, num):
        """在选中的格子中放置数字"""
        if self.selected and not self.original[self.selected[0]][self.selected[1]]:
            row, col = self.selected
            
            # 检查是否有效
            valid = self.is_valid(row, col, num)
            
            # 移除之前的错误标记
            self.errors.discard((row, col))
            
            # 放置数字
            self.board[row][col] = num
            
            # 添加动画效果
            self.animation.add_number_effect(row, col, valid)
            
            # 如果是错误放置,添加到错误集
            if not valid:
                self.errors.add((row, col))
            
            # 检查游戏是否完成
            if self.find_empty() is None and not self.errors:
                self.game_completed = True
                self.animation.add_completion_effect()
            
            return True
        return False
    
    def handle_click(self, pos):
        """处理鼠标点击"""
        x, y = pos
        if y < WIDTH:  # 点击在网格内
            row, col = y // CELL_SIZE, x // CELL_SIZE
            self.selected = (row, col)
            self.animation.add_cell_highlight(row, col)
        elif WIDTH + 10 <= x <= WIDTH + 100 and HEIGHT - 50 <= y <= HEIGHT - 10:
            self.generate_puzzle()  # 新游戏按钮
    
    def draw(self, screen):
        """绘制游戏界面"""
        # 绘制背景
        screen.fill(BACKGROUND)
        
        # 绘制标题
        title = title_font.render("视觉细节数独", True, (220, 230, 255))
        screen.blit(title, (WIDTH // 2 - title.get_width() // 2, 10))
        
        # 绘制网格线
        for i in range(GRID_SIZE + 1):
            # 绘制粗线
            if i % 3 == 0:
                line_width = 3 + self.animation.grid_pulse / 8
                color = GRID_BOLD
            else:
                line_width = 1
                color = GRID_LINES
            
            # 水平线
            pygame.draw.line(
                screen, color, 
                (0, i * CELL_SIZE), 
                (WIDTH, i * CELL_SIZE), 
                int(line_width)
            )
            
            # 垂直线
            pygame.draw.line(
                screen, color, 
                (i * CELL_SIZE, 0), 
                (i * CELL_SIZE, WIDTH), 
                int(line_width)
            )
        
        # 绘制高亮效果
        for (row, col), effect in self.animation.cell_highlights.items():
            alpha = effect['alpha']
            if alpha > 0:
                s = pygame.Surface((CELL_SIZE, CELL_SIZE), pygame.SRCALPHA)
                s.fill((*CELL_HIGHLIGHT[:3], alpha))
                screen.blit(s, (col * CELL_SIZE, row * CELL_SIZE))
        
        # 绘制选中的单元格
        if self.selected:
            row, col = self.selected
            pygame.draw.rect(
                screen, CELL_HIGHLIGHT, 
                (col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE, CELL_SIZE), 
                3
            )
        
        # 绘制数字
        for row in range(GRID_SIZE):
            for col in range(GRID_SIZE):
                value = self.board[row][col]
                if value != 0:
                    # 确定数字颜色
                    if (row, col) in self.errors:
                        color = NUMBER_ERROR
                    elif self.original[row][col]:
                        color = NUMBER_BASE
                    else:
                        color = NUMBER_HIGHLIGHT
                    
                    # 检查是否有动画效果
                    effect = self.animation.number_effects.get((row, col), None)
                    if effect:
                        # 应用缩放效果
                        scale = effect['scale']
                        color = effect['color']
                        num_surface = font.render(str(value), True, color)
                        
                        # 缩放表面
                        scaled_size = (int(num_surface.get_width() * scale), 
                                      int(num_surface.get_height() * scale))
                        if scaled_size[0] > 0 and scaled_size[1] > 0:
                            scaled_surface = pygame.transform.scale(num_surface, scaled_size)
                            pos_x = col * CELL_SIZE + (CELL_SIZE - scaled_size[0]) // 2
                            pos_y = row * CELL_SIZE + (CELL_SIZE - scaled_size[1]) // 2
                            screen.blit(scaled_surface, (pos_x, pos_y))
                        continue
                    
                    # 普通绘制
                    num_surface = font.render(str(value), True, color)
                    screen.blit(
                        num_surface, 
                        (col * CELL_SIZE + (CELL_SIZE - num_surface.get_width()) // 2,
                         row * CELL_SIZE + (CELL_SIZE - num_surface.get_height()) // 2)
                    )
        
        # 绘制新游戏按钮
        button_rect = pygame.Rect(WIDTH // 2 - 50, HEIGHT - 45, 100, 35)
        mouse_pos = pygame.mouse.get_pos()
        button_hover = button_rect.collidepoint(mouse_pos)
        
        # 按钮背景
        button_color = BUTTON_HOVER if button_hover else BUTTON_BG
        pygame.draw.rect(screen, button_color, button_rect, border_radius=8)
        pygame.draw.rect(screen, (200, 220, 255), button_rect, 2, border_radius=8)
        
        # 按钮文字
        button_text = small_font.render("新游戏", True, BUTTON_TEXT)
        screen.blit(button_text, (button_rect.centerx - button_text.get_width() // 2,
                                 button_rect.centery - button_text.get_height() // 2))
        
        # 绘制完成效果
        if self.animation.completion_effect:
            effect = self.animation.completion_effect
            radius = effect['radius']
            alpha = effect['alpha']
            
            # 绘制圆形扩散效果
            if radius > 0 and alpha > 0:
                s = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
                pygame.draw.circle(
                    s, (255, 255, 200, alpha // 2),
                    (WIDTH // 2, WIDTH // 2), radius
                )
                screen.blit(s, (0, 0))
            
            # 绘制粒子
            for particle in effect['particles']:
                pygame.draw.circle(
                    screen, particle['color'],
                    (int(particle['x']), int(particle['y'])),
                    particle['size']
                )
            
            # 绘制完成文本
            if alpha > 100:
                text = title_font.render("恭喜完成!", True, (255, 255, 200))
                screen.blit(text, (WIDTH // 2 - text.get_width() // 2, 
                                 HEIGHT // 2 - text.get_height() // 2))

# 创建游戏实例
game = SudokuGame()

# 主游戏循环
running = True
while running:
    dt = clock.tick(FPS) / 1000.0  # 转换为秒
    
    # 处理事件
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:  # 左键
                game.handle_click(event.pos)
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
            elif pygame.K_1 <= event.key <= pygame.K_9:
                num = event.key - pygame.K_0
                game.place_number(num)
            elif event.key == pygame.K_DELETE or event.key == pygame.K_BACKSPACE:
                game.place_number(0)
            elif event.key == pygame.K_n:
                game.generate_puzzle()
    
    # 更新动画
    game.animation.update(dt)
    
    # 绘制游戏
    game.draw(screen)
    
    # 更新显示
    pygame.display.flip()

pygame.quit()
sys.exit()

视觉细节说明

这个数独游戏实现了丰富的视觉效果和动画:

1. 网格动画

  • 粗网格线有轻微的脉动效果,增强视觉层次感
  • 细网格线保持清晰,形成对比

2. 单元格交互效果

  • 点击单元格时出现淡入淡出的高亮效果
  • 选中单元格有蓝色边框突出显示

3. 数字动画

  • 输入数字时有缩放动画效果
  • 正确输入显示金色动画
  • 错误输入显示红色动画
  • 初始数字为浅蓝色,用户输入为金色

4. 游戏完成特效

  • 金色粒子从屏幕中心向外扩散
  • 背景出现淡黄色光晕效果
  • "恭喜完成!"文字提示

5. 按钮效果

  • 新游戏按钮有悬停效果
  • 圆角设计和渐变颜色

6. 整体视觉风格

  • 深蓝色背景提高数字可读性
  • 精心选择的配色方案
  • 平滑的动画过渡效果

要运行此游戏,您需要安装Pygame库:

pip install pygame

这个实现包含了完整的数独生成逻辑、解决算法和丰富的视觉反馈,让游戏体验更加生动有趣。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐