Files
morse-trainer/main.ts

326 lines
8.4 KiB
TypeScript

#!/usr/bin/env -S deno run --allow-read --allow-write
import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors";
import {
clearScreen,
printBanner,
showMainMenu,
selectGameMode,
configureGame,
playRound,
showGameResults,
showStats,
showReference,
showTranslator,
confirmAction,
} from "./ui.ts";
import { createGameSession, isGameComplete } from "./game.ts";
import type { GameMode } from "./game.ts";
import { loadStats, updateStats, resetStats, getAccuracy } from "./stats.ts";
import type { GameResult } from "./stats.ts";
/**
* Main application class
*/
class MorseGame {
/**
* Run the interactive menu
*/
async runInteractive(): Promise<void> {
while (true) {
clearScreen();
printBanner();
const action = await showMainMenu();
switch (action) {
case "play":
await this.playGame();
break;
case "stats":
await this.viewStats();
break;
case "reference":
await this.showReference();
break;
case "translator":
await this.showTranslator();
break;
case "reset":
await this.resetStats();
break;
case "exit":
console.log(
colors.cyan("\nThanks for playing! 73 (Best regards in morse)\n")
);
return;
}
}
}
/**
* Play a game
*/
async playGame(): Promise<void> {
const mode = await selectGameMode();
const config = await configureGame(mode);
const session = createGameSession({
mode,
rounds: config.rounds,
timePerRound: config.timePerRound,
dynamicTime: config.dynamicTime,
});
// Play all rounds
for (let i = 0; i < config.rounds; i++) {
await playRound(session, i + 1);
if (isGameComplete(session)) {
break;
}
}
// Show results
showGameResults(session);
// Update stats
const summary = session.results.reduce(
(acc, result) => ({
correct: acc.correct + (result.correct ? 1 : 0),
incorrect: acc.incorrect + (result.correct ? 0 : 1),
totalTime: acc.totalTime + result.timeSpent,
}),
{ correct: 0, incorrect: 0, totalTime: 0 }
);
const gameResult: GameResult = {
mode,
rounds: session.results.length,
correct: summary.correct,
incorrect: summary.incorrect,
averageTime: summary.totalTime / session.results.length / 1000,
streak: session.bestStreak,
};
try {
await updateStats(gameResult);
} catch (error) {
console.log(colors.red("\n⚠ Warning: Failed to save statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
// Wait for user to continue
console.log(colors.gray("\nPress Enter to continue..."));
await new Promise((resolve) => {
const buf = new Uint8Array(1);
Deno.stdin.setRaw(true);
Deno.stdin.read(buf)
.then(() => resolve(undefined))
.catch(() => resolve(undefined))
.finally(() => Deno.stdin.setRaw(false));
});
}
/**
* View statistics
*/
async viewStats(): Promise<void> {
const stats = await loadStats();
showStats(stats);
// Wait for user to continue
console.log(colors.gray("Press Enter to continue..."));
await new Promise((resolve) => {
const buf = new Uint8Array(1);
Deno.stdin.setRaw(true);
Deno.stdin.read(buf)
.then(() => resolve(undefined))
.catch(() => resolve(undefined))
.finally(() => Deno.stdin.setRaw(false));
});
}
/**
* Show reference
*/
async showReference(): Promise<void> {
showReference();
// Wait for user to continue
console.log(colors.gray("Press Enter to continue..."));
await new Promise((resolve) => {
const buf = new Uint8Array(1);
Deno.stdin.setRaw(true);
Deno.stdin.read(buf)
.then(() => resolve(undefined))
.catch(() => resolve(undefined))
.finally(() => Deno.stdin.setRaw(false));
});
}
/**
* Show translator
*/
async showTranslator(): Promise<void> {
await showTranslator();
}
/**
* Reset statistics
*/
async resetStats(): Promise<void> {
const confirmed = await confirmAction(
"Are you sure you want to reset all statistics? This cannot be undone."
);
if (confirmed) {
try {
await resetStats();
console.log(colors.green("\n✓ Statistics have been reset.\n"));
} catch (error) {
console.log(colors.red("\n⚠ Error: Failed to reset statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
await new Promise((resolve) => setTimeout(resolve, 1500));
}
}
/**
* Quick play with CLI arguments
*/
async quickPlay(mode: string, rounds: number, time: number): Promise<void> {
const validModes = ["letters", "alphanumeric", "full", "words", "phrases"];
if (!validModes.includes(mode)) {
console.error(colors.red(`Invalid mode: ${mode}`));
console.log(`Valid modes: ${validModes.join(", ")}`);
Deno.exit(1);
}
clearScreen();
printBanner();
const session = createGameSession({
mode: mode as GameMode,
rounds,
timePerRound: time,
});
// Play all rounds
for (let i = 0; i < rounds; i++) {
await playRound(session, i + 1);
}
// Show results
showGameResults(session);
// Update stats
const summary = session.results.reduce(
(acc, result) => ({
correct: acc.correct + (result.correct ? 1 : 0),
incorrect: acc.incorrect + (result.correct ? 0 : 1),
totalTime: acc.totalTime + result.timeSpent,
}),
{ correct: 0, incorrect: 0, totalTime: 0 }
);
const gameResult: GameResult = {
mode: mode as GameMode,
rounds: session.results.length,
correct: summary.correct,
incorrect: summary.incorrect,
averageTime: summary.totalTime / session.results.length / 1000,
streak: session.bestStreak,
};
try {
await updateStats(gameResult);
} catch (error) {
console.log(colors.red("\n⚠ Warning: Failed to save statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
}
/**
* Show quick stats
*/
async showQuickStats(): Promise<void> {
const stats = await loadStats();
if (stats.totalGames === 0) {
console.log(colors.yellow("No games played yet!"));
return;
}
console.log(colors.bold.cyan("\n📈 QUICK STATS\n"));
console.log(`Games Played: ${colors.bold(stats.totalGames.toString())}`);
console.log(`Total Rounds: ${colors.bold(stats.totalRounds.toString())}`);
console.log(
`Accuracy: ${colors.bold(getAccuracy(stats).toFixed(1) + "%")}`
);
console.log(`Best Streak: ${colors.bold(stats.bestStreak.toString())}`);
console.log();
}
}
// CLI Setup
const app = new MorseGame();
await new Command()
.name("morse-game")
.version("1.0.0")
.description("A terminal-based morse code practice game")
.action(async () => {
// Default action: run interactive mode
await app.runInteractive();
})
.command("play", "Start a quick game with specified settings")
.option(
"-m, --mode <mode:string>",
"Game mode (letters, alphanumeric, full, words, phrases)",
{
default: "letters",
}
)
.option("-r, --rounds <rounds:number>", "Number of rounds", { default: 10 })
.option("-t, --time <seconds:number>", "Seconds per round", { default: 30 })
.action(async (options) => {
await app.quickPlay(options.mode, options.rounds, options.time);
})
.command("stats", "Display your statistics")
.action(async () => {
await app.showQuickStats();
})
.command("reset", "Reset all statistics")
.action(async () => {
const confirmed = await confirmAction(
"Are you sure you want to reset all statistics? This cannot be undone."
);
if (confirmed) {
try {
await resetStats();
console.log(colors.green("\n✓ Statistics have been reset.\n"));
} catch (error) {
console.log(colors.red("\n⚠ Error: Failed to reset statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
} else {
console.log(colors.yellow("\nCancelled.\n"));
}
})
.command("reference", "Show morse code reference")
.action(() => {
showReference();
})
.parse(Deno.args);