请添加图片描述

每日挑战是数独游戏的特色功能。它为所有玩家提供同一天相同的谜题,增加了游戏的社交性和竞争性。今天我们来详细讲解如何实现每日挑战的生成算法。

在设计每日挑战之前,我们需要理解其核心要求。首先是确定性,同一天生成的谜题必须相同。其次是唯一性,不同天的谜题应该不同。最后是公平性,谜题难度应该适中且一致。

让我们从基于日期的随机种子开始。

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

Logo

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

更多推荐