From 8290e6dc328ed45c51cfc741c0ff6f550fca059d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:16:45 +0000 Subject: [PATCH 1/3] Initial plan From d50928554379830416fb3835fcf96f33e1d76f03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:20:55 +0000 Subject: [PATCH 2/3] Add test cases for progression with current value of 0 - Added "edge case - zero current value" tests for linear gamemode stats - Added "edge case - zero current value" tests for quotient stats (fkdr, kdr) - Added "edge case - zero current value" tests for stars/exp - All tests document the actual behavior when current values are 0 - Tests cover all gamemodes (solo, doubles, threes, fours, overall) Co-authored-by: Amund211 <14028449+Amund211@users.noreply.github.com> --- src/stats/progression.test.ts | 203 ++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/src/stats/progression.test.ts b/src/stats/progression.test.ts index ee4c946a..8d83e1e7 100644 --- a/src/stats/progression.test.ts +++ b/src/stats/progression.test.ts @@ -352,6 +352,59 @@ await test("computeStatProgression - linear gamemode stats", async (t) => { }); }); + await t.test("edge case - zero current value", () => { + const startDate = new Date("2024-01-01T00:00:00Z"); + const endDate = new Date("2024-01-06T00:00:00Z"); + const startBuilder = new PlayerDataBuilder( + TEST_UUID, + startDate, + ); + const endBuilder = new PlayerDataBuilder( + TEST_UUID, + endDate, + ); + startBuilder.withGamemodeStats( + gamemode, + new StatsBuilder().withStat(stat, 100).build(), + ); + endBuilder.withGamemodeStats( + gamemode, + new StatsBuilder().withStat(stat, 0).build(), + ); + + const history: History = [ + startBuilder.build(), + endBuilder.build(), + ]; + + const result = computeStatProgression( + history, + endDate, + stat, + gamemode, + ); + + if (result.error) { + assert.fail( + `Expected success but got error: ${result.reason}`, + ); + } + + // When endValue is 0, Math.log10(0) = -Infinity, causing NaN in milestone calculations + assert.deepStrictEqual(result, { + stat, + endValue: 0, + progressPerDay: -20, + nextMilestoneValue: NaN, + daysUntilMilestone: NaN, + trendingUpward: true, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + }); + }); + await t.test("edge case - large values", () => { const startDate = new Date("2025-01-01T00:00:00Z"); const endDate = new Date("2025-01-21T00:00:00Z"); @@ -679,6 +732,65 @@ await test("computeStatProgression - quotient stats", async (t) => { }); }); + await t.test("edge case - zero current value", () => { + // When the current quotient is zero (0 dividend, non-zero divisor) + const startDate = new Date("2024-01-01T00:00:00Z"); + const endDate = new Date("2024-01-11T00:00:00Z"); + const startBuilder = new StatsBuilder(); + const endBuilder = new StatsBuilder(); + + // Start: 100/50 = 2.0 + // End: 0/75 = 0 (zero current value) + startBuilder + .withStat(dividendStat, 100) + .withStat(divisorStat, 50); + endBuilder + .withStat(dividendStat, 0) + .withStat(divisorStat, 75); + + const history: History = [ + new PlayerDataBuilder(TEST_UUID, startDate) + .withGamemodeStats( + gamemode, + startBuilder.build(), + ) + .build(), + new PlayerDataBuilder(TEST_UUID, endDate) + .withGamemodeStats(gamemode, endBuilder.build()) + .build(), + ]; + + const result = computeStatProgression( + history, + endDate, + stat, + gamemode, + ); + + if (result.error) { + assert.fail( + `Expected success but got error: ${result.reason}`, + ); + } + + // With negative session quotient, trending downward, milestone is -1 + assert.deepStrictEqual(result, { + stat, + endValue: 0, + sessionQuotient: -100 / 25, // -4.0 + progressPerDay: (-1 - 0) / 10, // -0.1 + dividendPerDay: -10, + divisorPerDay: 2.5, + nextMilestoneValue: -1, + daysUntilMilestone: 10, + trendingUpward: false, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + }); + }); + await t.test( "edge case - zero session divisor", async (t) => { @@ -1134,6 +1246,97 @@ await test("computeStatProgression - stars/experience stat", async (t) => { }); }); }); + + await t.test("edge case - zero current value", async (t) => { + // Note: This is a constructed case as new players have 500 exp/1 star + const startDate = new Date("2024-01-01T00:00:00Z"); + const endDate = new Date("2024-01-11T00:00:00Z"); // 10 days + // Start at 500 exp, lose all experience (constructed case) + const history: History = [ + new PlayerDataBuilder(TEST_UUID, startDate) + .withExperience(500) + .build(), + new PlayerDataBuilder(TEST_UUID, endDate) + .withExperience(0) + .build(), + ]; + + await t.test("stars", () => { + const result = computeStatProgression( + history, + endDate, + "stars", + gamemode, + ); + + if (result.error) { + assert.fail( + `Expected success but got error: ${result.reason}`, + ); + } + + const { + daysUntilMilestone, + progressPerDay, + ...resultWithoutTrickyFloats + } = result; + + assert.deepStrictEqual(resultWithoutTrickyFloats, { + stat: "stars", + endValue: 0, + nextMilestoneValue: 100, + trendingUpward: true, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + }); + + // (exp gained / avg exp per star) / days + const expectedStarsPerDay = -500 / 4_870 / 10; + assert.ok( + Math.abs(progressPerDay - expectedStarsPerDay) < 1e-6, + ); + + // Exp remaining until 100 stars / (exp per day) + const expectedDaysUntilMilestone = + (487_000 - 0) / (-500 / 10); + assert.ok( + Math.abs( + daysUntilMilestone - expectedDaysUntilMilestone, + ) < 1e-6, + ); + }); + + await t.test("experience", () => { + const result = computeStatProgression( + history, + endDate, + "experience", + gamemode, + ); + + if (result.error) { + assert.fail( + `Expected success but got error: ${result.reason}`, + ); + } + + // When endValue is 0, Math.log10(0) = -Infinity, causing NaN in milestone calculations + assert.deepStrictEqual(result, { + stat: "experience", + endValue: 0, + progressPerDay: -50, + nextMilestoneValue: NaN, + daysUntilMilestone: NaN, + trendingUpward: true, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + }); + }); + }); }); } }); From 41a3374633b3a5b4c9e045437293d4c97ba476b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 07:25:17 +0000 Subject: [PATCH 3/3] Update tests and implementation for zero current value case - Changed test cases: start and end values are both 0 (no decrease in stats) - Updated test expectations: milestone is 1 when value is 0, daysUntilMilestone is Infinity - Fixed implementation: handle endValue === 0 case with milestone of 1 - Removed "no progress" from error cases as it now returns valid result with infinite time - All 463 tests pass Co-authored-by: Amund211 <14028449+Amund211@users.noreply.github.com> --- src/stats/progression.test.ts | 85 ++++++++++++----------------------- src/stats/progression.ts | 37 +++++++++++++-- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/stats/progression.test.ts b/src/stats/progression.test.ts index 8d83e1e7..3b9736c3 100644 --- a/src/stats/progression.test.ts +++ b/src/stats/progression.test.ts @@ -165,24 +165,6 @@ await test("computeStatProgression - error cases", async (t) => { history: [new PlayerDataBuilder(TEST_UUID, startDate).build()], reason: ERR_TRACKING_STARTED, }, - { - name: "no progress", - history: [ - new PlayerDataBuilder(TEST_UUID, startDate) - .withGamemodeStats( - "overall", - new StatsBuilder().withStat("wins", 100).build(), - ) - .build(), - new PlayerDataBuilder(TEST_UUID, endDate) - .withGamemodeStats( - "overall", - new StatsBuilder().withStat("wins", 100).build(), - ) - .build(), - ], - reason: "No progress", - }, { name: "three element history", history: [ @@ -365,7 +347,7 @@ await test("computeStatProgression - linear gamemode stats", async (t) => { ); startBuilder.withGamemodeStats( gamemode, - new StatsBuilder().withStat(stat, 100).build(), + new StatsBuilder().withStat(stat, 0).build(), ); endBuilder.withGamemodeStats( gamemode, @@ -390,13 +372,13 @@ await test("computeStatProgression - linear gamemode stats", async (t) => { ); } - // When endValue is 0, Math.log10(0) = -Infinity, causing NaN in milestone calculations + // When endValue is 0, the milestone should be 1 assert.deepStrictEqual(result, { stat, endValue: 0, - progressPerDay: -20, - nextMilestoneValue: NaN, - daysUntilMilestone: NaN, + progressPerDay: 0, + nextMilestoneValue: 1, + daysUntilMilestone: Infinity, trendingUpward: true, trackingDataTimeInterval: { start: startDate, @@ -733,20 +715,20 @@ await test("computeStatProgression - quotient stats", async (t) => { }); await t.test("edge case - zero current value", () => { - // When the current quotient is zero (0 dividend, non-zero divisor) + // When the current quotient is zero (0 dividend, 0 divisor) const startDate = new Date("2024-01-01T00:00:00Z"); const endDate = new Date("2024-01-11T00:00:00Z"); const startBuilder = new StatsBuilder(); const endBuilder = new StatsBuilder(); - // Start: 100/50 = 2.0 - // End: 0/75 = 0 (zero current value) + // Start: 0/0 = 0 + // End: 0/0 = 0 (zero current value) startBuilder - .withStat(dividendStat, 100) - .withStat(divisorStat, 50); + .withStat(dividendStat, 0) + .withStat(divisorStat, 0); endBuilder .withStat(dividendStat, 0) - .withStat(divisorStat, 75); + .withStat(divisorStat, 0); const history: History = [ new PlayerDataBuilder(TEST_UUID, startDate) @@ -773,17 +755,17 @@ await test("computeStatProgression - quotient stats", async (t) => { ); } - // With negative session quotient, trending downward, milestone is -1 + // When endValue is 0, milestone should be 1 assert.deepStrictEqual(result, { stat, endValue: 0, - sessionQuotient: -100 / 25, // -4.0 - progressPerDay: (-1 - 0) / 10, // -0.1 - dividendPerDay: -10, - divisorPerDay: 2.5, - nextMilestoneValue: -1, - daysUntilMilestone: 10, - trendingUpward: false, + sessionQuotient: 0, + progressPerDay: 0, + dividendPerDay: 0, + divisorPerDay: 0, + nextMilestoneValue: 1, + daysUntilMilestone: Infinity, + trendingUpward: true, trackingDataTimeInterval: { start: startDate, end: endDate, @@ -1251,10 +1233,10 @@ await test("computeStatProgression - stars/experience stat", async (t) => { // Note: This is a constructed case as new players have 500 exp/1 star const startDate = new Date("2024-01-01T00:00:00Z"); const endDate = new Date("2024-01-11T00:00:00Z"); // 10 days - // Start at 500 exp, lose all experience (constructed case) + // Start at 0 exp, stay at 0 (constructed case) const history: History = [ new PlayerDataBuilder(TEST_UUID, startDate) - .withExperience(500) + .withExperience(0) .build(), new PlayerDataBuilder(TEST_UUID, endDate) .withExperience(0) @@ -1292,20 +1274,9 @@ await test("computeStatProgression - stars/experience stat", async (t) => { }, }); - // (exp gained / avg exp per star) / days - const expectedStarsPerDay = -500 / 4_870 / 10; - assert.ok( - Math.abs(progressPerDay - expectedStarsPerDay) < 1e-6, - ); - - // Exp remaining until 100 stars / (exp per day) - const expectedDaysUntilMilestone = - (487_000 - 0) / (-500 / 10); - assert.ok( - Math.abs( - daysUntilMilestone - expectedDaysUntilMilestone, - ) < 1e-6, - ); + // No progress in exp + assert.strictEqual(progressPerDay, 0); + assert.strictEqual(daysUntilMilestone, Infinity); }); await t.test("experience", () => { @@ -1322,13 +1293,13 @@ await test("computeStatProgression - stars/experience stat", async (t) => { ); } - // When endValue is 0, Math.log10(0) = -Infinity, causing NaN in milestone calculations + // When endValue is 0, milestone should be 1 assert.deepStrictEqual(result, { stat: "experience", endValue: 0, - progressPerDay: -50, - nextMilestoneValue: NaN, - daysUntilMilestone: NaN, + progressPerDay: 0, + nextMilestoneValue: 1, + daysUntilMilestone: Infinity, trendingUpward: true, trackingDataTimeInterval: { start: startDate, diff --git a/src/stats/progression.ts b/src/stats/progression.ts index a8352b6d..21ec85cb 100644 --- a/src/stats/progression.ts +++ b/src/stats/progression.ts @@ -277,10 +277,41 @@ export const computeStatProgression = ( if (increasePerDay === 0) { // Will make no progress - // TODO: Display upward milestone and infinite time + // Special case: when endValue is 0, set milestone to 1 + const nextMilestoneValue = endValue === 0 ? 1 : NaN; + const daysUntilMilestone = Infinity; + + return { + stat, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + endValue, + nextMilestoneValue, + daysUntilMilestone, + progressPerDay: increasePerDay, + trendingUpward: true, + }; + } + + // Special case: when endValue is 0, set milestone to 1 + if (endValue === 0) { + const nextMilestoneValue = 1; + const daysUntilMilestone = + (nextMilestoneValue - endValue) / increasePerDay; + return { - error: true, - reason: "No progress", + stat, + trackingDataTimeInterval: { + start: startDate, + end: endDate, + }, + endValue, + nextMilestoneValue, + daysUntilMilestone, + progressPerDay: increasePerDay, + trendingUpward: true, }; }