/// Pure-Dart interstitial gate. No plugin imports so the monetization rules /// are unit-tested headlessly. The service layer feeds it lifecycle events /// and asks [canShowInterstitial] before every stage-end interstitial. class AdFrequencyPolicy { AdFrequencyPolicy({ this.minStagesBetween = 3, this.minInterval = const Duration(seconds: 90), this.firstStagesProtected = 5, }); final int minStagesBetween; final Duration minInterval; final int firstStagesProtected; int _totalStagesCompleted = 0; int _stagesSinceLastInterstitial = 0; bool _rewardedShownThisRound = false; DateTime? _lastInterstitialAt; /// A new stage attempt began (fresh round). Clears the per-round rewarded /// flag so last round's rewarded does not block this round's interstitial. void onRoundStart() => _rewardedShownThisRound = false; /// A stage finished (won or lost). Drives both counters. void onStageCompleted() { _totalStagesCompleted++; _stagesSinceLastInterstitial++; } /// The player watched a rewarded ad in the current round. void onRewardedShown() => _rewardedShownThisRound = true; /// An interstitial was actually shown. Resets the spacing counters. void onInterstitialShown(DateTime now) { _stagesSinceLastInterstitial = 0; _lastInterstitialAt = now; } bool canShowInterstitial(DateTime now) { if (_totalStagesCompleted <= firstStagesProtected) return false; if (_rewardedShownThisRound) return false; if (_stagesSinceLastInterstitial < minStagesBetween) return false; final last = _lastInterstitialAt; if (last != null && now.difference(last) < minInterval) return false; return true; } }