mirror of
https://github.com/pupperpowell/bibdle.git
synced 2026-02-04 10:54:44 -05:00
Revamped middle statline (ranking instead of arbitrary percentage)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user