diff --git a/src/constants/judgment.ts b/src/constants/judgment.ts index a9ff2ec..5b24a2e 100644 --- a/src/constants/judgment.ts +++ b/src/constants/judgment.ts @@ -33,14 +33,19 @@ export function judgeByCounts(correct: number, t: Thresholds): DailyJudgment { return "fail"; } +/** + * 스트릭 유지·증가 시 지급되는 보너스 포인트 상한. + * 상한이 있어야 시즌을 넘겨 이월된 장기 스트릭의 이득이 + * "초반 램프업 생략" 수준(최대 ~105pt)으로 제한된다 — 이월은 의도적으로 허용. + */ +export const STREAK_BONUS_CAP = 15; + /** * 스트릭 유지·증가 시 지급되는 보너스 포인트. - * 스트릭이 길수록 가산폭이 커지도록 완만한 계단 함수. + * 스트릭 일수만큼 매일 가산하되 [STREAK_BONUS_CAP]에서 멈추는 일별 점증 구조 — + * 기본 적립(적중 수 × 10) 대비 30~50%까지 커져 스트릭을 지킬 유인을 만든다. */ export function streakBonus(streakAfter: number): number { - if (streakAfter >= 30) return 20; - if (streakAfter >= 14) return 10; - if (streakAfter >= 7) return 5; - if (streakAfter >= 3) return 2; - return 0; + if (streakAfter <= 0) return 0; + return Math.min(streakAfter, STREAK_BONUS_CAP); } diff --git a/tests/services/judgmentService.test.ts b/tests/services/judgmentService.test.ts index 4f1c05d..9bb5e38 100644 --- a/tests/services/judgmentService.test.ts +++ b/tests/services/judgmentService.test.ts @@ -135,7 +135,7 @@ describe("judgmentService (Firestore emulator)", () => { expect(hist.streakAfter).toBe(1); expect(user.currentStreak).toBe(1); expect(user.highestStreak).toBe(1); - expect(user.tierPoints).toBe(50); // 5 * 10, streak 1이라 보너스 0 + expect(user.tierPoints).toBe(51); // 5 * 10 + 스트릭 보너스 min(1, 15) expect(user.lastJudgedDate).toBe(TUE); }); @@ -157,7 +157,7 @@ describe("judgmentService (Firestore emulator)", () => { expect(hist.judgment).toBe("success"); expect(user.currentStreak).toBe(1); - expect(user.tierPoints).toBe(30); + expect(user.tierPoints).toBe(31); // 3 * 10 + 스트릭 보너스 1 }); it("5경기·2적중은 fail, 스트릭 0 유지, 포인트 불변", async () => { @@ -382,7 +382,7 @@ describe("judgmentService (Firestore emulator)", () => { const user = await readUser(); expect(user.currentStreak).toBe(1); - expect(user.tierPoints).toBe(50); + expect(user.tierPoints).toBe(51); }); });