From c185bd0886b2a33e6daa4f3997c066f038f118e4 Mon Sep 17 00:00:00 2001 From: airkjw Date: Thu, 18 Jun 2026 12:11:15 +0900 Subject: [PATCH] feat(daily): pure 7-day login-calendar logic + reward table --- lib/game/daily/daily_reward.dart | 67 ++++++++++++++++++++++++++ test/game/daily/daily_reward_test.dart | 51 ++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 lib/game/daily/daily_reward.dart create mode 100644 test/game/daily/daily_reward_test.dart diff --git a/lib/game/daily/daily_reward.dart b/lib/game/daily/daily_reward.dart new file mode 100644 index 0000000..5ac338e --- /dev/null +++ b/lib/game/daily/daily_reward.dart @@ -0,0 +1,67 @@ +// lib/game/daily/daily_reward.dart +import '../models/booster.dart'; + +/// The result of evaluating the calendar for "today". +class DailyResolution { + const DailyResolution({required this.claimable, required this.day}); + final bool claimable; + final int day; // 1..7, the day to show/grant +} + +/// Pure 7-day login-calendar logic. No storage or clock — callers pass the +/// persisted state and today's date so it is fully unit-testable. +class DailyRewardCalendar { + const DailyRewardCalendar(); + + static const int cycle = 7; + + String ymd(DateTime date) { + final m = date.month.toString().padLeft(2, '0'); + final d = date.day.toString().padLeft(2, '0'); + return '${date.year}-$m-$d'; + } + + DailyResolution resolve({ + required String? lastClaimedYmd, + required int storedDay, + required DateTime today, + }) { + final todayYmd = ymd(today); + if (lastClaimedYmd == todayYmd) { + return DailyResolution(claimable: false, day: storedDay); + } + final yesterday = ymd(today.subtract(const Duration(days: 1))); + final int day; + if (lastClaimedYmd == yesterday) { + day = (storedDay % cycle) + 1; // advance, wrapping 7 -> 1 + } else { + day = 1; // first ever, or a day was missed + } + return DailyResolution(claimable: true, day: day); + } + + /// Boosters granted for a given calendar day (1..7). Tunable. + Map rewardFor(int day) { + switch (day) { + case 1: + return {BoosterType.hammer: 1}; + case 2: + return {BoosterType.shuffle: 1}; + case 3: + return {BoosterType.lineBomb: 1}; + case 4: + return {BoosterType.hammer: 1, BoosterType.shuffle: 1}; + case 5: + return {BoosterType.shuffle: 1, BoosterType.lineBomb: 1}; + case 6: + return {BoosterType.hammer: 1, BoosterType.lineBomb: 1}; + case 7: + default: + return { + BoosterType.hammer: 2, + BoosterType.shuffle: 2, + BoosterType.lineBomb: 2, + }; + } + } +} diff --git a/test/game/daily/daily_reward_test.dart b/test/game/daily/daily_reward_test.dart new file mode 100644 index 0000000..9bdb873 --- /dev/null +++ b/test/game/daily/daily_reward_test.dart @@ -0,0 +1,51 @@ +import 'package:block_seasons/game/daily/daily_reward.dart'; +import 'package:block_seasons/game/models/booster.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + final cal = DailyRewardCalendar(); + + DateTime d(int day) => DateTime(2026, 6, day); + + test('first ever claim is day 1', () { + final r = cal.resolve(lastClaimedYmd: null, storedDay: 0, today: d(18)); + expect(r.claimable, isTrue); + expect(r.day, 1); + }); + + test('claiming again the same day is not claimable', () { + final r = cal.resolve( + lastClaimedYmd: '2026-06-18', storedDay: 1, today: d(18)); + expect(r.claimable, isFalse); + }); + + test('a consecutive day advances the calendar', () { + final r = cal.resolve( + lastClaimedYmd: '2026-06-18', storedDay: 3, today: d(19)); + expect(r.claimable, isTrue); + expect(r.day, 4); + }); + + test('day 7 wraps back to day 1', () { + final r = cal.resolve( + lastClaimedYmd: '2026-06-18', storedDay: 7, today: d(19)); + expect(r.day, 1); + }); + + test('a missed day resets to day 1', () { + final r = cal.resolve( + lastClaimedYmd: '2026-06-18', storedDay: 4, today: d(20)); + expect(r.claimable, isTrue); + expect(r.day, 1); + }); + + test('reward table covers days 1..7 and day 7 is the jackpot', () { + expect(cal.rewardFor(1), {BoosterType.hammer: 1}); + expect(cal.rewardFor(7), + {BoosterType.hammer: 2, BoosterType.shuffle: 2, BoosterType.lineBomb: 2}); + }); + + test('ymd formats a date as YYYY-MM-DD', () { + expect(cal.ymd(DateTime(2026, 6, 7)), '2026-06-07'); + }); +}