/// Daily streak: at least one stage attempt (win or lose) per local /// calendar day keeps it alive. No clock-cheat defense — single-player, /// low stakes. class StreakState { const StreakState({ required this.current, required this.best, required this.lastYmd, this.hitMilestone, }); static const initial = StreakState(current: 0, best: 0, lastYmd: null); static const milestones = [3, 7, 14, 30]; final int current; final int best; /// Local date of the last counted play, as `yyyy-MM-dd`. final String? lastYmd; /// Set when this advance just reached a milestone (celebrate once). final int? hitMilestone; } String _ymd(DateTime d) => '${d.year.toString().padLeft(4, '0')}-' '${d.month.toString().padLeft(2, '0')}-' '${d.day.toString().padLeft(2, '0')}'; StreakState advanceStreak(StreakState state, DateTime now) { final today = _ymd(now); if (state.lastYmd == today) { return StreakState( current: state.current, best: state.best, lastYmd: today, ); } // Normalized constructor handles month/year boundaries (and DST). final yesterday = _ymd(DateTime(now.year, now.month, now.day - 1)); final current = state.lastYmd == yesterday ? state.current + 1 : 1; return StreakState( current: current, best: current > state.best ? current : state.best, lastYmd: today, hitMilestone: StreakState.milestones.contains(current) ? current : null, ); }