// Game logic for morse code practice import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts"; import wordsArray from "an-array-of-english-words" with { type: "json" }; export type GameMode = | "letters" | "alphanumeric" | "full" | "words" | "phrases"; export interface GameConfig { mode: GameMode; rounds: number; timePerRound: number; // seconds dynamicTime?: boolean; // For words/phrases: multiply time by character count } export interface RoundResult { challenge: string; expectedMorse: string; userInput: string; correct: boolean; timeSpent: number; // milliseconds } export interface GameSession { config: GameConfig; currentRound: number; results: RoundResult[]; currentStreak: number; bestStreak: number; } // Constants for word filtering const MIN_WORD_LENGTH = 3; const MAX_WORD_LENGTH = 8; const WORD_POOL_SIZE = 500; // Character sets for different difficulty levels const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); const NUMBERS = "0123456789".split(""); const PUNCTUATION = ".,?!-/()@".split(""); const ALPHANUMERIC = [...LETTERS, ...NUMBERS]; const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION]; /** * Fisher-Yates shuffle algorithm - O(n) complexity */ function shuffleArray(array: T[]): T[] { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } // Filter words for morse code practice (3-8 letters, common words) const ALL_WORDS = wordsArray .filter((word: string) => { const upper = word.toUpperCase(); return ( word.length >= MIN_WORD_LENGTH && word.length <= MAX_WORD_LENGTH && /^[A-Z]+$/.test(upper) // Only letters, no special chars ); }) .map((word: string) => word.toUpperCase()); // Randomly select a subset for variety using Fisher-Yates const WORDS = shuffleArray(ALL_WORDS).slice(0, WORD_POOL_SIZE); /** * Generate a random phrase from word combinations */ function generatePhrase(): string { const phraseLength = Math.floor(Math.random() * 3) + 2; // 2-4 words const selectedWords: string[] = []; for (let i = 0; i < phraseLength; i++) { const word = WORDS[Math.floor(Math.random() * WORDS.length)]; selectedWords.push(word); } return selectedWords.join(" "); } /** * Get a random challenge based on game mode */ export function getChallenge(mode: GameMode): string { switch (mode) { case "letters": return LETTERS[Math.floor(Math.random() * LETTERS.length)]; case "alphanumeric": return ALPHANUMERIC[Math.floor(Math.random() * ALPHANUMERIC.length)]; case "full": return FULL_SET[Math.floor(Math.random() * FULL_SET.length)]; case "words": return WORDS[Math.floor(Math.random() * WORDS.length)]; case "phrases": return generatePhrase(); } } /** * Create a new game session */ export function createGameSession(config: GameConfig): GameSession { return { config, currentRound: 0, results: [], currentStreak: 0, bestStreak: 0, }; } /** * Process a round result */ export function processRound( session: GameSession, challenge: string, userInput: string, timeSpent: number ): RoundResult { const expectedMorse = textToMorse(challenge); const normalizedInput = normalizeMorse(userInput); const correct = compareMorse(expectedMorse, normalizedInput); const result: RoundResult = { challenge, expectedMorse, userInput: normalizedInput, correct, timeSpent, }; session.results.push(result); session.currentRound++; // Update streak if (correct) { session.currentStreak++; session.bestStreak = Math.max(session.bestStreak, session.currentStreak); } else { session.currentStreak = 0; } return result; } /** * Check if game is complete */ export function isGameComplete(session: GameSession): boolean { return session.currentRound >= session.config.rounds; } /** * Get game summary statistics */ export function getGameSummary(session: GameSession) { const totalRounds = session.results.length; const correct = session.results.filter((r) => r.correct).length; const incorrect = totalRounds - correct; const accuracy = totalRounds > 0 ? (correct / totalRounds) * 100 : 0; const averageTime = totalRounds > 0 ? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds : 0; return { totalRounds, correct, incorrect, accuracy, averageTime, streak: session.bestStreak, }; } /** * Get difficulty description for a mode */ export function getDifficultyDescription(mode: GameMode): string { switch (mode) { case "letters": return "Single letters A-Z"; case "alphanumeric": return "Letters A-Z and numbers 0-9"; case "full": return "Letters, numbers, and punctuation"; case "words": return "Common 4-6 letter words"; case "phrases": return "Short phrases"; } }