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">
|
<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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user