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">
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 Container from "./Container.svelte";
import CountdownTimer from "./CountdownTimer.svelte";
@@ -11,6 +15,7 @@
guessRank: number;
totalSolves: number;
averageGuesses: number;
tiedCount: number;
}
interface WeightedMessage {
@@ -33,7 +38,7 @@
let bookName = $derived(getBookById(correctBookId)?.name ?? "");
let hasWebShare = $derived(
typeof navigator !== "undefined" && "share" in navigator
typeof navigator !== "undefined" && "share" in navigator,
);
let copySuccess = $state(false);
@@ -59,7 +64,7 @@
const totalWeight = congratulationsMessages.reduce(
(sum, msg) => sum + msg.weight,
0
0,
);
let random = Math.random() * totalWeight;
@@ -89,7 +94,9 @@
<p class="text-lg sm:text-xl md:text-2xl mt-4">
You guessed correctly after {guessCount}
{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>
<div class="flex justify-center mt-6">
@@ -112,7 +119,9 @@
}}
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 ${
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"}
@@ -164,22 +173,23 @@
#{statsData.solveRank}
</div>
<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>
<!-- Guess Rank Column -->
<div class="flex flex-col">
<div class="text-3xl sm:text-4xl font-black">
{Math.round(
((statsData.totalSolves - statsData.guessRank + 1) /
statsData.totalSolves) *
100
)}%
{toOrdinal(statsData.guessRank)}
</div>
<div class="text-sm sm:text-sm opacity-90 mt-1">
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>
@@ -190,7 +200,8 @@
</div>
<div class="text-sm sm:text-sm opacity-90 mt-1">
People guessed correctly after {statsData.averageGuesses}
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on average
{statsData.averageGuesses === 1 ? "guess" : "guesses"} on
average
</div>
</div>
</div>

View File

@@ -41,6 +41,7 @@
guessRank: number;
totalSolves: number;
averageGuesses: number;
tiedCount: number;
} | null>(null);
let guessedIds = $derived(new Set(guesses.map((g) => g.book.id)));
@@ -51,7 +52,7 @@
year: "numeric",
month: "long",
day: "numeric",
})
}),
);
let isWon = $derived(guesses.some((g) => g.book.id === correctBookId));
@@ -59,11 +60,14 @@
isWon
? guesses.length === 1 && chapterCorrect
? "S++"
: getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0)
: ""
: getGrade(
guesses.length,
getBookById(correctBookId)?.popularity ?? 0,
)
: "",
);
let blurChapter = $derived(
isWon && guesses.length === 1 && !chapterGuessCompleted
isWon && guesses.length === 1 && !chapterGuessCompleted,
);
function getBookById(id: string): BibleBook | undefined {
@@ -91,15 +95,18 @@
// Special case: if correct book is Epistles + starts with "1",
// 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 firstLetterMatch = correctIsEpistlesWithNumber && guessStartsWithNumber
const firstLetterMatch =
correctIsEpistlesWithNumber && guessStartsWithNumber
? true
: book.name[0].toUpperCase() === correctBook.name[0].toUpperCase();
: book.name[0].toUpperCase() ===
correctBook.name[0].toUpperCase();
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) {
@@ -136,7 +143,8 @@
// Fallback UUID v4 generator for older browsers
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;
return v.toString(16);
});
@@ -194,12 +202,16 @@
const adjacent = isAdjacent(bookId, correctBookId);
// 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 firstLetterMatch = correctIsEpistlesWithNumber && guessStartsWithNumber
const firstLetterMatch =
correctIsEpistlesWithNumber && guessStartsWithNumber
? true
: book.name[0].toUpperCase() === correctBook.name[0].toUpperCase();
: book.name[0].toUpperCase() ===
correctBook.name[0].toUpperCase();
return {
book,
@@ -216,7 +228,7 @@
if (!browser) return;
localStorage.setItem(
`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 () => {
try {
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();
console.log("Stats response:", result);
@@ -251,7 +263,7 @@
statsData = result.stats;
localStorage.setItem(
`bibdle-stats-submitted-${dailyVerse.date}`,
"true"
"true",
);
} else if (result.error) {
console.error("Server error:", result.error);
@@ -295,7 +307,7 @@
statsSubmitted = true;
localStorage.setItem(
`bibdle-stats-submitted-${dailyVerse.date}`,
"true"
"true",
);
} else if (result.error) {
console.error("Server error:", result.error);
@@ -341,7 +353,7 @@
year: "numeric",
});
const formattedDate = dateFormatter.format(
new Date(`${dailyVerse.date}T00:00:00`)
new Date(`${dailyVerse.date}T00:00:00`),
);
const siteUrl = window.location.origin;
return [
@@ -468,9 +480,13 @@
const saved = localStorage.getItem(key);
if (saved) {
const data = JSON.parse(saved);
const match = dailyVerse.reference.match(/\s(\d+):/);
const correctChapter = match ? parseInt(match[1], 10) : 1;
chapterCorrect = data.selectedChapter === correctChapter;
const match =
dailyVerse.reference.match(/\s(\d+):/);
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 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
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
return json({
success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses }
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount }
});
} catch (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 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
const totalGuesses = allCompletions.reduce((sum, c) => sum + c.guessCount, 0);
const averageGuesses = Math.round((totalGuesses / totalSolves) * 10) / 10;
return json({
success: true,
stats: { solveRank, guessRank, totalSolves, averageGuesses }
stats: { solveRank, guessRank, totalSolves, averageGuesses, tiedCount }
});
} catch (err) {
console.error('Error fetching stats:', err);