298 lines
7.5 KiB
TypeScript
298 lines
7.5 KiB
TypeScript
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env
|
|
|
|
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,
|
|
};
|
|
|
|
await updateStats(gameResult);
|
|
|
|
// 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(() => {
|
|
Deno.stdin.setRaw(false);
|
|
resolve(undefined);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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(() => {
|
|
Deno.stdin.setRaw(false);
|
|
resolve(undefined);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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(() => {
|
|
Deno.stdin.setRaw(false);
|
|
resolve(undefined);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
await resetStats();
|
|
console.log(colors.green("\n✓ Statistics have been reset.\n"));
|
|
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,
|
|
};
|
|
|
|
await updateStats(gameResult);
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
await resetStats();
|
|
console.log(colors.green("\n✓ Statistics have been reset.\n"));
|
|
} else {
|
|
console.log(colors.yellow("\nCancelled.\n"));
|
|
}
|
|
})
|
|
.command("reference", "Show morse code reference")
|
|
.action(() => {
|
|
showReference();
|
|
})
|
|
.parse(Deno.args);
|