feat(daily): pure 7-day login-calendar logic + reward table

This commit is contained in:
2026-06-18 12:11:15 +09:00
parent 544a2b8be4
commit c185bd0886
2 changed files with 118 additions and 0 deletions
+67
View File
@@ -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<BoosterType, int> 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,
};
}
}
}
+51
View File
@@ -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');
});
}