add percentile stats, update chapter guess UI

This commit is contained in:
George Powell
2026-01-28 23:03:51 -05:00
parent 2df97f66bf
commit d21ca9d687
4 changed files with 235 additions and 204 deletions

View File

@@ -93,14 +93,15 @@
// Generate 6 random chapter options including the correct one // Generate 6 random chapter options including the correct one
function generateChapterOptions( function generateChapterOptions(
correctChapter: number, correctChapter: number,
totalChapters: number totalChapters: number,
): number[] { ): number[] {
const options = new Set<number>(); const options = new Set<number>();
options.add(correctChapter); options.add(correctChapter);
if (totalChapters >= 6) { if (totalChapters >= 6) {
while (options.size < 6) { while (options.size < 6) {
const randomChapter = Math.floor(Math.random() * totalChapters) + 1; const randomChapter =
Math.floor(Math.random() * totalChapters) + 1;
options.add(randomChapter); options.add(randomChapter);
} }
} else { } else {
@@ -118,7 +119,10 @@
$effect(() => { $effect(() => {
if (chapterOptions.length === 0) { if (chapterOptions.length === 0) {
chapterOptions = generateChapterOptions(correctChapter, totalChapters); chapterOptions = generateChapterOptions(
correctChapter,
totalChapters,
);
} }
}); });
@@ -144,7 +148,7 @@
const key = `bibdle-chapter-guess-${reference}`; const key = `bibdle-chapter-guess-${reference}`;
localStorage.setItem( localStorage.setItem(
key, key,
JSON.stringify({ selectedChapter, hasAnswered, chapterOptions }) JSON.stringify({ selectedChapter, hasAnswered, chapterOptions }),
); );
}); });
@@ -158,7 +162,7 @@
} }
let isCorrect = $derived( let isCorrect = $derived(
selectedChapter !== null && selectedChapter === correctChapter selectedChapter !== null && selectedChapter === correctChapter,
); );
</script> </script>
@@ -171,13 +175,15 @@
Guess the chapter for an even higher grade Guess the chapter for an even higher grade
</p> </p>
<div class="grid grid-cols-3 gap-3 sm:gap-4 max-w-md mx-auto mb-6"> <div
class="grid grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4 justify-center mx-auto mb-6"
>
{#each chapterOptions as chapter} {#each chapterOptions as chapter}
<button <button
onclick={() => handleChapterSelect(chapter)} onclick={() => handleChapterSelect(chapter)}
disabled={hasAnswered} disabled={hasAnswered}
class={` class={`
aspect-square text-2xl sm:text-3xl font-bold rounded-xl w-20 h-20 sm:w-24 sm:h-24 text-2xl sm:text-3xl font-bold rounded-xl
transition-all duration-300 border-2 transition-all duration-300 border-2
${ ${
hasAnswered hasAnswered

View File

@@ -16,6 +16,7 @@
totalSolves: number; totalSolves: number;
averageGuesses: number; averageGuesses: number;
tiedCount: number; tiedCount: number;
percentile: number;
} }
interface WeightedMessage { interface WeightedMessage {
@@ -169,7 +170,9 @@
> >
<!-- Solve Rank Column --> <!-- Solve Rank Column -->
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black"> <div
class="text-3xl sm:text-4xl font-black border-b border-gray-300 pb-2"
>
#{statsData.solveRank} #{statsData.solveRank}
</div> </div>
<div class="text-sm sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
@@ -180,7 +183,9 @@
<!-- Guess Rank Column --> <!-- Guess Rank Column -->
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black"> <div
class="text-3xl sm:text-4xl font-black border-b border-gray-300 pb-2"
>
{toOrdinal(statsData.guessRank)} {toOrdinal(statsData.guessRank)}
</div> </div>
<div class="text-sm sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
@@ -189,13 +194,20 @@
? "solve" ? "solve"
: "solves"}{statsData.tiedCount > 0 : "solves"}{statsData.tiedCount > 0
? `, tied with ${statsData.tiedCount} ${statsData.tiedCount === 1 ? "other" : "others"}` ? `, tied with ${statsData.tiedCount} ${statsData.tiedCount === 1 ? "other" : "others"}`
: ""} : ""}.<br />
{#if statsData.percentile <= 25}
<span class="font-bold">
(Top {statsData.percentile}%)
</span>
{/if}
</div> </div>
</div> </div>
<!-- Average Column --> <!-- Average Column -->
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black"> <div
class="text-3xl sm:text-4xl font-black border-b border-gray-300 pb-2"
>
{statsData.averageGuesses} {statsData.averageGuesses}
</div> </div>
<div class="text-sm sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">

View File

@@ -43,6 +43,7 @@
totalSolves: number; totalSolves: number;
averageGuesses: number; averageGuesses: number;
tiedCount: number; tiedCount: number;
percentile: number;
} | null>(null); } | null>(null);
let guessedIds = $derived(new Set(guesses.map((g) => g.book.id))); let guessedIds = $derived(new Set(guesses.map((g) => g.book.id)));

View File

@@ -44,9 +44,11 @@ export const POST: RequestHandler = async ({ request }) => {
// Solve rank: position in time-ordered list // Solve rank: position in time-ordered list
const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1; const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1;
// Guess rank: count how many had FEWER guesses (ties get same rank) // Guess rank: count how many DISTINCT guess counts are better (grouped ranking)
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length; const uniqueBetterGuessCounts = new Set(
const guessRank = betterGuesses + 1; allCompletions.filter(c => c.guessCount < guessCount).map(c => c.guessCount)
);
const guessRank = uniqueBetterGuessCounts.size + 1;
// Count ties: how many have the SAME guessCount (excluding self) // Count ties: how many have the SAME guessCount (excluding self)
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length; const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
@@ -55,9 +57,13 @@ export const POST: RequestHandler = async ({ request }) => {
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0); const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10; const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
// Percentile: what percentage of people you beat (100 - your rank percentage)
const betterOrEqualCount = allCompletions.filter(c => c.guessCount <= guessCount).length;
const percentile = Math.round((betterOrEqualCount / totalSolves) * 100);
return json({ return json({
success: true, success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount } stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount, percentile }
}); });
} catch (err) { } catch (err) {
console.error('Error submitting completion:', err); console.error('Error submitting completion:', err);
@@ -104,9 +110,11 @@ export const GET: RequestHandler = async ({ url }) => {
// Solve rank: position in time-ordered list // Solve rank: position in time-ordered list
const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1; const solveRank = allCompletions.findIndex(c => c.anonymousId === anonymousId) + 1;
// Guess rank: count how many had FEWER guesses (ties get same rank) // Guess rank: count how many DISTINCT guess counts are better (grouped ranking)
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length; const uniqueBetterGuessCounts = new Set(
const guessRank = betterGuesses + 1; allCompletions.filter(c => c.guessCount < guessCount).map(c => c.guessCount)
);
const guessRank = uniqueBetterGuessCounts.size + 1;
// Count ties: how many have the SAME guessCount (excluding self) // Count ties: how many have the SAME guessCount (excluding self)
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length; const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
@@ -115,9 +123,13 @@ export const GET: RequestHandler = async ({ url }) => {
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0); const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10; const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
// Percentile: what percentage of people you beat (100 - your rank percentage)
const betterOrEqualCount = allCompletions.filter(c => c.guessCount <= guessCount).length;
const percentile = Math.round((betterOrEqualCount / totalSolves) * 100);
return json({ return json({
success: true, success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount } stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount, percentile }
}); });
} catch (err) { } catch (err) {
console.error('Error fetching stats:', err); console.error('Error fetching stats:', err);