Flutter for OpenHarmony数独游戏App实战:每日挑战生成
本文介绍了数独游戏每日挑战功能的实现方法。核心算法通过基于日期(YYYYMMDD格式)的随机种子确保同一天生成相同谜题,同时保证不同天的谜题差异。使用回溯法和Fisher-Yates洗牌算法生成完整数独解答,然后固定挖空51个数字(保留30个)形成中等难度的谜题。系统还提供了获取每日谜题、解答和记录完成情况的功能类,实现了数独每日挑战的确定性、唯一性和公平性要求。

每日挑战是数独游戏的特色功能。它为所有玩家提供同一天相同的谜题,增加了游戏的社交性和竞争性。今天我们来详细讲解如何实现每日挑战的生成算法。
在设计每日挑战之前,我们需要理解其核心要求。首先是确定性,同一天生成的谜题必须相同。其次是唯一性,不同天的谜题应该不同。最后是公平性,谜题难度应该适中且一致。
让我们从基于日期的随机种子开始。
class DailyChallengeGenerator {
List<List<int>> generateDailyChallenge(DateTime date) {
final seed = date.year * 10000 + date.month * 100 + date.day;
final dailyRandom = Random(seed);
List<List<int>> board = List.generate(9, (_) => List.filled(9, 0));
_fillBoardWithRandom(board, dailyRandom);
return _createPuzzle(board, dailyRandom);
}
generateDailyChallenge使用日期计算随机种子。种子格式为YYYYMMDD,比如2024年3月15日的种子是20240315。使用固定种子的Random可以产生可重复的随机序列,确保同一天生成相同的谜题。
使用指定Random填充棋盘。
bool _fillBoardWithRandom(List<List<int>> board, Random random) {
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
if (board[row][col] == 0) {
List<int> numbers = List.generate(9, (i) => i + 1);
_shuffleWithRandom(numbers, random);
for (int num in numbers) {
if (_isValid(board, row, col, num)) {
board[row][col] = num;
if (_fillBoardWithRandom(board, random)) return true;
board[row][col] = 0;
}
}
return false;
}
}
}
return true;
}
void _shuffleWithRandom(List<int> list, Random random) {
for (int i = list.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
var temp = list[i];
list[i] = list[j];
list[j] = temp;
}
}
_fillBoardWithRandom使用回溯法填充棋盘,但使用传入的Random实例而不是全局Random。_shuffleWithRandom手动实现Fisher-Yates洗牌算法,使用指定的Random。这确保了使用相同种子时产生相同的填充结果。
验证数字有效性。
bool _isValid(List<List<int>> board, int row, int col, int num) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == num) return false;
}
for (int i = 0; i < 9; i++) {
if (board[i][col] == num) return false;
}
int boxRow = (row ~/ 3) * 3;
int boxCol = (col ~/ 3) * 3;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] == num) return false;
}
}
return true;
}
_isValid检查在指定位置填入数字是否违反数独规则。检查同行、同列、同宫格是否有重复。这是标准的数独验证逻辑。
创建每日挑战谜题。
List<List<int>> _createPuzzle(List<List<int>> solution, Random random) {
List<List<int>> puzzle = solution.map((row) => List<int>.from(row)).toList();
int cellsToRemove = 81 - 30; // 每日挑战固定保留30个数字
List<List<int>> positions = [];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
positions.add([i, j]);
}
}
_shufflePositionsWithRandom(positions, random);
for (int i = 0; i < cellsToRemove; i++) {
puzzle[positions[i][0]][positions[i][1]] = 0;
}
return puzzle;
}
void _shufflePositionsWithRandom(List<List<int>> positions, Random random) {
for (int i = positions.length - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
var temp = positions[i];
positions[i] = positions[j];
positions[j] = temp;
}
}
}
_createPuzzle从完整解答中挖去数字形成谜题。每日挑战固定保留30个数字,难度介于中等和困难之间。位置列表的打乱也使用dailyRandom,确保挖空位置是确定性的。
获取每日挑战的解答。
class DailyChallengeService {
static final DailyChallengeGenerator _generator = DailyChallengeGenerator();
static List<List<int>> getTodayChallenge() {
return _generator.generateDailyChallenge(DateTime.now());
}
static List<List<int>> getTodaySolution() {
DateTime today = DateTime.now();
final seed = today.year * 10000 + today.month * 100 + today.day;
final dailyRandom = Random(seed);
List<List<int>> board = List.generate(9, (_) => List.filled(9, 0));
_generator._fillBoardWithRandom(board, dailyRandom);
return board;
}
static List<List<int>> getChallengeForDate(DateTime date) {
return _generator.generateDailyChallenge(date);
}
}
DailyChallengeService提供便捷的方法获取每日挑战。getTodayChallenge获取今天的谜题,getTodaySolution获取今天的解答,getChallengeForDate获取指定日期的谜题。
每日挑战的完成记录。
class DailyChallengeRecord {
final DateTime date;
final bool completed;
final int? timeSeconds;
final int? hintsUsed;
final DateTime? completedAt;
DailyChallengeRecord({
required this.date,
this.completed = false,
this.timeSeconds,
this.hintsUsed,
this.completedAt,
});
String get dateKey => '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
Map<String, dynamic> toJson() => {
'date': date.toIso8601String(),
'completed': completed,
'timeSeconds': timeSeconds,
'hintsUsed': hintsUsed,
'completedAt': completedAt?.toIso8601String(),
};
factory DailyChallengeRecord.fromJson(Map<String, dynamic> json) => DailyChallengeRecord(
date: DateTime.parse(json['date']),
completed: json['completed'] ?? false,
timeSeconds: json['timeSeconds'],
hintsUsed: json['hintsUsed'],
completedAt: json['completedAt'] != null ? DateTime.parse(json['completedAt']) : null,
);
}
DailyChallengeRecord记录每日挑战的完成情况。dateKey生成标准格式的日期字符串,用作存储的键。包含完成状态、用时、提示次数等信息。
保存和加载每日挑战记录。
class DailyChallengeStorage {
static const String _recordsKey = 'dailyChallengeRecords';
static Future<void> saveRecord(DailyChallengeRecord record) async {
Map<String, dynamic> records = await _loadAllRecords();
records[record.dateKey] = record.toJson();
await StorageService.setJson(_recordsKey, records);
}
static Future<DailyChallengeRecord?> getRecord(DateTime date) async {
Map<String, dynamic> records = await _loadAllRecords();
String key = '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
if (records.containsKey(key)) {
return DailyChallengeRecord.fromJson(records[key]);
}
return null;
}
static Future<Map<String, dynamic>> _loadAllRecords() async {
return StorageService.getJson(_recordsKey) ?? {};
}
static Future<Set<DateTime>> getCompletedDates() async {
Map<String, dynamic> records = await _loadAllRecords();
Set<DateTime> completed = {};
records.forEach((key, value) {
if (value['completed'] == true) {
completed.add(DateTime.parse(value['date']));
}
});
return completed;
}
}
DailyChallengeStorage管理每日挑战记录的持久化。使用日期字符串作为键存储记录。getCompletedDates返回所有已完成的日期,用于在日历上显示标记。
计算连续挑战天数。
class DailyChallengeStats {
static Future<int> getCurrentStreak() async {
Set<DateTime> completed = await DailyChallengeStorage.getCompletedDates();
int streak = 0;
DateTime checkDate = DateTime.now();
while (completed.any((d) => _isSameDay(d, checkDate))) {
streak++;
checkDate = checkDate.subtract(const Duration(days: 1));
}
return streak;
}
static Future<int> getLongestStreak() async {
Set<DateTime> completed = await DailyChallengeStorage.getCompletedDates();
if (completed.isEmpty) return 0;
List<DateTime> sortedDates = completed.toList()..sort();
int longestStreak = 1;
int currentStreak = 1;
for (int i = 1; i < sortedDates.length; i++) {
if (sortedDates[i].difference(sortedDates[i - 1]).inDays == 1) {
currentStreak++;
if (currentStreak > longestStreak) {
longestStreak = currentStreak;
}
} else {
currentStreak = 1;
}
}
return longestStreak;
}
static bool _isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day;
}
}
DailyChallengeStats计算连续挑战统计。getCurrentStreak从今天开始向前检查连续完成的天数。getLongestStreak遍历所有完成记录,找出最长的连续天数。
总结一下每日挑战生成的关键设计要点。首先是确定性生成,使用日期作为随机种子确保同一天生成相同谜题。其次是完整的记录系统,保存每天的完成情况。然后是连续统计,激励玩家保持每日挑战的习惯。最后是历史查看,支持查看和补打卡过去的挑战。
每日挑战为数独游戏增添了社交性和持续性,是提升用户粘性的重要功能。
接下来我们深入讲解每日挑战的难度调整机制。
class DailyChallengeGenerator {
String getDifficultyForDate(DateTime date) {
int dayOfWeek = date.weekday;
switch (dayOfWeek) {
case DateTime.monday:
case DateTime.tuesday:
return 'Easy';
case DateTime.wednesday:
case DateTime.thursday:
return 'Medium';
case DateTime.friday:
return 'Hard';
case DateTime.saturday:
case DateTime.sunday:
return 'Expert';
default:
return 'Medium';
}
}
int getCellsToKeep(String difficulty) {
switch (difficulty) {
case 'Easy': return 40;
case 'Medium': return 33;
case 'Hard': return 28;
case 'Expert': return 23;
default: return 33;
}
}
}
getDifficultyForDate根据星期几返回不同的难度。周一周二是简单难度,适合开始新的一周。周三周四是中等难度。周五是困难难度,给工作日一个挑战性的结尾。周末是专家难度,给有时间的玩家更大的挑战。
getCellsToKeep返回不同难度保留的数字数量。简单难度保留40个数字,专家难度只保留23个。保留的数字越少,谜题越难解决。
根据日期难度生成谜题。
List<List<int>> generateDailyChallengeWithDifficulty(DateTime date) {
final seed = date.year * 10000 + date.month * 100 + date.day;
final dailyRandom = Random(seed);
List<List<int>> board = List.generate(9, (_) => List.filled(9, 0));
_fillBoardWithRandom(board, dailyRandom);
String difficulty = getDifficultyForDate(date);
int cellsToKeep = getCellsToKeep(difficulty);
return _createPuzzleWithDifficulty(board, dailyRandom, cellsToKeep);
}
List<List<int>> _createPuzzleWithDifficulty(
List<List<int>> solution,
Random random,
int cellsToKeep
) {
List<List<int>> puzzle = solution.map((row) => List<int>.from(row)).toList();
int cellsToRemove = 81 - cellsToKeep;
List<List<int>> positions = [];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
positions.add([i, j]);
}
}
_shufflePositionsWithRandom(positions, random);
for (int i = 0; i < cellsToRemove; i++) {
puzzle[positions[i][0]][positions[i][1]] = 0;
}
return puzzle;
}
generateDailyChallengeWithDifficulty结合日期种子和难度生成谜题。先用日期种子生成完整解答,然后根据当天的难度决定挖去多少数字。这样同一天的谜题对所有玩家都是相同的,但不同天的难度会有变化。
每日挑战的时间限制。
class DailyChallengeConfig {
static int getTimeLimitSeconds(String difficulty) {
switch (difficulty) {
case 'Easy': return 30 * 60; // 30分钟
case 'Medium': return 45 * 60; // 45分钟
case 'Hard': return 60 * 60; // 60分钟
case 'Expert': return 90 * 60; // 90分钟
default: return 45 * 60;
}
}
static int getHintLimit(String difficulty) {
switch (difficulty) {
case 'Easy': return 5;
case 'Medium': return 3;
case 'Hard': return 2;
case 'Expert': return 1;
default: return 3;
}
}
static int getScoreMultiplier(String difficulty) {
switch (difficulty) {
case 'Easy': return 1;
case 'Medium': return 2;
case 'Hard': return 3;
case 'Expert': return 5;
default: return 1;
}
}
}
DailyChallengeConfig定义每日挑战的配置参数。不同难度有不同的时间限制、提示次数限制和分数倍率。简单难度给更多时间和提示,专家难度限制更严格但分数更高。
计算每日挑战得分。
class DailyChallengeScoring {
static int calculateScore({
required String difficulty,
required int timeSeconds,
required int hintsUsed,
required bool completed,
}) {
if (!completed) return 0;
int baseScore = 1000;
int multiplier = DailyChallengeConfig.getScoreMultiplier(difficulty);
int timeLimit = DailyChallengeConfig.getTimeLimitSeconds(difficulty);
// 时间奖励:越快完成分数越高
double timeRatio = 1 - (timeSeconds / timeLimit);
if (timeRatio < 0) timeRatio = 0;
int timeBonus = (timeRatio * 500).round();
// 提示惩罚:每使用一次提示扣100分
int hintPenalty = hintsUsed * 100;
int finalScore = (baseScore + timeBonus - hintPenalty) * multiplier;
if (finalScore < 0) finalScore = 0;
return finalScore;
}
}
calculateScore计算每日挑战的得分。基础分1000分,根据完成时间给予奖励,使用提示会扣分。最终分数乘以难度倍率。这种计分方式鼓励玩家快速完成且少用提示。
每日挑战排行榜数据结构。
class DailyLeaderboardEntry {
final String odayKey;
final String odplayerId;
final String playerName;
final int score;
final int timeSeconds;
final int hintsUsed;
final DateTime completedAt;
DailyLeaderboardEntry({
required this.dayKey,
required this.playerId,
required this.playerName,
required this.score,
required this.timeSeconds,
required this.hintsUsed,
required this.completedAt,
});
Map<String, dynamic> toJson() => {
'dayKey': dayKey,
'playerId': playerId,
'playerName': playerName,
'score': score,
'timeSeconds': timeSeconds,
'hintsUsed': hintsUsed,
'completedAt': completedAt.toIso8601String(),
};
factory DailyLeaderboardEntry.fromJson(Map<String, dynamic> json) {
return DailyLeaderboardEntry(
dayKey: json['dayKey'],
playerId: json['playerId'],
playerName: json['playerName'],
score: json['score'],
timeSeconds: json['timeSeconds'],
hintsUsed: json['hintsUsed'],
completedAt: DateTime.parse(json['completedAt']),
);
}
}
DailyLeaderboardEntry表示排行榜中的一条记录。包含日期、玩家信息、得分、用时、提示次数等。dayKey用于区分不同日期的排行榜。toJson和fromJson支持序列化。
本地排行榜管理。
class LocalLeaderboard {
static const String _leaderboardKey = 'dailyLeaderboard';
static Future<void> submitScore(DailyLeaderboardEntry entry) async {
List<DailyLeaderboardEntry> entries = await getLeaderboard(entry.dayKey);
// 检查是否已有该玩家的记录
int existingIndex = entries.indexWhere((e) => e.playerId == entry.playerId);
if (existingIndex >= 0) {
// 只保留更高分数
if (entry.score > entries[existingIndex].score) {
entries[existingIndex] = entry;
}
} else {
entries.add(entry);
}
// 按分数排序
entries.sort((a, b) => b.score.compareTo(a.score));
// 只保留前100名
if (entries.length > 100) {
entries = entries.sublist(0, 100);
}
await _saveLeaderboard(entry.dayKey, entries);
}
static Future<List<DailyLeaderboardEntry>> getLeaderboard(String dayKey) async {
String key = '${_leaderboardKey}_$dayKey';
String? jsonStr = await StorageService.getString(key);
if (jsonStr == null) return [];
List<dynamic> jsonList = jsonDecode(jsonStr);
return jsonList.map((json) => DailyLeaderboardEntry.fromJson(json)).toList();
}
static Future<void> _saveLeaderboard(String dayKey, List<DailyLeaderboardEntry> entries) async {
String key = '${_leaderboardKey}_$dayKey';
String jsonStr = jsonEncode(entries.map((e) => e.toJson()).toList());
await StorageService.setString(key, jsonStr);
}
}
LocalLeaderboard管理本地排行榜。submitScore提交分数,如果玩家已有记录则只保留更高分。排行榜按分数降序排列,只保留前100名。每天的排行榜单独存储。
每日挑战提醒功能。
class DailyChallengeReminder {
static Future<void> scheduleReminder() async {
// 检查今天是否已完成
DateTime today = DateTime.now();
DailyChallengeRecord? record = await DailyChallengeStorage.getRecord(today);
if (record == null || !record.completed) {
// 设置晚上8点的提醒
DateTime reminderTime = DateTime(today.year, today.month, today.day, 20, 0);
if (reminderTime.isBefore(DateTime.now())) {
// 如果已过8点,不设置提醒
return;
}
// 这里可以集成本地通知插件
_scheduleNotification(
id: today.day,
title: '每日挑战等你来',
body: '今天的数独挑战还没完成哦,快来挑战吧!',
scheduledTime: reminderTime,
);
}
}
static void _scheduleNotification({
required int id,
required String title,
required String body,
required DateTime scheduledTime,
}) {
// 实际实现需要使用flutter_local_notifications等插件
debugPrint('Scheduled notification: $title at $scheduledTime');
}
}
DailyChallengeReminder管理每日挑战的提醒。检查今天是否已完成,如果没有则在晚上8点发送提醒通知。这种提醒机制可以提高用户的参与度和留存率。
每日挑战的分享功能。
class DailyChallengeShare {
static String generateShareText({
required DateTime date,
required int score,
required int timeSeconds,
required int hintsUsed,
required String difficulty,
}) {
String dateStr = '${date.month}月${date.day}日';
String timeStr = _formatTime(timeSeconds);
StringBuffer buffer = StringBuffer();
buffer.writeln('🎮 数独每日挑战 $dateStr');
buffer.writeln('难度:$difficulty');
buffer.writeln('得分:$score');
buffer.writeln('用时:$timeStr');
buffer.writeln('提示:${hintsUsed}次');
buffer.writeln('');
buffer.writeln('来挑战我吧!');
return buffer.toString();
}
static String _formatTime(int seconds) {
int minutes = seconds ~/ 60;
int secs = seconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
}
static Future<void> shareResult({
required DateTime date,
required int score,
required int timeSeconds,
required int hintsUsed,
required String difficulty,
}) async {
String text = generateShareText(
date: date,
score: score,
timeSeconds: timeSeconds,
hintsUsed: hintsUsed,
difficulty: difficulty,
);
// 使用share_plus插件分享
// await Share.share(text);
debugPrint('Share text: $text');
}
}
DailyChallengeShare生成分享文本。包含日期、难度、得分、用时、提示次数等信息。使用emoji增加趣味性。分享功能可以让玩家炫耀成绩,也能吸引新玩家。
每日挑战的成就系统。
class DailyChallengeAchievements {
static List<Achievement> checkAchievements(DailyChallengeStats stats) {
List<Achievement> unlocked = [];
if (stats.currentStreak >= 7) {
unlocked.add(Achievement(
id: 'weekly_streak',
title: '周挑战者',
description: '连续完成7天每日挑战',
icon: Icons.calendar_today,
));
}
if (stats.currentStreak >= 30) {
unlocked.add(Achievement(
id: 'monthly_streak',
title: '月挑战者',
description: '连续完成30天每日挑战',
icon: Icons.calendar_month,
));
}
if (stats.totalCompleted >= 100) {
unlocked.add(Achievement(
id: 'century',
title: '百日挑战',
description: '累计完成100次每日挑战',
icon: Icons.emoji_events,
));
}
if (stats.perfectGames >= 10) {
unlocked.add(Achievement(
id: 'perfect_ten',
title: '完美十连',
description: '完成10次不使用提示的每日挑战',
icon: Icons.star,
));
}
return unlocked;
}
}
class Achievement {
final String id;
final String title;
final String description;
final IconData icon;
Achievement({
required this.id,
required this.title,
required this.description,
required this.icon,
});
}
DailyChallengeAchievements检查玩家是否解锁了成就。包括连续挑战、累计完成、完美通关等类型的成就。成就系统给玩家额外的目标和动力。
总结一下每日挑战生成的完整功能体系。首先是确定性生成算法,确保同一天所有玩家面对相同谜题。其次是难度调整机制,不同日期有不同难度。然后是完整的记录和统计系统。接着是排行榜和分享功能增加社交性。最后是成就系统提供长期目标。
每日挑战是数独游戏的核心社交功能,它让玩家有理由每天回来,与朋友比较成绩,追求更高的连续记录。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)