Flutter虚拟戳戳乐:打造趣味抽奖游戏体验

项目概述

虚拟戳戳乐是一款基于Flutter开发的趣味抽奖游戏应用,完美复现了传统戳戳乐的游戏体验。应用集成了奖品系统、戳洞动画、概率控制、游戏记录、统计分析等核心功能,通过精美的动画效果和直观的用户界面,为用户提供充满惊喜的游戏体验。
运行效果图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

应用特色

  • 真实游戏体验:5×4网格布局,完美还原传统戳戳乐
  • 丰富奖品系统:四个稀有度等级,12种不同奖品
  • 精美动画效果:戳洞动画、奖品展示动画、特效动画
  • 智能概率控制:科学的奖品分配算法,保证游戏平衡
  • 完整记录系统:详细的游戏记录和统计分析
  • 成就系统:最佳奖品记录,激励持续游戏

技术架构

核心技术栈

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

项目结构

lib/
├── main.dart                    # 应用入口和主要逻辑
├── models/                     # 数据模型(集成在main.dart中)
│   ├── prize.dart               # 奖品模型
│   ├── scratch_hole.dart        # 戳戳乐洞位模型
│   ├── game_record.dart         # 游戏记录模型
│   └── user_stats.dart          # 用户统计模型
├── screens/                    # 页面组件(集成在main.dart中)
│   ├── game_page.dart           # 游戏页面
│   ├── prize_page.dart          # 奖品页面
│   ├── records_page.dart        # 记录页面
│   └── stats_page.dart          # 统计页面
├── widgets/                    # 自定义组件(集成在main.dart中)
│   ├── scratch_hole_widget.dart # 戳戳乐洞位组件
│   ├── prize_card.dart          # 奖品卡片
│   └── game_board.dart          # 游戏板组件
└── utils/                      # 工具类(集成在main.dart中)
    ├── prize_distributor.dart   # 奖品分配器
    ├── animation_helper.dart    # 动画辅助
    └── stats_calculator.dart    # 统计计算

数据模型设计

奖品模型(Prize)

奖品模型是游戏的核心数据结构,定义了所有可能获得的奖品:

class Prize {
  final String id;              // 奖品唯一标识
  final String name;            // 奖品名称
  final String description;     // 奖品描述
  final String emoji;           // 奖品图标
  final int value;              // 奖品价值
  final String rarity;          // 稀有度等级
  final Color color;            // 奖品颜色
  final bool isSpecial;         // 是否为特殊奖品

  Prize({
    required this.id,
    required this.name,
    required this.description,
    required this.emoji,
    required this.value,
    required this.rarity,
    required this.color,
    this.isSpecial = false,
  });
}

奖品模型包含稀有度文本转换功能:

String get rarityText {
  switch (rarity) {
    case 'common':
      return '普通';
    case 'rare':
      return '稀有';
    case 'epic':
      return '史诗';
    case 'legendary':
      return '传说';
    default:
      return '未知';
  }
}

戳戳乐洞位模型(ScratchHole)

洞位模型管理每个戳戳乐位置的状态:

class ScratchHole {
  final int index;              // 洞位索引
  Prize? prize;                 // 包含的奖品
  bool isScratched;             // 是否已戳开
  bool isAnimating;             // 是否正在动画
  double scratchProgress;       // 戳开进度

  ScratchHole({
    required this.index,
    this.prize,
    this.isScratched = false,
    this.isAnimating = false,
    this.scratchProgress = 0.0,
  });
}

洞位模型提供状态复制功能:

ScratchHole copyWith({
  Prize? prize,
  bool? isScratched,
  bool? isAnimating,
  double? scratchProgress,
}) {
  return ScratchHole(
    index: index,
    prize: prize ?? this.prize,
    isScratched: isScratched ?? this.isScratched,
    isAnimating: isAnimating ?? this.isAnimating,
    scratchProgress: scratchProgress ?? this.scratchProgress,
  );
}

游戏记录模型(GameRecord)

游戏记录模型存储每局游戏的详细信息:

class GameRecord {
  final String id;                  // 记录唯一标识
  final DateTime playTime;          // 游戏时间
  final List<Prize> wonPrizes;      // 获得的奖品列表
  final int totalValue;             // 总价值
  final int holesScratched;         // 戳开的洞位数

  GameRecord({
    required this.id,
    required this.playTime,
    required this.wonPrizes,
    required this.totalValue,
    required this.holesScratched,
  });
}

用户统计模型(UserStats)

用户统计模型管理玩家的游戏统计数据:

class UserStats {
  int totalGames;                   // 总游戏次数
  int totalPrizes;                  // 总获得奖品数
  int totalValue;                   // 总价值
  Map<String, int> rarityCount;     // 稀有度统计
  Prize? bestPrize;                 // 最佳奖品

  UserStats({
    this.totalGames = 0,
    this.totalPrizes = 0,
    this.totalValue = 0,
    Map<String, int>? rarityCount,
    this.bestPrize,
  }) : rarityCount = rarityCount ?? {};
}

应用主体结构

应用入口

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '虚拟戳戳乐',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
        useMaterial3: true,
      ),
      home: const ScratchGameHomePage(),
    );
  }
}

应用采用橙色作为主题色,营造温暖活泼的游戏氛围。

主页面结构

主页面使用底部导航栏实现四个核心功能模块:

class ScratchGameHomePage extends StatefulWidget {
  const ScratchGameHomePage({super.key};

  
  State<ScratchGameHomePage> createState() => _ScratchGameHomePageState();
}

class _ScratchGameHomePageState extends State<ScratchGameHomePage>
    with TickerProviderStateMixin {
  int _selectedIndex = 0;
  List<ScratchHole> _holes = [];           // 戳戳乐洞位列表
  List<Prize> _availablePrizes = [];       // 可用奖品列表
  List<GameRecord> _gameRecords = [];      // 游戏记录列表
  UserStats _userStats = UserStats();      // 用户统计数据
  
  bool _isGameActive = false;              // 游戏是否进行中
  int _remainingHoles = 0;                 // 剩余洞位数
  List<Prize> _currentWonPrizes = [];      // 当前获得奖品
  
  // 动画控制器
  late AnimationController _scratchAnimationController;
  late AnimationController _prizeAnimationController;
  late Animation<double> _scratchAnimation;
  late Animation<double> _prizeScaleAnimation;
  late Animation<double> _prizeRotationAnimation;
}

动画系统设计

动画控制器初始化

应用使用多个动画控制器实现丰富的动画效果:

void _setupAnimations() {
  // 戳洞动画控制器
  _scratchAnimationController = AnimationController(
    duration: const Duration(milliseconds: 800),
    vsync: this,
  );

  // 奖品展示动画控制器
  _prizeAnimationController = AnimationController(
    duration: const Duration(milliseconds: 1200),
    vsync: this,
  );

  // 戳洞动画
  _scratchAnimation = Tween<double>(
    begin: 0.0,
    end: 1.0,
  ).animate(CurvedAnimation(
    parent: _scratchAnimationController,
    curve: Curves.easeOutBack,
  ));

  // 奖品缩放动画
  _prizeScaleAnimation = Tween<double>(
    begin: 0.0,
    end: 1.0,
  ).animate(CurvedAnimation(
    parent: _prizeAnimationController,
    curve: const Interval(0.0, 0.6, curve: Curves.elasticOut),
  ));

  // 奖品旋转动画(仅特殊奖品)
  _prizeRotationAnimation = Tween<double>(
    begin: 0.0,
    end: 2 * pi,
  ).animate(CurvedAnimation(
    parent: _prizeAnimationController,
    curve: const Interval(0.4, 1.0, curve: Curves.easeInOut),
  ));
}

动画效果实现

戳洞动画和奖品展示动画的协调配合:

void _scratchHole(ScratchHole hole) {
  if (!_isGameActive || hole.isScratched) return;

  setState(() {
    hole.isScratched = true;
    hole.isAnimating = true;
    _remainingHoles--;
  });

  // 开始戳洞动画
  _scratchAnimationController.forward().then((_) {
    _scratchAnimationController.reset();
  });

  // 如果有奖品,开始奖品动画
  if (hole.prize != null) {
    _currentWonPrizes.add(hole.prize!);
    _prizeAnimationController.forward().then((_) {
      _prizeAnimationController.reset();
      setState(() {
        hole.isAnimating = false;
      });
    });

    // 显示获奖提示
    _showPrizeDialog(hole.prize!);
  } else {
    setState(() {
      hole.isAnimating = false;
    });
  }
}

游戏页面实现

游戏状态展示

游戏页面顶部展示当前游戏状态:

Widget _buildGamePage() {
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 游戏状态卡片
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.orange.shade400, Colors.red.shade400],
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
            ),
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                '🎯 戳戳乐游戏',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
              const SizedBox(height: 8),
              Text(
                _isGameActive ? '选择一个洞位戳开,看看有什么惊喜!' : '点击"开始新游戏"开始游戏',
                style: const TextStyle(
                  fontSize: 16,
                  color: Colors.white70,
                ),
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  _buildGameStatCard('剩余洞位', '$_remainingHoles'),
                  const SizedBox(width: 16),
                  _buildGameStatCard('已获奖品', '${_currentWonPrizes.length}'),
                ],
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

戳戳乐游戏板

游戏板采用5×4网格布局,完美还原传统戳戳乐:

Container(
  padding: const EdgeInsets.all(16),
  decoration: BoxDecoration(
    color: Colors.brown.shade100,
    borderRadius: BorderRadius.circular(16),
    border: Border.all(color: Colors.brown.shade300, width: 3),
  ),
  child: Column(
    children: [
      const Text(
        '戳戳乐',
        style: TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
          color: Colors.brown,
        ),
      ),
      const SizedBox(height: 16),
      GridView.builder(
        shrinkWrap: true,
        physics: const NeverScrollableScrollPhysics(),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 5,
          childAspectRatio: 1.0,
          crossAxisSpacing: 8,
          mainAxisSpacing: 8,
        ),
        itemCount: _holes.length,
        itemBuilder: (context, index) {
          return _buildScratchHole(_holes[index]);
        },
      ),
    ],
  ),
),

戳戳乐洞位组件

每个洞位的视觉效果和交互逻辑:

Widget _buildScratchHole(ScratchHole hole) {
  return GestureDetector(
    onTap: () => _scratchHole(hole),
    child: AnimatedBuilder(
      animation: _scratchAnimation,
      builder: (context, child) {
        return Container(
          decoration: BoxDecoration(
            color: hole.isScratched 
                ? (hole.prize?.color ?? Colors.grey.shade300)
                : Colors.brown.shade400,
            borderRadius: BorderRadius.circular(8),
            border: Border.all(
              color: hole.isAnimating 
                  ? Colors.yellow 
                  : Colors.brown.shade600,
              width: hole.isAnimating ? 3 : 2,
            ),
            boxShadow: hole.isAnimating
                ? [
                    BoxShadow(
                      color: Colors.yellow.withValues(alpha: 0.5),
                      blurRadius: 10,
                      spreadRadius: 2,
                    ),
                  ]
                : null,
          ),
          child: hole.isScratched
              ? _buildRevealedPrize(hole)
              : _buildUnscratched(hole),
        );
      },
    ),
  );
}

奖品展示效果

戳开后的奖品展示,包含动画效果:

Widget _buildRevealedPrize(ScratchHole hole) {
  if (hole.prize == null) return const SizedBox.shrink();

  return AnimatedBuilder(
    animation: _prizeAnimationController,
    builder: (context, child) {
      return Transform.scale(
        scale: _prizeScaleAnimation.value,
        child: Transform.rotate(
          angle: hole.prize!.isSpecial ? _prizeRotationAnimation.value : 0,
          child: Container(
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(6),
              gradient: LinearGradient(
                colors: [
                  hole.prize!.color.withValues(alpha: 0.3),
                  hole.prize!.color.withValues(alpha: 0.7),
                ],
                begin: Alignment.topLeft,
                end: Alignment.bottomRight,
              ),
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  hole.prize!.emoji,
                  style: const TextStyle(fontSize: 20),
                ),
                const SizedBox(height: 2),
                Text(
                  '${hole.prize!.value}',
                  style: const TextStyle(
                    fontSize: 10,
                    fontWeight: FontWeight.bold,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    },
  );
}

奖品系统设计

奖品数据初始化

应用包含12种不同的奖品,分为四个稀有度等级:

void _initializeData() {
  _availablePrizes = [
    // 普通奖品 (50%概率)
    Prize(
      id: 'common_1',
      name: '小糖果',
      description: '甜甜的小糖果',
      emoji: '🍬',
      value: 1,
      rarity: 'common',
      color: Colors.pink,
    ),
    Prize(
      id: 'common_2',
      name: '小饼干',
      description: '香脆的小饼干',
      emoji: '🍪',
      value: 2,
      rarity: 'common',
      color: Colors.brown,
    ),
    // ... 更多普通奖品

    // 稀有奖品 (30%概率)
    Prize(
      id: 'rare_1',
      name: '巧克力',
      description: '丝滑的巧克力',
      emoji: '🍫',
      value: 10,
      rarity: 'rare',
      color: Colors.orange,
    ),
    // ... 更多稀有奖品

    // 史诗奖品 (15%概率)
    Prize(
      id: 'epic_1',
      name: '生日蛋糕',
      description: '精美的生日蛋糕',
      emoji: '🎂',
      value: 50,
      rarity: 'epic',
      color: Colors.green,
    ),
    // ... 更多史诗奖品

    // 传说奖品 (5%概率)
    Prize(
      id: 'legendary_1',
      name: '黄金糖果',
      description: '传说中的黄金糖果',
      emoji: '⭐',
      value: 200,
      rarity: 'legendary',
      color: Colors.amber,
      isSpecial: true,
    ),
    Prize(
      id: 'legendary_2',
      name: '钻石蛋糕',
      description: '闪闪发光的钻石蛋糕',
      emoji: '💎',
      value: 500,
      rarity: 'legendary',
      color: Colors.blue,
      isSpecial: true,
    ),
  ];
}

奖品分配算法

科学的概率分配算法确保游戏平衡:

void _distributePrizes() {
  final random = Random();
  final shuffledHoles = List<ScratchHole>.from(_holes);
  shuffledHoles.shuffle();

  // 分配奖品概率
  final prizeDistribution = <String, double>{
    'common': 0.5,      // 50% 普通
    'rare': 0.3,        // 30% 稀有
    'epic': 0.15,       // 15% 史诗
    'legendary': 0.05,  // 5% 传说
  };

  for (int i = 0; i < shuffledHoles.length; i++) {
    final hole = shuffledHoles[i];
    final randomValue = random.nextDouble();
    
    String selectedRarity = 'common';
    double cumulativeRate = 0.0;
    
    // 累积概率计算
    for (final entry in prizeDistribution.entries) {
      cumulativeRate += entry.value;
      if (randomValue <= cumulativeRate) {
        selectedRarity = entry.key;
        break;
      }
    }

    // 从对应稀有度中随机选择奖品
    final availablePrizes = _availablePrizes
        .where((prize) => prize.rarity == selectedRarity)
        .toList();
    
    if (availablePrizes.isNotEmpty) {
      final selectedPrize = availablePrizes[random.nextInt(availablePrizes.length)];
      hole.prize = selectedPrize;
    }
  }
}

奖品展示页面

奖品页面按稀有度分组展示所有奖品:

Widget _buildPrizePage() {
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 奖品展示头部
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.purple.shade400, Colors.pink.shade400],
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
            ),
            borderRadius: BorderRadius.circular(16),
          ),
          child: const Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '🎁 奖品大全',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
              SizedBox(height: 8),
              Text(
                '查看所有可能获得的奖品',
                style: TextStyle(
                  fontSize: 16,
                  color: Colors.white70,
                ),
              ),
            ],
          ),
        ),

        const SizedBox(height: 24),

        // 按稀有度分组展示奖品
        ...['legendary', 'epic', 'rare', 'common'].map((rarity) {
          final prizes = _availablePrizes.where((p) => p.rarity == rarity).toList();
          if (prizes.isEmpty) return const SizedBox.shrink();

          return Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  Container(
                    width: 4,
                    height: 20,
                    decoration: BoxDecoration(
                      color: _getRarityColor(rarity),
                      borderRadius: BorderRadius.circular(2),
                    ),
                  ),
                  const SizedBox(width: 8),
                  Text(
                    '${_getRarityText(rarity)} (${prizes.length})',
                    style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 12),
              GridView.builder(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                  childAspectRatio: 1.2,
                  crossAxisSpacing: 12,
                  mainAxisSpacing: 12,
                ),
                itemCount: prizes.length,
                itemBuilder: (context, index) {
                  return _buildPrizeCard(prizes[index]);
                },
              ),
              const SizedBox(height: 24),
            ],
          );
        }).toList(),
      ],
    ),
  );
}

获奖提示系统

获奖对话框

当玩家获得奖品时,显示精美的获奖对话框:

void _showPrizeDialog(Prize prize) {
  showDialog(
    context: context,
    barrierDismissible: false,
    builder: (context) => Dialog(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
      child: Container(
        padding: const EdgeInsets.all(24),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(20),
          gradient: LinearGradient(
            colors: [
              prize.color.withValues(alpha: 0.1),
              prize.color.withValues(alpha: 0.3),
            ],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              '🎉 恭喜获得 🎉',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: prize.color,
              ),
            ),
            
            const SizedBox(height: 20),
            
            // 奖品展示容器
            Container(
              width: 100,
              height: 100,
              decoration: BoxDecoration(
                color: prize.color.withValues(alpha: 0.2),
                borderRadius: BorderRadius.circular(20),
                border: Border.all(color: prize.color, width: 3),
              ),
              child: Center(
                child: Text(
                  prize.emoji,
                  style: const TextStyle(fontSize: 48),
                ),
              ),
            ),
            
            const SizedBox(height: 16),
            
            Text(
              prize.name,
              style: const TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            
            const SizedBox(height: 8),
            
            Text(
              prize.description,
              style: TextStyle(
                fontSize: 14,
                color: Colors.grey.shade600,
              ),
              textAlign: TextAlign.center,
            ),
            
            const SizedBox(height: 16),
            
            // 奖品信息标签
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                  decoration: BoxDecoration(
                    color: prize.color,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Icon(Icons.star, color: Colors.white, size: 16),
                      const SizedBox(width: 4),
                      Text(
                        '价值:${prize.value}',
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
                const SizedBox(width: 12),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: _getRarityColor(prize.rarity).withValues(alpha: 0.2),
                    borderRadius: BorderRadius.circular(8),
                    border: Border.all(color: _getRarityColor(prize.rarity)),
                  ),
                  child: Text(
                    prize.rarityText,
                    style: TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.bold,
                      color: _getRarityColor(prize.rarity),
                    ),
                  ),
                ),
              ],
            ),
            
            const SizedBox(height: 20),
            
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: () => Navigator.of(context).pop(),
                style: ElevatedButton.styleFrom(
                  backgroundColor: prize.color,
                  foregroundColor: Colors.white,
                ),
                child: const Text('太棒了!'),
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

游戏记录系统

记录页面设计

游戏记录页面展示所有游戏历史:

Widget _buildRecordsPage() {
  if (_gameRecords.isEmpty) {
    return const Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.history, size: 64, color: Colors.grey),
          SizedBox(height: 16),
          Text(
            '暂无游戏记录',
            style: TextStyle(fontSize: 18, color: Colors.grey),
          ),
          SizedBox(height: 8),
          Text(
            '完成一局游戏后会显示在这里',
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(
              '游戏记录 (${_gameRecords.length})',
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            TextButton(
              onPressed: _clearRecords,
              child: const Text('清空记录'),
            ),
          ],
        ),

        const SizedBox(height: 16),

        ListView.builder(
          shrinkWrap: true,
          physics: const NeverScrollableScrollPhysics(),
          itemCount: _gameRecords.length,
          itemBuilder: (context, index) {
            final record = _gameRecords[index];
            return Container(
              margin: const EdgeInsets.only(bottom: 12),
              child: Card(
                child: ExpansionTile(
                  leading: Container(
                    width: 48,
                    height: 48,
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        colors: [Colors.blue.shade400, Colors.purple.shade400],
                      ),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Center(
                      child: Text(
                        '${index + 1}',
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                  ),
                  title: Text('游戏 ${index + 1}'),
                  subtitle: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('获得 ${record.wonPrizes.length} 个奖品,总价值 ${record.totalValue}'),
                      Text(
                        _formatDateTime(record.playTime),
                        style: const TextStyle(fontSize: 12),
                      ),
                    ],
                  ),
                  children: [
                    if (record.wonPrizes.isNotEmpty)
                      Container(
                        padding: const EdgeInsets.all(16),
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            const Text(
                              '获得奖品:',
                              style: TextStyle(fontWeight: FontWeight.bold),
                            ),
                            const SizedBox(height: 8),
                            Wrap(
                              spacing: 8,
                              runSpacing: 8,
                              children: record.wonPrizes.map((prize) {
                                return Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 8,
                                    vertical: 4,
                                  ),
                                  decoration: BoxDecoration(
                                    color: prize.color.withValues(alpha: 0.2),
                                    borderRadius: BorderRadius.circular(12),
                                    border: Border.all(color: prize.color),
                                  ),
                                  child: Row(
                                    mainAxisSize: MainAxisSize.min,
                                    children: [
                                      Text(prize.emoji, style: const TextStyle(fontSize: 12)),
                                      const SizedBox(width: 4),
                                      Text(
                                        prize.name,
                                        style: const TextStyle(fontSize: 10),
                                      ),
                                      const SizedBox(width: 4),
                                      Text(
                                        '(${prize.value})',
                                        style: TextStyle(
                                          fontSize: 10,
                                          color: prize.color,
                                          fontWeight: FontWeight.bold,
                                        ),
                                      ),
                                    ],
                                  ),
                                );
                              }).toList(),
                            ),
                          ],
                        ),
                      ),
                  ],
                ),
              ),
            );
          },
        ),
      ],
    ),
  );
}

统计分析系统

统计页面设计

统计页面提供详细的游戏数据分析:

Widget _buildStatsPage() {
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 统计概览
        Container(
          width: double.infinity,
          padding: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            gradient: LinearGradient(
              colors: [Colors.green.shade400, Colors.teal.shade400],
              begin: Alignment.topLeft,
              end: Alignment.bottomRight,
            ),
            borderRadius: BorderRadius.circular(16),
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text(
                '📊 游戏统计',
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold,
                  color: Colors.white,
                ),
              ),
              const SizedBox(height: 16),
              Row(
                children: [
                  _buildStatsCard('游戏次数', '${_userStats.totalGames}'),
                  const SizedBox(width: 16),
                  _buildStatsCard('获得奖品', '${_userStats.totalPrizes}'),
                ],
              ),
              const SizedBox(height: 12),
              Row(
                children: [
                  _buildStatsCard('总价值', '${_userStats.totalValue}'),
                  const SizedBox(width: 16),
                  _buildStatsCard('平均价值', _getAverageValue()),
                ],
              ),
            ],
          ),
        ),

        const SizedBox(height: 24),

        // 稀有度统计
        const Text(
          '稀有度统计',
          style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
        ),
        const SizedBox(height: 12),

        ...['legendary', 'epic', 'rare', 'common'].map((rarity) {
          final count = _userStats.rarityCount[rarity] ?? 0;
          final color = _getRarityColor(rarity);
          final percentage = _userStats.totalPrizes > 0 
              ? (count / _userStats.totalPrizes * 100).toStringAsFixed(1)
              : '0.0';

          return Container(
            margin: const EdgeInsets.only(bottom: 8),
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: color.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(8),
              border: Border.all(color: color.withValues(alpha: 0.3)),
            ),
            child: Row(
              children: [
                Container(
                  width: 12,
                  height: 12,
                  decoration: BoxDecoration(
                    color: color,
                    shape: BoxShape.circle,
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: Text(
                    _getRarityText(rarity),
                    style: const TextStyle(fontWeight: FontWeight.bold),
                  ),
                ),
                Text(
                  '$count ($percentage%)',
                  style: TextStyle(
                    color: color,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          );
        }).toList(),
      ],
    ),
  );
}

最佳奖品展示

统计页面还展示玩家获得的最佳奖品:

// 最佳奖品
if (_userStats.bestPrize != null) ...[
  const Text(
    '最佳奖品',
    style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
  ),
  const SizedBox(height: 12),
  Container(
    width: double.infinity,
    padding: const EdgeInsets.all(16),
    decoration: BoxDecoration(
      gradient: LinearGradient(
        colors: [
          _userStats.bestPrize!.color.withValues(alpha: 0.2),
          _userStats.bestPrize!.color.withValues(alpha: 0.4),
        ],
      ),
      borderRadius: BorderRadius.circular(12),
      border: Border.all(color: _userStats.bestPrize!.color, width: 2),
    ),
    child: Row(
      children: [
        Text(
          _userStats.bestPrize!.emoji,
          style: const TextStyle(fontSize: 48),
        ),
        const SizedBox(width: 16),
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                _userStats.bestPrize!.name,
                style: const TextStyle(
                  fontSize: 18,
                  fontWeight: FontWeight.bold,
                ),
              ),
              Text(
                _userStats.bestPrize!.description,
                style: TextStyle(
                  fontSize: 14,
                  color: Colors.grey.shade600,
                ),
              ),
              const SizedBox(height: 8),
              Row(
                children: [
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 8,
                      vertical: 4,
                    ),
                    decoration: BoxDecoration(
                      color: _userStats.bestPrize!.color,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        const Icon(
                          Icons.star,
                          size: 12,
                          color: Colors.white,
                        ),
                        const SizedBox(width: 4),
                        Text(
                          '${_userStats.bestPrize!.value}',
                          style: const TextStyle(
                            fontSize: 12,
                            fontWeight: FontWeight.bold,
                            color: Colors.white,
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(width: 8),
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 6,
                      vertical: 2,
                    ),
                    decoration: BoxDecoration(
                      color: _getRarityColor(_userStats.bestPrize!.rarity)
                          .withValues(alpha: 0.2),
                      borderRadius: BorderRadius.circular(8),
                      border: Border.all(
                        color: _getRarityColor(_userStats.bestPrize!.rarity),
                      ),
                    ),
                    child: Text(
                      _userStats.bestPrize!.rarityText,
                      style: TextStyle(
                        fontSize: 10,
                        fontWeight: FontWeight.bold,
                        color: _getRarityColor(_userStats.bestPrize!.rarity),
                      ),
                    ),
                  ),
                ],
              ),
            ],
          ),
        ),
      ],
    ),
  ),
],

游戏结束处理

游戏结束逻辑

当所有洞位都被戳开或玩家主动结束游戏时:

void _endCurrentGame() {
  if (!_isGameActive) return;

  final gameRecord = GameRecord(
    id: 'game_${DateTime.now().millisecondsSinceEpoch}',
    playTime: DateTime.now(),
    wonPrizes: List.from(_currentWonPrizes),
    totalValue: _currentWonPrizes.fold(0, (sum, prize) => sum + prize.value),
    holesScratched: 20 - _remainingHoles,
  );

  setState(() {
    _isGameActive = false;
    _gameRecords.insert(0, gameRecord);
    
    // 更新统计
    _userStats.totalGames++;
    _userStats.totalPrizes += _currentWonPrizes.length;
    _userStats.totalValue += gameRecord.totalValue;
    
    // 更新稀有度统计
    for (final prize in _currentWonPrizes) {
      _userStats.rarityCount[prize.rarity] = 
          (_userStats.rarityCount[prize.rarity] ?? 0) + 1;
      
      // 更新最佳奖品
      if (_userStats.bestPrize == null || prize.value > _userStats.bestPrize!.value) {
        _userStats.bestPrize = prize;
      }
    }
  });

  // 显示游戏结束对话框
  _showGameEndDialog(gameRecord);
}

游戏结束对话框

游戏结束时显示本轮游戏总结:

void _showGameEndDialog(GameRecord record) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('🎮 游戏结束'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('本轮获得 ${record.wonPrizes.length} 个奖品'),
          Text('总价值:${record.totalValue}'),
          Text('戳开洞位:${record.holesScratched}/20'),
          if (record.wonPrizes.isNotEmpty) ...[
            const SizedBox(height: 12),
            const Text('获得奖品:', style: TextStyle(fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            ...record.wonPrizes.map((prize) => Text('${prize.emoji} ${prize.name} (${prize.value})')),
          ],
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('查看记录'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.of(context).pop();
            _initializeNewGame();
          },
          child: const Text('再来一局'),
        ),
      ],
    ),
  );
}

工具函数

稀有度相关函数

处理稀有度颜色和文本转换:

Color _getRarityColor(String rarity) {
  switch (rarity) {
    case 'common':
      return Colors.grey;
    case 'rare':
      return Colors.blue;
    case 'epic':
      return Colors.purple;
    case 'legendary':
      return Colors.orange;
    default:
      return Colors.grey;
  }
}

String _getRarityText(String rarity) {
  switch (rarity) {
    case 'common':
      return '普通';
    case 'rare':
      return '稀有';
    case 'epic':
      return '史诗';
    case 'legendary':
      return '传说';
    default:
      return '未知';
  }
}

统计计算函数

计算平均价值和格式化时间:

String _getAverageValue() {
  if (_userStats.totalPrizes == 0) return '0';
  return (_userStats.totalValue / _userStats.totalPrizes).toStringAsFixed(1);
}

String _formatDateTime(DateTime dateTime) {
  return '${dateTime.month}-${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}

项目总结

虚拟戳戳乐应用成功实现了完整的抽奖游戏体验,通过精心设计的概率系统、丰富的动画效果和完善的数据管理,为用户提供了充满惊喜和乐趣的游戏体验。

技术亮点

  1. 复杂动画系统:多层次动画效果,包括戳洞、奖品展示、特效动画
  2. 科学概率算法:平衡的奖品分配系统,确保游戏公平性
  3. 完整数据管理:游戏记录、统计分析、用户数据持久化
  4. 响应式UI设计:适配不同屏幕尺寸,优秀的用户体验
  5. 状态管理:复杂的游戏状态管理和动画协调

功能特色

  • 20个洞位的完整戳戳乐游戏板
  • 12种不同奖品,四个稀有度等级
  • 精美的获奖动画和提示系统
  • 详细的游戏记录和统计分析
  • 最佳奖品记录和成就系统
  • 完整的数据清理和重置功能

扩展方向

  1. 社交功能:好友系统、排行榜、分享功能
  2. 更多游戏模式:限时挑战、特殊活动、主题活动
  3. 奖品系统扩展:更多奖品类型、季节性奖品、限定奖品
  4. 音效系统:戳洞音效、获奖音效、背景音乐
  5. 数据持久化:本地存储、云端同步、跨设备数据
  6. 付费功能:购买游戏次数、特殊道具、VIP特权

通过本教程的学习,你已经掌握了Flutter游戏开发的核心技能,包括复杂动画、概率算法、状态管理和数据分析。这些技能可以应用到各种游戏和娱乐应用的开发中。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐