Files
adventure/packages/shared/src/scores.ts
Zaine 194330fb47
Some checks failed
CI / test (push) Has been cancelled
initial
2026-06-26 09:21:14 +01:00

138 lines
3.9 KiB
TypeScript
Executable File

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