From be033f93c4d5428a8ba002da4ac9f5b7487ea2c8 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 7 Apr 2026 20:59:46 +0700 Subject: [PATCH] feat: add score calculation with speed bonus and combo Co-Authored-By: Claude Sonnet 4.6 --- superpowers/src/game/scoring.ts | 14 ++++++++++++ superpowers/tests/game/scoring.test.ts | 31 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 superpowers/src/game/scoring.ts create mode 100644 superpowers/tests/game/scoring.test.ts diff --git a/superpowers/src/game/scoring.ts b/superpowers/src/game/scoring.ts new file mode 100644 index 0000000..313b4b5 --- /dev/null +++ b/superpowers/src/game/scoring.ts @@ -0,0 +1,14 @@ +import { BASE_MATCH_SCORE, SPEED_BONUS_MAX, SPEED_BONUS_WINDOW_MS } from "./constants"; + +export function calculateMatchScore( + msSinceLastMatch: number, + combo: number +): number { + let speedBonus = 0; + if (msSinceLastMatch < SPEED_BONUS_WINDOW_MS) { + const ratio = 1 - msSinceLastMatch / SPEED_BONUS_WINDOW_MS; + speedBonus = Math.round(SPEED_BONUS_MAX * ratio); + } + + return (BASE_MATCH_SCORE + speedBonus) * combo; +} diff --git a/superpowers/tests/game/scoring.test.ts b/superpowers/tests/game/scoring.test.ts new file mode 100644 index 0000000..8b6d297 --- /dev/null +++ b/superpowers/tests/game/scoring.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { calculateMatchScore } from "../../src/game/scoring"; + +describe("calculateMatchScore", () => { + it("returns base score with no speed bonus and combo 1", () => { + const score = calculateMatchScore(10000, 1); + expect(score).toBe(100); + }); + + it("adds speed bonus for fast match", () => { + const score = calculateMatchScore(1000, 1); + expect(score).toBeGreaterThan(100); + expect(score).toBeLessThanOrEqual(150); + }); + + it("applies combo multiplier", () => { + const base = calculateMatchScore(10000, 1); + const combo3 = calculateMatchScore(10000, 3); + expect(combo3).toBe(base * 3); + }); + + it("combines speed bonus and combo", () => { + const score = calculateMatchScore(0, 2); + expect(score).toBe((100 + 50) * 2); + }); + + it("no speed bonus after window expires", () => { + const score = calculateMatchScore(6000, 1); + expect(score).toBe(100); + }); +});