Files
bibdle/src/lib/utils/game.ts
George Powell e6081c28f1 Refactor game logic into utility modules and add cross-device sync
Extracted game state management, share logic, and stats API calls into dedicated modules (game-persistence.svelte.ts, share.ts, stats-client.ts), and moved daily verse loading to client-side to fix timezone issues. Added a guesses column to daily_completions for cross-device state restoration for logged-in users, a new GET /api/stats endpoint, and a staging deploy script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 13:25:40 -05:00

101 lines
3.2 KiB
TypeScript

import { bibleBooks, type BibleBook } from '$lib/types/bible';
export interface Guess {
book: BibleBook;
testamentMatch: boolean;
sectionMatch: boolean;
adjacent: boolean;
firstLetterMatch: boolean;
}
export function getBookById(id: string): BibleBook | undefined {
return bibleBooks.find((b) => b.id === id);
}
export 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);
}
export function getFirstLetter(bookName: string): string {
const match = bookName.match(/[a-zA-Z]/);
return match ? match[0] : bookName[0];
}
export function evaluateGuess(guessBookId: string, correctBookId: string): Guess | null {
const book = getBookById(guessBookId);
const correctBook = getBookById(correctBookId);
if (!book || !correctBook) return null;
const testamentMatch = book.testament === correctBook.testament;
const sectionMatch = book.section === correctBook.section;
const adjacent = isAdjacent(guessBookId, correctBookId);
// Special case: if correct book is in the Epistles + starts with "1",
// any guess starting with "1" counts as first letter match
const correctIsEpistlesWithNumber =
(correctBook.section === "Pauline Epistles" ||
correctBook.section === "General Epistles") &&
correctBook.name[0] === "1";
const guessIsEpistlesWithNumber =
(book.section === "Pauline Epistles" ||
book.section === "General Epistles") &&
book.name[0] === "1";
const firstLetterMatch =
correctIsEpistlesWithNumber && guessIsEpistlesWithNumber
? true
: getFirstLetter(book.name).toUpperCase() ===
getFirstLetter(correctBook.name).toUpperCase();
return {
book,
testamentMatch,
sectionMatch,
adjacent,
firstLetterMatch,
};
}
export function getGrade(numGuesses: number): string {
if (numGuesses === 1) return "S+";
if (numGuesses === 2) return "A+";
if (numGuesses === 3) return "A";
if (numGuesses >= 4 && numGuesses <= 6) return "B+";
if (numGuesses >= 7 && numGuesses <= 10) return "B";
if (numGuesses >= 11 && numGuesses <= 15) return "C+";
return "C";
}
export function getNextGradeMessage(numGuesses: number): string {
if (numGuesses === 1) return "";
if (numGuesses === 2) return "Next grade: 1 guess or less";
if (numGuesses === 3) return "Next grade: 2 guesses or less";
if (numGuesses >= 4 && numGuesses <= 6) return "Next grade: 3 guesses or less";
if (numGuesses >= 7 && numGuesses <= 10) return "Next grade: 6 guesses or less";
if (numGuesses >= 11 && numGuesses <= 15) return "Next grade: 10 guesses or less";
return "Next grade: 15 guesses or less";
}
export function toOrdinal(n: number): string {
if (n >= 11 && n <= 13) {
return `${n}th`;
}
const mod = n % 10;
const suffix = mod === 1 ? "st" : mod === 2 ? "nd" : mod === 3 ? "rd" : "th";
return `${n}${suffix}`;
}
export function generateUUID(): string {
if (typeof window !== 'undefined' && typeof (window as any).crypto?.randomUUID === "function") {
return (window as any).crypto.randomUUID();
}
// Fallback
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = ((window as any).crypto.getRandomValues(new Uint8Array(1))[0] % 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}