1
0
Fork 0

feat(opord): add par-anchored speed bonus to scoring

This commit is contained in:
Jason Fraley 2026-09-09 11:28:39 -04:00
parent 181f244f62
commit eb77f45b95
2 changed files with 21 additions and 3 deletions

View file

@ -150,8 +150,9 @@ export function OpordArena({
<span className="font-medium text-foreground">Debrief</span> <span className="font-medium text-foreground">Debrief</span>
<span> <span>
A perfect run inside {OPORD_DIFFICULTIES[difficulty].parTimeMs / 1000}s earns a{" "} A perfect run inside {OPORD_DIFFICULTIES[difficulty].parTimeMs / 1000}s earns a{" "}
{OPORD_DIFFICULTIES[difficulty].perfectBonus}-point bonus. Each tier tracks its own {OPORD_DIFFICULTIES[difficulty].perfectBonus}-point bonus. Finishing under the par
best score. time also earns a speed bonus that scales with how fast you finish and how many
answers you got right. Each tier tracks its own best score.
</span> </span>
</div> </div>
</CardContent> </CardContent>

View file

@ -26,6 +26,12 @@ export interface OpordTierConfig {
readonly parTimeMs: number; readonly parTimeMs: number;
/** Bonus for a perfect run inside the par time. */ /** Bonus for a perfect run inside the par time. */
readonly perfectBonus: number; readonly perfectBonus: number;
/**
* Points pool for the time bonus: a run finishing under the par time earns
* this scaled by the fraction of par time saved and the fraction of
* questions answered correctly, so speed pays only alongside accuracy.
*/
readonly maxSpeedBonus: number;
} }
/** Grace added to the quiz limit for network/rounding before the server rejects. */ /** Grace added to the quiz limit for network/rounding before the server rejects. */
@ -50,6 +56,7 @@ export const OPORD_DIFFICULTIES: Record<OpordDifficulty, OpordTierConfig> = {
correctPoints: 100, correctPoints: 100,
parTimeMs: 100_000, parTimeMs: 100_000,
perfectBonus: 150, perfectBonus: 150,
maxSpeedBonus: 150,
}, },
veteran: { veteran: {
studyHintSeconds: 25, studyHintSeconds: 25,
@ -58,6 +65,7 @@ export const OPORD_DIFFICULTIES: Record<OpordDifficulty, OpordTierConfig> = {
correctPoints: 150, correctPoints: 150,
parTimeMs: 150_000, parTimeMs: 150_000,
perfectBonus: 300, perfectBonus: 300,
maxSpeedBonus: 250,
}, },
elite: { elite: {
// Extra clock headroom: elite relational questions include time arithmetic. // Extra clock headroom: elite relational questions include time arithmetic.
@ -67,6 +75,7 @@ export const OPORD_DIFFICULTIES: Record<OpordDifficulty, OpordTierConfig> = {
correctPoints: 200, correctPoints: 200,
parTimeMs: 200_000, parTimeMs: 200_000,
perfectBonus: 500, perfectBonus: 500,
maxSpeedBonus: 400,
}, },
}; };
@ -103,7 +112,15 @@ export function calculateOpordScore(
): number { ): number {
const tier = OPORD_DIFFICULTIES[difficulty]; const tier = OPORD_DIFFICULTIES[difficulty];
const perfect = correct >= tier.questionCount && timeMs <= tier.parTimeMs; const perfect = correct >= tier.questionCount && timeMs <= tier.parTimeMs;
return Math.max(0, correct * tier.correctPoints + (perfect ? tier.perfectBonus : 0)); const speedBonus =
timeMs < tier.parTimeMs
? Math.round(
((tier.parTimeMs - timeMs) / tier.parTimeMs) *
tier.maxSpeedBonus *
(correct / tier.questionCount),
)
: 0;
return Math.max(0, correct * tier.correctPoints + speedBonus + (perfect ? tier.perfectBonus : 0));
} }
export function calculateOpordXp(score: number, isNewTierBest: boolean): number { export function calculateOpordXp(score: number, isNewTierBest: boolean): number {