Revamped middle statline (ranking instead of arbitrary percentage)

This commit is contained in:
George Powell
2026-01-28 15:04:29 -05:00
parent 6365cfb363
commit 0ee3d8a4d0
3 changed files with 656 additions and 623 deletions

View File

@@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { getBookById, toOrdinal, getNextGradeMessage } from "$lib/utils/game"; import {
getBookById,
toOrdinal,
getNextGradeMessage,
} from "$lib/utils/game";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Container from "./Container.svelte"; import Container from "./Container.svelte";
import CountdownTimer from "./CountdownTimer.svelte"; import CountdownTimer from "./CountdownTimer.svelte";
@@ -11,6 +15,7 @@
guessRank: number; guessRank: number;
totalSolves: number; totalSolves: number;
averageGuesses: number; averageGuesses: number;
tiedCount: number;
} }
interface WeightedMessage { interface WeightedMessage {
@@ -33,7 +38,7 @@
let bookName = $derived(getBookById(correctBookId)?.name ?? ""); let bookName = $derived(getBookById(correctBookId)?.name ?? "");
let hasWebShare = $derived( let hasWebShare = $derived(
typeof navigator !== "undefined" && "share" in navigator typeof navigator !== "undefined" && "share" in navigator,
); );
let copySuccess = $state(false); let copySuccess = $state(false);
@@ -59,7 +64,7 @@
const totalWeight = congratulationsMessages.reduce( const totalWeight = congratulationsMessages.reduce(
(sum, msg) => sum + msg.weight, (sum, msg) => sum + msg.weight,
0 0,
); );
let random = Math.random() * totalWeight; let random = Math.random() * totalWeight;
@@ -89,7 +94,9 @@
<p class="text-lg sm:text-xl md:text-2xl mt-4"> <p class="text-lg sm:text-xl md:text-2xl mt-4">
You guessed correctly after {guessCount} You guessed correctly after {guessCount}
{guessCount === 1 ? "guess" : "guesses"}. {guessCount === 1 ? "guess" : "guesses"}.
<span class="font-bold bg-white/40 rounded px-1.5 py-0.75">{grade}</span> <span class="font-bold bg-white/40 rounded px-1.5 py-0.75"
>{grade}</span
>
</p> </p>
<div class="flex justify-center mt-6"> <div class="flex justify-center mt-6">
@@ -112,7 +119,9 @@
}} }}
data-umami-event="Copy to Clipboard" data-umami-event="Copy to Clipboard"
class={`text-2xl font-bold p-4 rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none ${ class={`text-2xl font-bold p-4 rounded-lg inline-block transition-all shadow-lg mx-2 cursor-pointer border-none appearance-none ${
copySuccess ? "bg-white/30" : "bg-white/70 hover:bg-white/80" copySuccess
? "bg-white/30"
: "bg-white/70 hover:bg-white/80"
}`} }`}
> >
{copySuccess ? "✅ Copied!" : "📋 Copy"} {copySuccess ? "✅ Copied!" : "📋 Copy"}
@@ -164,22 +173,23 @@
#{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">
You were the {toOrdinal(statsData.solveRank)} person to solve today You were the {toOrdinal(statsData.solveRank)} person to solve
today
</div> </div>
</div> </div>
<!-- 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">
{Math.round( {toOrdinal(statsData.guessRank)}
((statsData.totalSolves - statsData.guessRank + 1) /
statsData.totalSolves) *
100
)}%
</div> </div>
<div class="text-sm sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves} You ranked {toOrdinal(statsData.guessRank)} of {statsData.totalSolves}
total solves {statsData.totalSolves === 1
? "solve"
: "solves"}{statsData.tiedCount > 0
? `, tied with ${statsData.tiedCount} ${statsData.tiedCount === 1 ? "other" : "others"}`
: ""}
</div> </div>
</div> </div>
@@ -190,7 +200,8 @@
</div> </div>
<div class="text-sm sm:text-sm opacity-90 mt-1"> <div class="text-sm sm:text-sm opacity-90 mt-1">
People guessed correctly after {statsData.averageGuesses} People guessed correctly after {statsData.averageGuesses}
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on average {statsData.averageGuesses === 1 ? "guess" : "guesses"} on
average
</div> </div>
</div> </div>
</div> </div>

View File

@@ -41,6 +41,7 @@
guessRank: number; guessRank: number;
totalSolves: number; totalSolves: number;
averageGuesses: number; averageGuesses: number;
tiedCount: 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)));
@@ -51,7 +52,7 @@
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
}) }),
); );
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
@@ -59,11 +60,14 @@
isWon isWon
? guesses.length === 1 && chapterCorrect ? guesses.length === 1 && chapterCorrect
? "S++" ? "S++"
: getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0) : getGrade(
: "" guesses.length,
getBookById(correctBookId)?.popularity ?? 0,
)
: "",
); );
let blurChapter = $derived( let blurChapter = $derived(
isWon && guesses.length === 1 && !chapterGuessCompleted isWon && guesses.length === 1 && !chapterGuessCompleted,
); );
function getBookById(id: string): BibleBook | undefined { function getBookById(id: string): BibleBook | undefined {
@@ -91,15 +95,18 @@
// Special case: if correct book is Epistles + starts with "1", // Special case: if correct book is Epistles + starts with "1",
// any guess starting with "1" counts as first letter match // any guess starting with "1" counts as first letter match
const correctIsEpistlesWithNumber = correctBook.section === "Epistles" && correctBook.name[0] === "1"; const correctIsEpistlesWithNumber =
correctBook.section === "Epistles" && correctBook.name[0] === "1";
const guessStartsWithNumber = book.name[0] === "1"; const guessStartsWithNumber = book.name[0] === "1";
const firstLetterMatch = correctIsEpistlesWithNumber && guessStartsWithNumber const firstLetterMatch =
correctIsEpistlesWithNumber && guessStartsWithNumber
? true ? true
: book.name[0].toUpperCase() === correctBook.name[0].toUpperCase(); : book.name[0].toUpperCase() ===
correctBook.name[0].toUpperCase();
console.log( console.log(
`Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}` `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`,
); );
if (guesses.length === 0) { if (guesses.length === 0) {
@@ -136,7 +143,8 @@
// Fallback UUID v4 generator for older browsers // Fallback UUID v4 generator for older browsers
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0; const r =
window.crypto.getRandomValues(new Uint8Array(1))[0] % 16 | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8; const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16); return v.toString(16);
}); });
@@ -194,12 +202,16 @@
const adjacent = isAdjacent(bookId, correctBookId); const adjacent = isAdjacent(bookId, correctBookId);
// Apply same first letter logic as in submitGuess // Apply same first letter logic as in submitGuess
const correctIsEpistlesWithNumber = correctBook.section === "Epistles" && correctBook.name[0] === "1"; const correctIsEpistlesWithNumber =
correctBook.section === "Epistles" &&
correctBook.name[0] === "1";
const guessStartsWithNumber = book.name[0] === "1"; const guessStartsWithNumber = book.name[0] === "1";
const firstLetterMatch = correctIsEpistlesWithNumber && guessStartsWithNumber const firstLetterMatch =
correctIsEpistlesWithNumber && guessStartsWithNumber
? true ? true
: book.name[0].toUpperCase() === correctBook.name[0].toUpperCase(); : book.name[0].toUpperCase() ===
correctBook.name[0].toUpperCase();
return { return {
book, book,
@@ -216,7 +228,7 @@
if (!browser) return; if (!browser) return;
localStorage.setItem( localStorage.setItem(
`bibdle-guesses-${dailyVerse.date}`, `bibdle-guesses-${dailyVerse.date}`,
JSON.stringify(guesses.map((g) => g.book.id)) JSON.stringify(guesses.map((g) => g.book.id)),
); );
}); });
@@ -241,7 +253,7 @@
(async () => { (async () => {
try { try {
const response = await fetch( const response = await fetch(
`/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}` `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`,
); );
const result = await response.json(); const result = await response.json();
console.log("Stats response:", result); console.log("Stats response:", result);
@@ -251,7 +263,7 @@
statsData = result.stats; statsData = result.stats;
localStorage.setItem( localStorage.setItem(
`bibdle-stats-submitted-${dailyVerse.date}`, `bibdle-stats-submitted-${dailyVerse.date}`,
"true" "true",
); );
} else if (result.error) { } else if (result.error) {
console.error("Server error:", result.error); console.error("Server error:", result.error);
@@ -295,7 +307,7 @@
statsSubmitted = true; statsSubmitted = true;
localStorage.setItem( localStorage.setItem(
`bibdle-stats-submitted-${dailyVerse.date}`, `bibdle-stats-submitted-${dailyVerse.date}`,
"true" "true",
); );
} else if (result.error) { } else if (result.error) {
console.error("Server error:", result.error); console.error("Server error:", result.error);
@@ -341,7 +353,7 @@
year: "numeric", year: "numeric",
}); });
const formattedDate = dateFormatter.format( const formattedDate = dateFormatter.format(
new Date(`${dailyVerse.date}T00:00:00`) new Date(`${dailyVerse.date}T00:00:00`),
); );
const siteUrl = window.location.origin; const siteUrl = window.location.origin;
return [ return [
@@ -468,9 +480,13 @@
const saved = localStorage.getItem(key); const saved = localStorage.getItem(key);
if (saved) { if (saved) {
const data = JSON.parse(saved); const data = JSON.parse(saved);
const match = dailyVerse.reference.match(/\s(\d+):/); const match =
const correctChapter = match ? parseInt(match[1], 10) : 1; dailyVerse.reference.match(/\s(\d+):/);
chapterCorrect = data.selectedChapter === correctChapter; const correctChapter = match
? parseInt(match[1], 10)
: 1;
chapterCorrect =
data.selectedChapter === correctChapter;
} }
}} }}
/> />

View File

@@ -48,13 +48,16 @@ export const POST: RequestHandler = async ({ request }) => {
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length; const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length;
const guessRank = betterGuesses + 1; const guessRank = betterGuesses + 1;
// Count ties: how many have the SAME guessCount (excluding self)
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
// Average guesses // Average guesses
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;
return json({ return json({
success: true, success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses } stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount }
}); });
} catch (err) { } catch (err) {
console.error('Error submitting completion:', err); console.error('Error submitting completion:', err);
@@ -105,13 +108,16 @@ export const GET: RequestHandler = async ({ url }) => {
const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length; const betterGuesses = allCompletions.filter(c => c.guessCount < guessCount).length;
const guessRank = betterGuesses + 1; const guessRank = betterGuesses + 1;
// Count ties: how many have the SAME guessCount (excluding self)
const tiedCount = allCompletions.filter(c => c.guessCount === guessCount && c.anonymousId !== anonymousId).length;
// Average guesses // Average guesses
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;
return json({ return json({
success: true, success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses } stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount }
}); });
} catch (err) { } catch (err) {
console.error('Error fetching stats:', err); console.error('Error fetching stats:', err);