Added streak percentage

This commit is contained in:
George Powell
2026-02-21 16:17:06 -05:00
parent c3307b3920
commit 6554ef8f41
6 changed files with 143 additions and 17 deletions

View File

@@ -0,0 +1,26 @@
#!/bin/zsh
# Seed the database with 10 fake completions with random anonymous_ids
# Useful for testing streak percentile and stats features
DB_PATH="dev.db"
TODAY=$(date +%Y-%m-%d)
NOW=$(date +%s)
echo "Seeding 10 fake completions for date: $TODAY"
for i in {1..50}; do
ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
ANON_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
GUESS_COUNT=$(( (RANDOM % 6) + 1 )) # 16 guesses
sqlite3 "$DB_PATH" "
INSERT OR IGNORE INTO daily_completions (id, anonymous_id, date, guess_count, completed_at)
VALUES ('$ID', '$ANON_ID', '$TODAY', $GUESS_COUNT, $NOW);
"
echo " [$i] anon=$ANON_ID guesses=$GUESS_COUNT"
done
TOTAL=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM daily_completions WHERE date = '$TODAY';")
echo "✓ Done. Total completions for $TODAY: $TOTAL"

View File

@@ -90,7 +90,7 @@
return chapterCounts[bookId] || 1; return chapterCounts[bookId] || 1;
} }
// Generate 6 random chapter options including the correct one // Generate 4 random chapter options including the correct one
function generateChapterOptions( function generateChapterOptions(
correctChapter: number, correctChapter: number,
totalChapters: number, totalChapters: number,
@@ -98,14 +98,14 @@
const options = new Set<number>(); const options = new Set<number>();
options.add(correctChapter); options.add(correctChapter);
if (totalChapters >= 6) { if (totalChapters >= 4) {
while (options.size < 6) { while (options.size < 4) {
const randomChapter = const randomChapter =
Math.floor(Math.random() * totalChapters) + 1; Math.floor(Math.random() * totalChapters) + 1;
options.add(randomChapter); options.add(randomChapter);
} }
} else { } else {
while (options.size < 6) { while (options.size < 4) {
const randomChapter = Math.floor(Math.random() * 10) + 1; const randomChapter = Math.floor(Math.random() * 10) + 1;
options.add(randomChapter); options.add(randomChapter);
} }
@@ -167,18 +167,18 @@
</script> </script>
<Container <Container
class="w-full p-6 sm:p-8 bg-linear-to-br from-yellow-100/80 to-amber-200/80 text-gray-800 shadow-md" class="w-full p-3 sm:p-4 bg-linear-to-br from-yellow-100/80 to-amber-200/80 text-gray-800 shadow-md"
> >
<div class="text-center"> <div class="text-center">
<p class="text-xl sm:text-2xl font-bold mb-2">Bonus Challenge</p> <p class="font-bold mb-3 text-lg sm:text-xl">
<p class="text-sm sm:text-base opacity-80 mb-6"> Bonus Challenge
Guess the chapter for an even higher grade <span class="text-base sm:text-lg opacity-60 font-normal"
>— guess the chapter for an even higher grade</span
>
</p> </p>
<div <div class="grid grid-cols-4 gap-2 justify-center mx-auto mb-3">
class="grid grid-cols-3 lg:grid-cols-6 gap-3 sm:gap-4 justify-center mx-auto mb-6" {#each chapterOptions as chapter (chapter)}
>
{#each chapterOptions as chapter}
<button <button
onclick={() => handleChapterSelect(chapter)} onclick={() => handleChapterSelect(chapter)}
disabled={hasAnswered} disabled={hasAnswered}

View File

@@ -32,6 +32,7 @@
onChapterGuessCompleted, onChapterGuessCompleted,
shareText, shareText,
streak = 0, streak = 0,
streakPercentile = null,
}: { }: {
statsData: StatsData | null; statsData: StatsData | null;
correctBookId: string; correctBookId: string;
@@ -44,6 +45,7 @@
onChapterGuessCompleted: () => void; onChapterGuessCompleted: () => void;
shareText: string; shareText: string;
streak?: number; streak?: number;
streakPercentile?: number | null;
} = $props(); } = $props();
let bookName = $derived(getBookById(correctBookId)?.name ?? ""); let bookName = $derived(getBookById(correctBookId)?.name ?? "");
@@ -66,9 +68,9 @@
if (guessCount === 1) { if (guessCount === 1) {
const n = Math.random(); const n = Math.random();
if (n < 0.99) { if (n < 0.99) {
return "🌟 First try! 🌟"; return "First try!";
} else { } else {
return "🗣️ Axios! 🗣️"; return "Axios!";
} }
} }
@@ -107,9 +109,20 @@
</p> </p>
{#if streak > 1} {#if streak > 1}
<p class="big-text text-orange-500! text-sm! mt-4"> <p class="big-text text-orange-500! text-lg! my-4">
🔥 {streak} day streak! 🔥 {streak} days in a row!
</p> </p>
{#if streak >= 7}
<p class="font-black text-lg font-triodion">
Thank you for making Bibdle part of your daily routine!
</p>
{/if}
{#if streakPercentile !== null}
<p class="text-sm mt-4 text-gray-700 font-triodion">
Only {streakPercentile}% of players have a streak of {streak}
or greater.
</p>
{/if}
{/if} {/if}
</Container> </Container>

View File

@@ -5,3 +5,11 @@ export async function fetchStreak(anonymousId: string, localDate: string): Promi
const data = await res.json(); const data = await res.json();
return typeof data.streak === 'number' ? data.streak : 0; return typeof data.streak === 'number' ? data.streak : 0;
} }
export async function fetchStreakPercentile(streak: number, localDate: string): Promise<number | null> {
const params = new URLSearchParams({ streak: String(streak), localDate });
const res = await fetch(`/api/streak-percentile?${params}`);
if (!res.ok) return null;
const data = await res.json();
return typeof data.percentile === 'number' ? data.percentile : null;
}

View File

@@ -18,7 +18,7 @@
shareResult, shareResult,
copyToClipboard as clipboardCopy, copyToClipboard as clipboardCopy,
} from "$lib/utils/share"; } from "$lib/utils/share";
import { fetchStreak } from "$lib/utils/streak"; import { fetchStreak, fetchStreakPercentile } from "$lib/utils/streak";
import { import {
submitCompletion, submitCompletion,
fetchExistingStats, fetchExistingStats,
@@ -41,6 +41,7 @@
let showWinScreen = $state(false); let showWinScreen = $state(false);
let statsData = $state<StatsData | null>(null); let statsData = $state<StatsData | null>(null);
let streak = $state(0); let streak = $state(0);
let streakPercentile = $state<number | null>(null);
const persistence = createGamePersistence( const persistence = createGamePersistence(
() => dailyVerse.date, () => dailyVerse.date,
@@ -217,6 +218,11 @@
const localDate = new Date().toLocaleDateString("en-CA"); const localDate = new Date().toLocaleDateString("en-CA");
fetchStreak(persistence.anonymousId, localDate).then((result) => { fetchStreak(persistence.anonymousId, localDate).then((result) => {
streak = result; streak = result;
if (result >= 2) {
fetchStreakPercentile(result, localDate).then((p) => {
streakPercentile = p;
});
}
}); });
}); });
@@ -308,6 +314,7 @@
onChapterGuessCompleted={persistence.onChapterGuessCompleted} onChapterGuessCompleted={persistence.onChapterGuessCompleted}
shareText={getShareText()} shareText={getShareText()}
{streak} {streak}
{streakPercentile}
/> />
</div> </div>
{/if} {/if}

View File

@@ -0,0 +1,72 @@
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { dailyCompletions } from '$lib/server/db/schema';
import { desc } from 'drizzle-orm';
export const GET: RequestHandler = async ({ url }) => {
const streakParam = url.searchParams.get('streak');
const localDate = url.searchParams.get('localDate');
if (!streakParam || !localDate) {
error(400, 'Missing streak or localDate');
}
const targetStreak = parseInt(streakParam, 10);
if (isNaN(targetStreak) || targetStreak < 1) {
error(400, 'Invalid streak');
}
// Fetch all completions ordered by anonymous_id and date desc
// so we can walk each user's history to compute their current streak.
const rows = await db
.select({
anonymousId: dailyCompletions.anonymousId,
date: dailyCompletions.date,
})
.from(dailyCompletions)
.orderBy(desc(dailyCompletions.date));
// Group dates by user
const byUser = new Map<string, string[]>();
for (const row of rows) {
const list = byUser.get(row.anonymousId);
if (list) {
list.push(row.date);
} else {
byUser.set(row.anonymousId, [row.date]);
}
}
// Calculate the current streak for each user
const streaks: number[] = [];
for (const [, dates] of byUser) {
// dates are already desc-sorted
const dateSet = new Set(dates);
let streak = 0;
let cursor = new Date(`${localDate}T00:00:00`);
while (true) {
const dateStr = cursor.toLocaleDateString('en-CA');
if (!dateSet.has(dateStr)) break;
streak++;
cursor.setDate(cursor.getDate() - 1);
}
streaks.push(streak);
}
// Only count users who have an active streak (streak >= 1)
const activeStreaks = streaks.filter((s) => s >= 1);
if (activeStreaks.length === 0) {
return json({ percentile: 100 });
}
// Percentage of active-streak users who have a streak >= targetStreak
const atOrAbove = activeStreaks.filter((s) => s >= targetStreak).length;
const raw = (atOrAbove / activeStreaks.length) * 100;
const percentile = raw < 1 ? Math.round(raw * 100) / 100 : Math.round(raw);
return json({ percentile });
};