Flutter for OpenHarmony数独游戏App实战:难度级别控制
label: '简单',description: '适合初学者',),label: '中等',description: '需要一些技巧',),使用配置类让难度管理更加集中和灵活。简单难度保留40-45个数字,中等难度保留30-35个数字。颜色和描述用于UI展示。int clues;});CustomDifficulty让高级玩家可以精确控制谜题参数。clues是初始数字数量,symmetricRe
难度级别是数独游戏的核心设计之一。不同难度的谜题适合不同水平的玩家,从初学者到专家都能找到适合自己的挑战。今天我们来详细讲解数独游戏的难度级别控制实现。
难度配置类
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
更多推荐


所有评论(0)