Files
bibdle/src/lib/utils/game.ts
George Powell e550965086 add unit tests for core game, bible, share, and stats utilities
146 tests covering evaluateGuess, grading, ordinals, bible data
integrity, section counts, share text generation, and stat
string helpers. Also fixes toOrdinal for 111-113 (was using
>= 11 && <= 13 instead of % 100 check).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:02:46 -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 % 100 >= 11 && n % 100 <= 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);
});
}