From 54c7e3cdbb0736dfb43c4e37bee5098ec1353db6 Mon Sep 17 00:00:00 2001 From: George Powell Date: Sat, 27 Dec 2025 00:48:06 -0500 Subject: [PATCH 1/5] Visual fixes, typo fixes, and updated todo + metadata --- .gitignore | 3 + EnglishNKJBible.xml | 2 +- src/lib/components/VerseDisplay.svelte | 5 +- src/lib/components/WinScreen.svelte | 4 +- src/routes/+page.svelte | 712 +++++++++++++------------ todo.md | 34 +- 6 files changed, 396 insertions(+), 364 deletions(-) diff --git a/.gitignore b/.gitignore index fd83719..30287df 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ vite.config.ts.timestamp-* llms-* +engwebu_usfx.xml +embeddings-cache-L12.json +embeddings-cache-L6.json diff --git a/EnglishNKJBible.xml b/EnglishNKJBible.xml index db2f224..be873df 100644 --- a/EnglishNKJBible.xml +++ b/EnglishNKJBible.xml @@ -20271,7 +20271,7 @@ Thus says the Lord: “Keep justice, and do righteousness, For My salvation is about to come, And My righteousness to be revealed. Blessed is the man who does this, And the son of man who lays hold on it; Who keeps from defiling the Sabbath, And keeps his hand from doing any evil.” - Do not let the son of the foreigner Who has joined himself to the LordSpeak, saying, “The Lord has utterly separated me from His people”; Nor let the eunuch say, “Here I am, a dry tree.” + Do not let the son of the foreigner Who has joined himself to the Lord speak, saying, “The Lord has utterly separated me from His people”; Nor let the eunuch say, “Here I am, a dry tree.” For thus says the Lord: “To the eunuchs who keep My Sabbaths, And choose what pleases Me, And hold fast My covenant, Even to them I will give in My house And within My walls a place and a name Better than that of sons and daughters; I will give them an everlasting name That shall not be cut off. “Also the sons of the foreigner Who join themselves to the Lord, to serve Him, And to love the name of the Lord, to be His servants— Everyone who keeps from defiling the Sabbath, And holds fast My covenant— diff --git a/src/lib/components/VerseDisplay.svelte b/src/lib/components/VerseDisplay.svelte index 8455f74..e045399 100644 --- a/src/lib/components/VerseDisplay.svelte +++ b/src/lib/components/VerseDisplay.svelte @@ -6,13 +6,16 @@ let displayReference = $derived( dailyVerse.reference.replace(/^Psalms /, "Psalm ") ); + let displayVerseText = $derived( + dailyVerse.verseText.replace(/^([a-z])/, (c) => c.toUpperCase()) + );
- {dailyVerse.verseText} + {displayVerseText}
{#if isWon}

diff --git a/src/lib/components/WinScreen.svelte b/src/lib/components/WinScreen.svelte index e11d0db..b580e40 100644 --- a/src/lib/components/WinScreen.svelte +++ b/src/lib/components/WinScreen.svelte @@ -167,8 +167,8 @@ {statsData.averageGuesses}

- People guessed correctly after {statsData.averageGuesses} guesses on - average + People guessed correctly after {statsData.averageGuesses} + {statsData.averageGuesses === 1 ? "guess" : "guesses"} on average
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f38e39b..e9e9aac 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,417 +1,421 @@ - Bibdle — A daily bible game{isDev ? " (dev)" : ""} - + Bibdle — A daily bible game{isDev ? " (dev)" : ""} +
-
-

- -
-

-
- {isDev ? "Dev Edition | " : ""}{currentDate} -
+
+

+ +
+

+
+ {isDev ? "Dev Edition | " : ""}{currentDate} +
- + - {#if !isWon} - - {:else} - - - {/if} + {#if !isWon} + + {:else} + + + {/if} - - {#if isWon} - - {/if} - {#if isDev} - - {/if} -
+ + {#if isWon} + + {/if} + {#if isDev} + + {/if} +
diff --git a/todo.md b/todo.md index 486b49c..8e56d71 100644 --- a/todo.md +++ b/todo.md @@ -1,20 +1,36 @@ +# in progress + + +- root menu: classic / imposter mode / impossible mode (complete today's classic and imposter modes to unlock) + + # todo -- Difficulty levels -- difficult mode (guess old or new testament, first try _only_) -- impossible mode (1894 scrivener koine greek NT or some hebrew version for OT) three guesses only +- impossible mode (1904 greek bible) three guesses only. + - share both classic and impossible mode with both buttons -- "login to see your stats, unlock practice mode, and more" +- add imposter mode +- instructions + - classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. + - imposter mode: out of four options, identify the verse that is not in the Bible + - impossible mode: identify which book of the bible the verse is from in less than three guesses. + +- add login + saved stats + streak etc. + +- add deuterocanonical books + + - Practice mode: Unlimited verses - Create public or private leaderboards - Passport book with badges: - - Guess each Gospel first try - "Guessed all Gospels", "Perfect week", "Old Testament expert" - Theologian: Guess each book first try -- instructions + + +- difficult mode (guess old or new testament, first try _only_) (???) # places to send @@ -36,6 +52,12 @@ I created Bibdle from a combination of two things. The first is my lifelong desi # done +## december 26th + +- created embeddings for every bible verse (verse similarity finder) +- failed at having AI write a USFX format parser +- found a npm library for parsing USFX + ## december 23rd - switched to local copy of NKJV From c71170e6b3602b2fc540f0c72ac8c44734c1fad7 Mon Sep 17 00:00:00 2001 From: George Powell Date: Sat, 27 Dec 2025 11:42:15 -0500 Subject: [PATCH 2/5] added first-guess and correct-guess umami event tracking --- src/lib/components/WinScreen.svelte | 32 +- src/routes/+page.svelte | 716 ++++++++++++++-------------- todo.md | 29 +- 3 files changed, 392 insertions(+), 385 deletions(-) diff --git a/src/lib/components/WinScreen.svelte b/src/lib/components/WinScreen.svelte index b580e40..0e69813 100644 --- a/src/lib/components/WinScreen.svelte +++ b/src/lib/components/WinScreen.svelte @@ -1,6 +1,7 @@
-

+ +

+ {congratulationsMessage} The verse is from + {bookName}.

- import { bibleBooks, type BibleBook } from "$lib/types/bible"; + import { bibleBooks, type BibleBook } from "$lib/types/bible"; - import type { PageProps } from "./$types"; - import { browser } from "$app/environment"; + import type { PageProps } from "./$types"; + import { browser } from "$app/environment"; - import VerseDisplay from "$lib/components/VerseDisplay.svelte"; - import SearchInput from "$lib/components/SearchInput.svelte"; - import GuessesTable from "$lib/components/GuessesTable.svelte"; - import CountdownTimer from "$lib/components/CountdownTimer.svelte"; - import WinScreen from "$lib/components/WinScreen.svelte"; - import Feedback from "$lib/components/Feedback.svelte"; - import TitleAnimation from "$lib/components/TitleAnimation.svelte"; - import { getGrade } from "$lib/utils/game"; + import VerseDisplay from "$lib/components/VerseDisplay.svelte"; + import SearchInput from "$lib/components/SearchInput.svelte"; + import GuessesTable from "$lib/components/GuessesTable.svelte"; + import CountdownTimer from "$lib/components/CountdownTimer.svelte"; + import WinScreen from "$lib/components/WinScreen.svelte"; + import Feedback from "$lib/components/Feedback.svelte"; + import TitleAnimation from "$lib/components/TitleAnimation.svelte"; + import { getGrade } from "$lib/utils/game"; - interface Guess { - book: BibleBook; - testamentMatch: boolean; - sectionMatch: boolean; - adjacent: boolean; - } + interface Guess { + book: BibleBook; + testamentMatch: boolean; + sectionMatch: boolean; + adjacent: boolean; + } - let { data }: PageProps = $props(); + let { data }: PageProps = $props(); - let dailyVerse = $derived(data.dailyVerse); - let correctBookId = $derived(data.correctBookId); + let dailyVerse = $derived(data.dailyVerse); + let correctBookId = $derived(data.correctBookId); - let guesses = $state([]); + let guesses = $state([]); - let searchQuery = $state(""); + let searchQuery = $state(""); - let copied = $state(false); - let isDev = $state(false); + let copied = $state(false); + let isDev = $state(false); - let anonymousId = $state(""); - let statsSubmitted = $state(false); - let statsData = $state<{ - solveRank: number; - guessRank: number; - totalSolves: number; - averageGuesses: number; - } | null>(null); + let anonymousId = $state(""); + let statsSubmitted = $state(false); + let statsData = $state<{ + solveRank: number; + guessRank: number; + totalSolves: number; + averageGuesses: number; + } | null>(null); - let guessedIds = $derived(new Set(guesses.map((g) => g.book.id))); + let guessedIds = $derived(new Set(guesses.map((g) => g.book.id))); - const currentDate = $derived( - new Date().toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }), - ); + const currentDate = $derived( + new Date().toLocaleDateString("en-US", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }) + ); - let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); - let grade = $derived( - isWon - ? getGrade( - guesses.length, - getBookById(correctBookId)?.popularity ?? 0, - ) - : "", - ); + let isWon = $derived(guesses.some((g) => g.book.id === correctBookId)); + let grade = $derived( + isWon + ? getGrade(guesses.length, getBookById(correctBookId)?.popularity ?? 0) + : "" + ); - function getBookById(id: string): BibleBook | undefined { - return bibleBooks.find((b) => b.id === id); - } + function getBookById(id: string): BibleBook | undefined { + return bibleBooks.find((b) => b.id === id); + } - function isAdjacent(id1: string, id2: string): boolean { - const b1 = getBookById(id1); - const b2 = getBookById(id2); - return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1); - } + function isAdjacent(id1: string, id2: string): boolean { + const b1 = getBookById(id1); + const b2 = getBookById(id2); + return !!(b1 && b2 && Math.abs(b1.order - b2.order) === 1); + } - function submitGuess(bookId: string) { - if (guesses.some((g) => g.book.id === bookId)) return; + function submitGuess(bookId: string) { + if (guesses.some((g) => g.book.id === bookId)) return; - const book = getBookById(bookId); - if (!book) return; + const book = getBookById(bookId); + if (!book) return; - const correctBook = getBookById(correctBookId); - if (!correctBook) return; + const correctBook = getBookById(correctBookId); + if (!correctBook) return; - const testamentMatch = book.testament === correctBook.testament; - const sectionMatch = book.section === correctBook.section; - const adjacent = isAdjacent(book.id, correctBookId); + const testamentMatch = book.testament === correctBook.testament; + const sectionMatch = book.section === correctBook.section; + const adjacent = isAdjacent(book.id, correctBookId); - console.log( - `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}`, - ); + console.log( + `Guess: ${book.name} (order ${book.order}), Correct: ${correctBook.name} (order ${correctBook.order}), Adjacent: ${adjacent}` + ); - guesses = [ - { - book, - testamentMatch, - sectionMatch, - adjacent, - }, - ...guesses, - ]; + if (guesses.length === 0 && browser && (window as any).umami) { + (window as any).umami.track("first-guess"); + } - searchQuery = ""; - } + guesses = [ + { + book, + testamentMatch, + sectionMatch, + adjacent, + }, + ...guesses, + ]; - function generateUUID(): string { - // Try native randomUUID if available - if (typeof window.crypto.randomUUID === "function") { - return window.crypto.randomUUID(); - } + searchQuery = ""; + } - // 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 v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - } + function generateUUID(): string { + // Try native randomUUID if available + if (typeof window.crypto.randomUUID === "function") { + return window.crypto.randomUUID(); + } - function getOrCreateAnonymousId(): string { - if (!browser) return ""; - const key = "bibdle-anonymous-id"; - let id = localStorage.getItem(key); - if (!id) { - id = generateUUID(); - localStorage.setItem(key, id); - } - return id; - } + // 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 v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } - // Initialize anonymous ID - $effect(() => { - if (!browser) return; - anonymousId = getOrCreateAnonymousId(); - const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; - statsSubmitted = localStorage.getItem(statsKey) === "true"; - }); + function getOrCreateAnonymousId(): string { + if (!browser) return ""; + const key = "bibdle-anonymous-id"; + let id = localStorage.getItem(key); + if (!id) { + id = generateUUID(); + localStorage.setItem(key, id); + } + return id; + } - $effect(() => { - if (!browser) return; - isDev = window.location.host === "localhost:5173"; - }); + // Initialize anonymous ID + $effect(() => { + if (!browser) return; + anonymousId = getOrCreateAnonymousId(); + const statsKey = `bibdle-stats-submitted-${dailyVerse.date}`; + statsSubmitted = localStorage.getItem(statsKey) === "true"; + }); - // Load saved guesses - $effect(() => { - if (!browser) return; + $effect(() => { + if (!browser) return; + isDev = window.location.host === "localhost:5173"; + }); - const key = `bibdle-guesses-${dailyVerse.date}`; - const saved = localStorage.getItem(key); - if (saved) { - let savedIds: string[] = JSON.parse(saved); - savedIds = Array.from(new Set(savedIds)); - guesses = savedIds.map((bookId: string) => { - const book = getBookById(bookId)!; - const correctBook = getBookById(correctBookId)!; - const testamentMatch = book.testament === correctBook.testament; - const sectionMatch = book.section === correctBook.section; - const adjacent = isAdjacent(bookId, correctBookId); - return { - book, - testamentMatch, - sectionMatch, - adjacent, - }; - }); - } - }); + // Load saved guesses + $effect(() => { + if (!browser) return; - $effect(() => { - if (!browser) return; - localStorage.setItem( - `bibdle-guesses-${dailyVerse.date}`, - JSON.stringify(guesses.map((g) => g.book.id)), - ); - }); + const key = `bibdle-guesses-${dailyVerse.date}`; + const saved = localStorage.getItem(key); + if (saved) { + let savedIds: string[] = JSON.parse(saved); + savedIds = Array.from(new Set(savedIds)); + guesses = savedIds.map((bookId: string) => { + const book = getBookById(bookId)!; + const correctBook = getBookById(correctBookId)!; + const testamentMatch = book.testament === correctBook.testament; + const sectionMatch = book.section === correctBook.section; + const adjacent = isAdjacent(bookId, correctBookId); + return { + book, + testamentMatch, + sectionMatch, + adjacent, + }; + }); + } + }); - // Auto-submit stats when user wins - $effect(() => { - console.log("Stats effect triggered:", { - browser, - isWon, - anonymousId, - statsSubmitted, - statsData, - }); + $effect(() => { + if (!browser) return; + localStorage.setItem( + `bibdle-guesses-${dailyVerse.date}`, + JSON.stringify(guesses.map((g) => g.book.id)) + ); + }); - if (!browser || !isWon || !anonymousId) { - console.log("Basic conditions not met"); - return; - } + // Auto-submit stats when user wins + $effect(() => { + console.log("Stats effect triggered:", { + browser, + isWon, + anonymousId, + statsSubmitted, + statsData, + }); - if (statsSubmitted && !statsData) { - console.log("Fetching existing stats..."); + if (!browser || !isWon || !anonymousId) { + console.log("Basic conditions not met"); + return; + } - (async () => { - try { - const response = await fetch( - `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}`, - ); - const result = await response.json(); - console.log("Stats response:", result); + if (statsSubmitted && !statsData) { + console.log("Fetching existing stats..."); - if (result.success && result.stats) { - console.log("Setting stats data:", result.stats); - statsData = result.stats; - localStorage.setItem( - `bibdle-stats-submitted-${dailyVerse.date}`, - "true", - ); - } else if (result.error) { - console.error("Server error:", result.error); - } else { - console.error("Unexpected response format:", result); - } - } catch (err) { - console.error("Stats fetch failed:", err); - } - })(); + (async () => { + try { + const response = await fetch( + `/api/submit-completion?anonymousId=${anonymousId}&date=${dailyVerse.date}` + ); + const result = await response.json(); + console.log("Stats response:", result); - return; - } + if (result.success && result.stats) { + console.log("Setting stats data:", result.stats); + statsData = result.stats; + localStorage.setItem( + `bibdle-stats-submitted-${dailyVerse.date}`, + "true" + ); + } else if (result.error) { + console.error("Server error:", result.error); + } else { + console.error("Unexpected response format:", result); + } + } catch (err) { + console.error("Stats fetch failed:", err); + } + })(); - console.log("Submitting stats..."); + return; + } - async function submitStats() { - try { - const payload = { - anonymousId, - date: dailyVerse.date, - guessCount: guesses.length, - }; + console.log("Submitting stats..."); - console.log("Sending POST request with:", payload); + async function submitStats() { + try { + const payload = { + anonymousId, + date: dailyVerse.date, + guessCount: guesses.length, + }; - const response = await fetch("/api/submit-completion", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); + console.log("Sending POST request with:", payload); - const result = await response.json(); - console.log("Stats response:", result); + const response = await fetch("/api/submit-completion", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); - if (result.success && result.stats) { - console.log("Setting stats data:", result.stats); - statsData = result.stats; - statsSubmitted = true; - localStorage.setItem( - `bibdle-stats-submitted-${dailyVerse.date}`, - "true", - ); - } else if (result.error) { - console.error("Server error:", result.error); - } else { - console.error("Unexpected response format:", result); - } - } catch (err) { - console.error("Stats submission failed:", err); - } - } + const result = await response.json(); + console.log("Stats response:", result); - submitStats(); - }); + if (result.success && result.stats) { + console.log("Setting stats data:", result.stats); + statsData = result.stats; + statsSubmitted = true; + localStorage.setItem( + `bibdle-stats-submitted-${dailyVerse.date}`, + "true" + ); + } else if (result.error) { + console.error("Server error:", result.error); + } else { + console.error("Unexpected response format:", result); + } + } catch (err) { + console.error("Stats submission failed:", err); + } + } - function generateShareText(): string { - const emojis = guesses - .slice() - .reverse() - .map((guess) => { - if (guess.book.id === correctBookId) return "✅"; - if (guess.adjacent) return "‼️"; - if (guess.sectionMatch) return "🟩"; - if (guess.testamentMatch) return "🟧"; - return "🟥"; - }) - .join(""); + submitStats(); + }); - const dateFormatter = new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); - const formattedDate = dateFormatter.format( - new Date(`${dailyVerse.date}T00:00:00`), - ); - const siteUrl = window.location.origin; - return [ - `📖 Bibdle | ${formattedDate} 📖`, - `${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`, - `${emojis}`, - siteUrl, - ].join("\n"); - } + function generateShareText(): string { + const emojis = guesses + .slice() + .reverse() + .map((guess) => { + if (guess.book.id === correctBookId) return "✅"; + if (guess.adjacent) return "‼️"; + if (guess.sectionMatch) return "🟩"; + if (guess.testamentMatch) return "🟧"; + return "🟥"; + }) + .join(""); - async function share() { - if (!browser) return; + const dateFormatter = new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + const formattedDate = dateFormatter.format( + new Date(`${dailyVerse.date}T00:00:00`) + ); + const siteUrl = window.location.origin; + return [ + `📖 Bibdle | ${formattedDate} 📖`, + `${grade} (${guesses.length} ${guesses.length == 1 ? "guess" : "guesses"})`, + `${emojis}`, + siteUrl, + ].join("\n"); + } - const shareText = generateShareText(); + async function share() { + if (!browser) return; - try { - if ("share" in navigator) { - await (navigator as any).share({ text: shareText }); - } else { - await (navigator as any).clipboard.writeText(shareText); - } - } catch (err) { - console.error("Share failed:", err); - throw err; - } - } + const shareText = generateShareText(); - async function copyToClipboard() { - if (!browser) return; + try { + if ("share" in navigator) { + await (navigator as any).share({ text: shareText }); + } else { + await (navigator as any).clipboard.writeText(shareText); + } + } catch (err) { + console.error("Share failed:", err); + throw err; + } + } - const shareText = generateShareText(); + async function copyToClipboard() { + if (!browser) return; - try { - await (navigator as any).clipboard.writeText(shareText); - copied = true; - setTimeout(() => { - copied = false; - }, 5000); - } catch (err) { - console.error("Copy to clipboard failed:", err); - throw err; - } - } + const shareText = generateShareText(); - function handleShare() { - if (copied || !browser) return; - const useClipboard = !("share" in navigator); - if (useClipboard) { - copied = true; - } - share() - .then(() => { - if (useClipboard) { - setTimeout(() => { - copied = false; - }, 5000); - } - }) - .catch(() => { - if (useClipboard) { - copied = false; - } - }); - } + try { + await (navigator as any).clipboard.writeText(shareText); + copied = true; + setTimeout(() => { + copied = false; + }, 5000); + } catch (err) { + console.error("Copy to clipboard failed:", err); + throw err; + } + } - function clearLocalStorage() { - if (!browser) return; - // Clear all bibdle-related localStorage items - const keysToRemove: string[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith("bibdle-")) { - keysToRemove.push(key); - } - } - keysToRemove.forEach((key) => localStorage.removeItem(key)); - // Reload the page to reset state - window.location.reload(); - } + function handleShare() { + if (copied || !browser) return; + const useClipboard = !("share" in navigator); + if (useClipboard) { + copied = true; + } + share() + .then(() => { + if (useClipboard) { + setTimeout(() => { + copied = false; + }, 5000); + } + }) + .catch(() => { + if (useClipboard) { + copied = false; + } + }); + } + + function clearLocalStorage() { + if (!browser) return; + // Clear all bibdle-related localStorage items + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith("bibdle-")) { + keysToRemove.push(key); + } + } + keysToRemove.forEach((key) => localStorage.removeItem(key)); + // Reload the page to reset state + window.location.reload(); + } - Bibdle — A daily bible game{isDev ? " (dev)" : ""} - + Bibdle — A daily bible game{isDev ? " (dev)" : ""} +

-
-

- -
-

-
- {isDev ? "Dev Edition | " : ""}{currentDate} -
+
+

+ +
+

+
+ {isDev ? "Dev Edition | " : ""}{currentDate} +
- + - {#if !isWon} - - {:else} - - - {/if} + {#if !isWon} + + {:else} + + + {/if} - - {#if isWon} - - {/if} - {#if isDev} - - {/if} -
+ + {#if isWon} + + {/if} + {#if isDev} + + {/if} +
diff --git a/todo.md b/todo.md index 8e56d71..a02acba 100644 --- a/todo.md +++ b/todo.md @@ -1,43 +1,38 @@ # in progress - - root menu: classic / imposter mode / impossible mode (complete today's classic and imposter modes to unlock) - # todo - impossible mode (1904 greek bible) three guesses only. - - share both classic and impossible mode with both buttons + + - share both classic and impossible mode with both buttons - add imposter mode - instructions - - classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. - - imposter mode: out of four options, identify the verse that is not in the Bible - - impossible mode: identify which book of the bible the verse is from in less than three guesses. + + - classic mode: identify what book the verse is from (e.g. Genesis, John, Revelations...) in as few guesses as possible. + - imposter mode: out of four options, identify the verse that is not in the Bible + - impossible mode: identify which book of the bible the verse is from in less than three guesses. - add login + saved stats + streak etc. - add deuterocanonical books + - Practice mode: Unlimited verses - Create public or private leaderboards -- Passport book with badges: +- Passport book with awards: + - Guess each Gospel first try - "Guessed all Gospels", "Perfect week", "Old Testament expert" - Theologian: Guess each book first try - - + - If chapter is 6 and verse 7, earn award "Six seven" - difficult mode (guess old or new testament, first try _only_) (???) -# places to send - -- linkedin post -- ocf discord server ✅ -- nick makiej ✅ - # About this game As a young camper at the Metropolis of Boston Camp, I remember His Eminence Metropolitan Methodios would visit every Sunday. He was often surrounded by important people for his entire time there, so I never gathered the courage to introduce myself, but his homilies during Liturgy always stood out to me. In some ways, they differed year after year, but a majority of his message remained strikingly familiar. "Take ten minutes to read the Bible every day," he asked. "Just ten minutes. Go somewhere quiet, turn off the TV (then iPod, then cell phone), and read in peace and quiet." @@ -52,6 +47,10 @@ I created Bibdle from a combination of two things. The first is my lifelong desi # done +## december 27th + +- add event log to submitting first-guess or correct-guess to umami (to make bounce rate more accurate) + ## december 26th - created embeddings for every bible verse (verse similarity finder) From 2b7682684af8e3437d5f7015f7df3b4cdce1a516 Mon Sep 17 00:00:00 2001 From: George Powell Date: Sat, 27 Dec 2025 11:50:35 -0500 Subject: [PATCH 3/5] fixed sloppy umami events --- src/lib/components/WinScreen.svelte | 8 -------- src/routes/+page.svelte | 24 ++++++++++++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/lib/components/WinScreen.svelte b/src/lib/components/WinScreen.svelte index 0e69813..487a634 100644 --- a/src/lib/components/WinScreen.svelte +++ b/src/lib/components/WinScreen.svelte @@ -71,14 +71,6 @@ // Generate the congratulations message let congratulationsMessage = $derived(getRandomCongratulationsMessage()); - - onMount(() => { - if (typeof window !== "undefined" && (window as any).umami) { - (window as any).umami.track("guessed-correctly", { - totalGuesses: guessCount, - }); - } - });
{ + if (!browser || !isWon) return; + const key = `bibdle-win-tracked-${dailyVerse.date}`; + if (localStorage.getItem(key) === "true") return; + if ((window as any).umami) { + (window as any).umami.track("Guessed correctly", { + totalGuesses: guesses.length, + }); + } + localStorage.setItem(key, "true"); + }); + function generateShareText(): string { const emojis = guesses .slice() From bf4ec8cfe01a7d88e00c4d59b41b3a7a346b7304 Mon Sep 17 00:00:00 2001 From: George Powell Date: Tue, 30 Dec 2025 15:15:06 -0500 Subject: [PATCH 4/5] average guesses script --- scripts/average-guesses.ts | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 scripts/average-guesses.ts diff --git a/scripts/average-guesses.ts b/scripts/average-guesses.ts new file mode 100644 index 0000000..2a07b08 --- /dev/null +++ b/scripts/average-guesses.ts @@ -0,0 +1,53 @@ +import Database from 'bun:sqlite'; + +// Database path - adjust if your database is located elsewhere +const dbPath = process.env.DATABASE_URL || './local.db'; + +console.log(`Connecting to database: ${dbPath}`); + +const db = new Database(dbPath); + +// Query all rows from daily_completions +const query = db.query(` + SELECT date, guess_count + FROM daily_completions + ORDER BY date +`); + +const rows = query.all() as { date: string; guess_count: number }[]; + +if (rows.length === 0) { + console.log('No completions found in the database.'); + db.close(); + process.exit(0); +} + +// Group by date and calculate average guesses +const dateStats = new Map(); + +for (const row of rows) { + const existing = dateStats.get(row.date) || { total: 0, count: 0 }; + existing.total += row.guess_count; + existing.count += 1; + dateStats.set(row.date, existing); +} + +// Display results +console.log('\n=== Average Guesses Per Day ===\n'); +console.log('Date | Avg Guesses | Total Completions'); +console.log('--------------|-------------|-------------------'); + +for (const [date, stats] of dateStats) { + const avg = (stats.total / stats.count).toFixed(2); + console.log(`${date.padEnd(14)}| ${avg.padStart(11)}| ${stats.count.toString().padStart(19)}`); +} + +// Calculate overall average +const totalGuesses = Array.from(dateStats.values()).reduce((sum, s) => sum + s.total, 0); +const totalCompletions = Array.from(dateStats.values()).reduce((sum, s) => sum + s.count, 0); +const overallAvg = (totalGuesses / totalCompletions).toFixed(2); + +console.log('--------------|-------------|-------------------'); +console.log(`Overall Average: ${overallAvg} guesses across ${totalCompletions} completions`); + +db.close(); From a83039a6a22465825cd54df69c502e96bc369e5c Mon Sep 17 00:00:00 2001 From: George Powell Date: Tue, 30 Dec 2025 15:21:18 -0500 Subject: [PATCH 5/5] style changes --- src/routes/+page.svelte | 7 ++++--- src/routes/layout.css | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 577c09d..5e09c86 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -386,11 +386,12 @@ - Bibdle — A daily bible game{isDev ? " (dev)" : ""} - Bibdle — A daily bible game{isDev ? " (dev)" : ""} --> + A daily bible game{isDev ? " (dev)" : ""} +
diff --git a/src/routes/layout.css b/src/routes/layout.css index a1824ae..77a35d8 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -7,7 +7,7 @@ } html, body { - background: oklch(98.11% 0.02777 158.93); + background: oklch(89.126% 0.06134 298.626); } .big-text {