137
packages/shared/src/scores.ts
Executable file
137
packages/shared/src/scores.ts
Executable file
@@ -0,0 +1,137 @@
|
||||
export interface DaySnapshot {
|
||||
date: string;
|
||||
adventureSlots: number;
|
||||
adventureTouched: number;
|
||||
exerciseDone: boolean;
|
||||
workHours: number;
|
||||
workTarget: number;
|
||||
sleepLogged: boolean;
|
||||
classesDone: number;
|
||||
teachingDone: boolean;
|
||||
explorationsDone: number;
|
||||
prayerChecks: number;
|
||||
prayerTotal: number;
|
||||
litanyChecks: number;
|
||||
litanyTotal: number;
|
||||
pagesRead: number;
|
||||
readingGoalWeekly: number;
|
||||
hadSpiritualEver: boolean;
|
||||
}
|
||||
|
||||
function decayWeight(daysAgo: number): number {
|
||||
return Math.exp(-daysAgo / 10);
|
||||
}
|
||||
|
||||
export function computeConsistencyScore(days: DaySnapshot[]): number {
|
||||
if (days.length === 0) return 0;
|
||||
let weighted = 0;
|
||||
let totalWeight = 0;
|
||||
days.forEach((d, i) => {
|
||||
const w = decayWeight(i);
|
||||
totalWeight += w;
|
||||
if (d.adventureSlots === 0) {
|
||||
weighted += w * 50;
|
||||
return;
|
||||
}
|
||||
weighted += w * (d.adventureTouched / d.adventureSlots) * 100;
|
||||
});
|
||||
return Math.round(weighted / totalWeight);
|
||||
}
|
||||
|
||||
export function computeDisciplineScore(days: DaySnapshot[]): number {
|
||||
if (days.length === 0) return 0;
|
||||
let weighted = 0;
|
||||
let totalWeight = 0;
|
||||
days.forEach((d, i) => {
|
||||
const w = decayWeight(i);
|
||||
totalWeight += w;
|
||||
const exercise = d.exerciseDone ? 100 : 0;
|
||||
const sleep = d.sleepLogged ? 100 : 50;
|
||||
const work =
|
||||
d.workTarget > 0
|
||||
? Math.min(100, (d.workHours / d.workTarget) * 100)
|
||||
: 50;
|
||||
weighted += w * (exercise * 0.4 + sleep * 0.3 + work * 0.3);
|
||||
});
|
||||
return Math.round(weighted / totalWeight);
|
||||
}
|
||||
|
||||
export function computeLearningScore(days: DaySnapshot[]): number {
|
||||
if (days.length === 0) return 0;
|
||||
let weighted = 0;
|
||||
let totalWeight = 0;
|
||||
days.forEach((d, i) => {
|
||||
const w = decayWeight(i);
|
||||
totalWeight += w;
|
||||
let dayScore = 0;
|
||||
if (d.classesDone > 0) dayScore += 40;
|
||||
if (d.teachingDone) dayScore += 30;
|
||||
if (d.explorationsDone > 0) dayScore += 30;
|
||||
weighted += w * Math.min(100, dayScore);
|
||||
});
|
||||
return Math.round(weighted / totalWeight);
|
||||
}
|
||||
|
||||
export function computeSpiritualScore(days: DaySnapshot[]): number {
|
||||
const hadEver = days.some((d) => d.hadSpiritualEver);
|
||||
if (days.length === 0) return hadEver ? 20 : 0;
|
||||
let weighted = 0;
|
||||
let totalWeight = 0;
|
||||
days.forEach((d, i) => {
|
||||
const w = decayWeight(i);
|
||||
totalWeight += w;
|
||||
const total = d.prayerTotal + d.litanyTotal;
|
||||
const checked = d.prayerChecks + d.litanyChecks;
|
||||
const rate = total > 0 ? (checked / total) * 100 : 0;
|
||||
weighted += w * rate;
|
||||
});
|
||||
const score = Math.round(weighted / totalWeight);
|
||||
return hadEver ? Math.max(20, score) : score;
|
||||
}
|
||||
|
||||
export function computeHealthScore(days: DaySnapshot[]): number {
|
||||
if (days.length === 0) return 0;
|
||||
let weighted = 0;
|
||||
let totalWeight = 0;
|
||||
days.forEach((d, i) => {
|
||||
const w = decayWeight(i);
|
||||
totalWeight += w;
|
||||
const exercise = d.exerciseDone ? 100 : 0;
|
||||
const sleep = d.sleepLogged ? 100 : 40;
|
||||
weighted += w * (exercise * 0.6 + sleep * 0.4);
|
||||
});
|
||||
return Math.round(weighted / totalWeight);
|
||||
}
|
||||
|
||||
export function computeReadingScore(
|
||||
days: DaySnapshot[],
|
||||
weeklyGoal: number
|
||||
): number {
|
||||
if (days.length === 0) return 0;
|
||||
const recentWeek = days.slice(0, 7);
|
||||
const pages = recentWeek.reduce((s, d) => s + d.pagesRead, 0);
|
||||
const goalScore = weeklyGoal > 0 ? Math.min(100, (pages / weeklyGoal) * 100) : 50;
|
||||
let streakBonus = 0;
|
||||
let streak = 0;
|
||||
for (const d of days) {
|
||||
if (d.pagesRead > 0) streak++;
|
||||
else break;
|
||||
}
|
||||
if (streak >= 7) streakBonus = 20;
|
||||
else if (streak >= 3) streakBonus = 10;
|
||||
return Math.min(100, Math.round(goalScore * 0.8 + streakBonus));
|
||||
}
|
||||
|
||||
export function computeAllScores(
|
||||
days: DaySnapshot[],
|
||||
weeklyReadingGoal: number
|
||||
) {
|
||||
return {
|
||||
consistencyScore: computeConsistencyScore(days),
|
||||
disciplineScore: computeDisciplineScore(days),
|
||||
learningScore: computeLearningScore(days),
|
||||
spiritualScore: computeSpiritualScore(days),
|
||||
healthScore: computeHealthScore(days),
|
||||
readingScore: computeReadingScore(days, weeklyReadingGoal),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user