Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 192 additions & 18 deletions src/stats/progression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -352,6 +334,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, 0).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, the milestone should be 1
assert.deepStrictEqual(result, {
stat,
endValue: 0,
progressPerDay: 0,
nextMilestoneValue: 1,
daysUntilMilestone: Infinity,
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");
Expand Down Expand Up @@ -679,6 +714,65 @@ await test("computeStatProgression - quotient stats", async (t) => {
});
});

await t.test("edge case - zero current value", () => {
// 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: 0/0 = 0
// End: 0/0 = 0 (zero current value)
startBuilder
.withStat(dividendStat, 0)
.withStat(divisorStat, 0);
endBuilder
.withStat(dividendStat, 0)
.withStat(divisorStat, 0);

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}`,
);
}

// When endValue is 0, milestone should be 1
assert.deepStrictEqual(result, {
stat,
endValue: 0,
sessionQuotient: 0,
progressPerDay: 0,
dividendPerDay: 0,
divisorPerDay: 0,
nextMilestoneValue: 1,
daysUntilMilestone: Infinity,
trendingUpward: true,
trackingDataTimeInterval: {
start: startDate,
end: endDate,
},
});
});

await t.test(
"edge case - zero session divisor",
async (t) => {
Expand Down Expand Up @@ -1134,6 +1228,86 @@ 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 0 exp, stay at 0 (constructed case)
const history: History = [
new PlayerDataBuilder(TEST_UUID, startDate)
.withExperience(0)
.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,
},
});

// No progress in exp
assert.strictEqual(progressPerDay, 0);
assert.strictEqual(daysUntilMilestone, Infinity);
});

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, milestone should be 1
assert.deepStrictEqual(result, {
stat: "experience",
endValue: 0,
progressPerDay: 0,
nextMilestoneValue: 1,
daysUntilMilestone: Infinity,
trendingUpward: true,
trackingDataTimeInterval: {
start: startDate,
end: endDate,
},
});
});
});
});
}
});
37 changes: 34 additions & 3 deletions src/stats/progression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down