184 lines
4.5 KiB
TypeScript
184 lines
4.5 KiB
TypeScript
// 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;
|
|
}
|
|
|
|
// 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];
|
|
|
|
// 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 >= 3 && word.length <= 8 && /^[A-Z]+$/.test(upper) // Only letters, no special chars
|
|
);
|
|
})
|
|
.map((word: string) => word.toUpperCase());
|
|
|
|
// Randomly select a subset for variety
|
|
const WORDS = ALL_WORDS.sort(() => Math.random() - 0.5).slice(0, 500);
|
|
|
|
/**
|
|
* 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";
|
|
}
|
|
}
|