难度级别是数独游戏的核心设计之一。不同难度的谜题适合不同水平的玩家,从初学者到专家都能找到适合自己的挑战。今天我们来详细讲解数独游戏的难度级别控制实现。

难度配置类

class DifficultyConfig {
  final String name;
  final String label;
  final int minClues;
  final int maxClues;
  final Color color;
  final String description;
  
  const DifficultyConfig({
    required this.name,
    required this.label,
    required this.minClues,
    required this.maxClues,
    required this.color,
    required this.description,
  });
}

DifficultyConfig定义了每个难度级别的配置。minClues和maxClues定义了初始数字的范围。color用于UI显示。description提供简短的难度说明。

难度列表定义

const List<DifficultyConfig> difficulties = [
  DifficultyConfig(
    name: 'Easy',
    label: '简单',
    minClues: 40,
    maxClues: 45,
    color: Colors.green,
    description: '适合初学者',
  ),
  DifficultyConfig(
    name: 'Medium',
    label: '中等',
    minClues: 30,
    maxClues: 35,
    color: Colors.blue,
    description: '需要一些技巧',
  ),

使用配置类让难度管理更加集中和灵活。简单难度保留40-45个数字,中等难度保留30-35个数字。颜色和描述用于UI展示。

更高难度配置

  DifficultyConfig(
    name: 'Hard',
    label: '困难',
    minClues: 25,
    maxClues: 30,
    color: Colors.orange,
    description: '需要高级技巧',
  ),
  DifficultyConfig(
    name: 'Expert',
    label: '专家',
    minClues: 20,
    maxClues: 25,
    color: Colors.red,
    description: '极具挑战性',
  ),
];

困难和专家难度保留更少的数字,需要更复杂的推理技巧。橙色和红色表示更高的挑战性。

计算需要移除的数字

int _getCellsToRemove(String difficulty) {
  switch (difficulty) {
    case 'Easy': return 81 - 43;
    case 'Medium': return 81 - 33;
    case 'Hard': return 81 - 28;
    case 'Expert': return 81 - 23;
    default: return 81 - 43;
  }
}

_getCellsToRemove根据难度返回需要挖去的数字数量。数独共81个格子,简单级别保留43个数字,专家级别只保留23个。

创建谜题

List<List<int>> createPuzzle(List<List<int>> solution, String difficulty) {
  List<List<int>> puzzle = solution.map((row) => List<int>.from(row)).toList();
  int cellsToRemove = _getCellsToRemove(difficulty);
  
  List<List<int>> positions = [];
  for (int i = 0; i < 9; i++) {
    for (int j = 0; j < 9; j++) {
      positions.add([i, j]);
    }
  }
  positions.shuffle(_random);

createPuzzle从完整解答中随机挖去指定数量的数字形成谜题。首先复制解答,然后生成所有位置并打乱顺序。

执行挖空

  int removed = 0;
  for (var pos in positions) {
    if (removed >= cellsToRemove) break;
    puzzle[pos[0]][pos[1]] = 0;
    removed++;
  }
  return puzzle;
}

按打乱后的顺序依次将格子设为0(空),直到达到目标数量。这种随机挖空保证了谜题的多样性。

难度选择器UI

Widget _buildDifficultySelector() {
  return GetBuilder<GameController>(
    builder: (controller) => Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: difficulties.map((config) {
        bool isSelected = controller.difficulty == config.name;
        return GestureDetector(
          onTap: () => controller.generateNewGame(config.name),
          child: Container(
            padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),

难度选择器显示四个难度按钮。GetBuilder监听controller的变化,isSelected判断当前选中的难度。点击按钮会生成对应难度的新游戏。

难度按钮样式

            decoration: BoxDecoration(
              color: isSelected ? config.color : config.color.withOpacity(0.1),
              borderRadius: BorderRadius.circular(20.r),
              border: Border.all(
                color: config.color,
                width: isSelected ? 2 : 1,
              ),
            ),
            child: Text(
              config.label,
              style: TextStyle(
                fontSize: 14.sp,
                fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
                color: isSelected ? Colors.white : config.color,
              ),
            ),
          ),
        );
      }).toList(),
    ),
  );
}

选中的难度使用实心背景和白色文字,未选中的使用浅色背景和彩色文字。圆角和边框让按钮更加美观。

难度标签组件

Widget _buildDifficultyBadge(String difficulty) {
  DifficultyConfig config = difficulties.firstWhere(
    (d) => d.name == difficulty,
    orElse: () => difficulties.first,
  );
  
  return Container(
    padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
    decoration: BoxDecoration(
      color: config.color.withOpacity(0.1),
      borderRadius: BorderRadius.circular(16.r),
      border: Border.all(color: config.color.withOpacity(0.3)),
    ),

难度标签在游戏界面显示当前难度。firstWhere从配置列表中找到对应的配置,orElse处理找不到的情况。

标签文字

    child: Text(
      config.label,
      style: TextStyle(
        fontSize: 14.sp,
        fontWeight: FontWeight.bold,
        color: config.color,
      ),
    ),
  );
}

使用配置中的颜色和标签,保持一致性。粗体字让标签更加醒目。

难度统计类

class DifficultyStats {
  Map<String, int> gamesByDifficulty = {};
  Map<String, int> winsByDifficulty = {};
  Map<String, int> bestTimeByDifficulty = {};
  Map<String, int> totalTimeByDifficulty = {};

DifficultyStats按难度分类统计游戏数据。使用Map存储各难度的游戏数、胜利数、最佳时间和总时间。

记录游戏

  void recordGame(String difficulty, bool won, int timeSeconds) {
    gamesByDifficulty[difficulty] = (gamesByDifficulty[difficulty] ?? 0) + 1;
    
    if (won) {
      winsByDifficulty[difficulty] = (winsByDifficulty[difficulty] ?? 0) + 1;
      
      int? currentBest = bestTimeByDifficulty[difficulty];
      if (currentBest == null || timeSeconds < currentBest) {
        bestTimeByDifficulty[difficulty] = timeSeconds;
      }
      
      totalTimeByDifficulty[difficulty] = 
          (totalTimeByDifficulty[difficulty] ?? 0) + timeSeconds;
    }
  }

recordGame记录一局游戏的结果。无论输赢都增加游戏数,胜利时更新胜利数、最佳时间和总时间。

计算胜率和平均时间

  double getWinRate(String difficulty) {
    int games = gamesByDifficulty[difficulty] ?? 0;
    int wins = winsByDifficulty[difficulty] ?? 0;
    return games > 0 ? wins / games : 0;
  }
  
  int? getAverageTime(String difficulty) {
    int? total = totalTimeByDifficulty[difficulty];
    int? wins = winsByDifficulty[difficulty];
    if (total == null || wins == null || wins == 0) return null;
    return total ~/ wins;
  }
}

getWinRate计算胜率,getAverageTime计算平均用时。这些数据可以帮助玩家了解自己在各难度上的表现。

难度推荐

String recommendDifficulty(DifficultyStats stats) {
  for (var config in difficulties) {
    double winRate = stats.getWinRate(config.name);
    int games = stats.gamesByDifficulty[config.name] ?? 0;
    
    if (games < 5) {
      return config.name;
    }
    
    if (winRate < 0.5) {
      return config.name;
    }
  }
  
  return 'Expert';
}

recommendDifficulty根据玩家的历史表现推荐合适的难度。如果某个难度玩的次数少于5次或胜率低于50%,推荐继续玩这个难度。

自定义难度类

class CustomDifficulty {
  int clues;
  bool symmetricRemoval;
  bool guaranteeUniqueSolution;
  
  CustomDifficulty({
    this.clues = 30,
    this.symmetricRemoval = true,
    this.guaranteeUniqueSolution = true,
  });
}

CustomDifficulty让高级玩家可以精确控制谜题参数。clues是初始数字数量,symmetricRemoval控制是否对称挖空。

自定义难度对话框

Widget _buildCustomDifficultyDialog() {
  CustomDifficulty custom = CustomDifficulty();
  
  return StatefulBuilder(
    builder: (context, setState) => AlertDialog(
      title: const Text('自定义难度'),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text('初始数字: ${custom.clues}'),
          Slider(
            value: custom.clues.toDouble(),
            min: 17,
            max: 50,
            divisions: 33,
            onChanged: (value) {
              setState(() => custom.clues = value.round());
            },
          ),

StatefulBuilder让对话框内部可以使用setState。滑块控制初始数字数量,17是数独有唯一解的最小线索数。

对话框按钮

          SwitchListTile(
            title: const Text('对称挖空'),
            value: custom.symmetricRemoval,
            onChanged: (value) {
              setState(() => custom.symmetricRemoval = value);
            },
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
            controller.generateCustomGame(custom);
          },
          child: const Text('开始'),
        ),
      ],
    ),
  );
}

开关控制是否对称挖空。点击开始按钮生成自定义难度的游戏。

难度进度追踪

class DifficultyProgress {
  String difficulty;
  int gamesPlayed;
  int gamesWon;
  int bestTime;
  int averageTime;
  bool isUnlocked;
  
  DifficultyProgress({
    required this.difficulty,
    this.gamesPlayed = 0,
    this.gamesWon = 0,
    this.bestTime = 0,
    this.averageTime = 0,
    this.isUnlocked = false,
  });

DifficultyProgress追踪玩家在每个难度上的进度。包括游戏数、胜利数、最佳时间等数据。

计算掌握程度

  double get winRate => gamesPlayed > 0 ? gamesWon / gamesPlayed : 0;
  
  String get masteryLevel {
    if (gamesWon >= 50 && winRate >= 0.8) return '大师';
    if (gamesWon >= 20 && winRate >= 0.6) return '专家';
    if (gamesWon >= 10 && winRate >= 0.4) return '熟练';
    if (gamesWon >= 5) return '入门';
    return '新手';
  }
}

winRate计算胜率,masteryLevel根据游戏数和胜率判断掌握程度。这些数据可以帮助玩家了解自己的成长轨迹。

难度解锁服务

class DifficultyUnlockService {
  static bool isDifficultyUnlocked(String difficulty, DifficultyStats stats) {
    switch (difficulty) {
      case 'Easy':
        return true;
      case 'Medium':
        int easyWins = stats.winsByDifficulty['Easy'] ?? 0;
        return easyWins >= 3;
      case 'Hard':
        int mediumWins = stats.winsByDifficulty['Medium'] ?? 0;
        return mediumWins >= 5;
      case 'Expert':
        int hardWins = stats.winsByDifficulty['Hard'] ?? 0;
        return hardWins >= 10;
      default:
        return false;
    }
  }

DifficultyUnlockService管理难度解锁逻辑。简单难度默认解锁,其他难度需要在前一个难度完成一定数量的游戏才能解锁。

获取解锁条件

  static String getUnlockRequirement(String difficulty) {
    switch (difficulty) {
      case 'Medium':
        return '完成3局简单难度';
      case 'Hard':
        return '完成5局中等难度';
      case 'Expert':
        return '完成10局困难难度';
      default:
        return '';
    }
  }
}

getUnlockRequirement返回解锁条件的文字描述,用于UI显示。这种渐进式解锁设计可以引导新玩家从简单难度开始。

带锁定状态的难度按钮

Widget _buildDifficultyButton(DifficultyConfig config, DifficultyStats stats) {
  bool isUnlocked = DifficultyUnlockService.isDifficultyUnlocked(
    config.name, stats
  );
  bool isSelected = controller.difficulty == config.name;
  
  return GestureDetector(
    onTap: isUnlocked 
        ? () => controller.generateNewGame(config.name)
        : () => _showUnlockDialog(config),
    child: Container(
      padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),

未解锁的难度点击后显示解锁条件对话框,而不是开始游戏。isUnlocked决定点击行为。

锁定状态样式

      decoration: BoxDecoration(
        color: isUnlocked
            ? (isSelected ? config.color : config.color.withOpacity(0.1))
            : Colors.grey.shade300,
        borderRadius: BorderRadius.circular(12.r),
        border: Border.all(
          color: isUnlocked ? config.color : Colors.grey,
          width: isSelected ? 2 : 1,
        ),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (!isUnlocked)
            Icon(Icons.lock, size: 16.sp, color: Colors.grey),
          if (!isUnlocked) SizedBox(width: 4.w),

未解锁的难度显示锁图标和灰色样式。这种视觉区分让玩家清楚知道哪些难度可以选择。

按钮文字

          Text(
            config.label,
            style: TextStyle(
              fontSize: 14.sp,
              fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
              color: isUnlocked
                  ? (isSelected ? Colors.white : config.color)
                  : Colors.grey,
            ),
          ),
        ],
      ),
    ),
  );
}

未解锁的难度文字也使用灰色,与锁图标保持一致。选中的难度使用白色文字。

解锁提示对话框

void _showUnlockDialog(DifficultyConfig config) {
  String requirement = DifficultyUnlockService.getUnlockRequirement(config.name);
  
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Row(
        children: [
          Icon(Icons.lock, color: config.color),
          SizedBox(width: 8.w),
          Text('${config.label}难度已锁定'),
        ],
      ),
      content: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('解锁条件:'),
          SizedBox(height: 8.h),
          Text(
            requirement,
            style: TextStyle(
              fontWeight: FontWeight.bold,
              color: config.color,
            ),
          ),
        ],
      ),

解锁对话框清晰地告诉玩家需要完成什么才能解锁这个难度。使用难度对应的颜色保持视觉一致性。

对话框按钮

      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('知道了'),
        ),
      ],
    ),
  );
}

简洁的设计让玩家快速理解解锁条件。只有一个按钮关闭对话框。

难度对比视图

Widget _buildDifficultyComparison(DifficultyStats stats) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('难度对比', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold)),
      SizedBox(height: 16.h),
      ...difficulties.map((config) {
        int games = stats.gamesByDifficulty[config.name] ?? 0;
        int wins = stats.winsByDifficulty[config.name] ?? 0;
        double winRate = games > 0 ? wins / games : 0;

难度对比视图用进度条展示各难度的胜率。遍历所有难度配置,计算每个难度的胜率。

进度条显示

        return Padding(
          padding: EdgeInsets.only(bottom: 12.h),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Text(config.label, style: TextStyle(color: config.color)),
                  Text('${(winRate * 100).toStringAsFixed(0)}%'),
                ],
              ),
              SizedBox(height: 4.h),
              LinearProgressIndicator(
                value: winRate,
                backgroundColor: Colors.grey.shade200,
                valueColor: AlwaysStoppedAnimation<Color>(config.color),
              ),
            ],
          ),
        );
      }).toList(),
    ],
  );
}

每个难度使用对应的颜色,让玩家一眼就能看出自己在哪个难度表现最好。百分比显示具体胜率。

动态难度推荐

class AdaptiveDifficulty {
  static String suggestDifficulty(DifficultyStats stats) {
    for (int i = difficulties.length - 1; i >= 0; i--) {
      String diff = difficulties[i].name;
      int games = stats.gamesByDifficulty[diff] ?? 0;
      int wins = stats.winsByDifficulty[diff] ?? 0;
      
      if (games >= 5) {
        double winRate = wins / games;
        if (winRate >= 0.7) {
          if (i < difficulties.length - 1) {
            return difficulties[i + 1].name;
          }
          return diff;
        } else if (winRate >= 0.4) {
          return diff;
        }
      }
    }
    
    return 'Easy';
  }

AdaptiveDifficulty根据玩家表现推荐合适的难度。从最高难度开始检查,胜率高于70%推荐更高难度,40%-70%保持当前难度。

难度建议

  static String getDifficultyAdvice(String difficulty, DifficultyStats stats) {
    int games = stats.gamesByDifficulty[difficulty] ?? 0;
    int wins = stats.winsByDifficulty[difficulty] ?? 0;
    
    if (games < 5) {
      return '继续练习,积累经验';
    }
    
    double winRate = wins / games;
    if (winRate >= 0.8) {
      return '表现出色!可以尝试更高难度';
    } else if (winRate >= 0.5) {
      return '稳步提升中,继续保持';
    } else {
      return '多加练习,注意使用笔记功能';
    }
  }
}

getDifficultyAdvice给出针对性的建议。根据游戏数和胜率返回不同的建议文字,帮助玩家找到最佳的挑战水平。

难度切换确认

void _confirmDifficultyChange(String newDifficulty) {
  if (controller.elapsedSeconds > 0 && !controller.isComplete) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('切换难度'),
        content: const Text('当前游戏进度将丢失,确定要切换难度吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              controller.generateNewGame(newDifficulty);
            },
            child: const Text('确定'),
          ),
        ],
      ),
    );
  } else {
    controller.generateNewGame(newDifficulty);
  }
}

如果当前有进行中的游戏,切换难度前会弹出确认对话框。这可以防止玩家误操作丢失游戏进度。

难度成就类

class DifficultyAchievement {
  String id;
  String title;
  String description;
  String difficulty;
  int requirement;
  bool Function(DifficultyStats) checkUnlocked;
  
  DifficultyAchievement({
    required this.id,
    required this.title,
    required this.description,
    required this.difficulty,
    required this.requirement,
    required this.checkUnlocked,
  });
}

DifficultyAchievement定义与难度相关的成就。checkUnlocked是一个函数,用于检查成就是否已解锁。

成就列表

List<DifficultyAchievement> difficultyAchievements = [
  DifficultyAchievement(
    id: 'easy_master',
    title: '简单大师',
    description: '在简单难度完成50局',
    difficulty: 'Easy',
    requirement: 50,
    checkUnlocked: (stats) => (stats.winsByDifficulty['Easy'] ?? 0) >= 50,
  ),
  DifficultyAchievement(
    id: 'expert_first',
    title: '专家初体验',
    description: '首次完成专家难度',
    difficulty: 'Expert',
    requirement: 1,
    checkUnlocked: (stats) => (stats.winsByDifficulty['Expert'] ?? 0) >= 1,
  ),
];

成就列表定义各种难度相关的成就。每个成就有唯一ID、标题、描述和解锁条件。这种设计让成就系统更加灵活。

总结

难度级别控制的关键设计要点:配置化管理(使用配置类集中管理难度参数)、清晰的视觉区分(不同难度使用不同颜色)、统计支持(按难度分类统计游戏数据)、解锁系统(引导玩家循序渐进)、智能推荐(根据表现推荐合适难度)、成就系统(增加游戏的长期目标)。

难度系统是数独游戏的核心设计,它让不同水平的玩家都能找到适合自己的挑战,保持游戏的趣味性和挑战性。通过合理的难度设计,我们可以让玩家在游戏中不断成长,享受解谜的乐趣。

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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐