first commit

This commit is contained in:
Irinel Sarbu
2025-11-21 13:03:06 +01:00
commit 3f98712766
8 changed files with 1206 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# Deno
.deno/
deno.lock
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# User data
.morse-game/

169
README.md Normal file
View File

@@ -0,0 +1,169 @@
# Morse Code Practice Game 🎮
A terminal-based morse code practice game built with Deno. Improve your morse code translation skills through interactive gameplay with multiple difficulty levels and comprehensive statistics tracking.
## Features
- **Multiple Difficulty Modes**
- 🟢 Easy: Single letters (A-Z)
- 🟡 Medium: Single numbers (0-9)
- 🟣 Hard: Common words
- 🔴 Expert: Short phrases
- **Interactive TUI Interface**
- Beautiful terminal UI with colors and tables
- Real-time timer for each round
- Immediate feedback on answers
- Progress tracking with streak counter
- **Statistics Tracking**
- Overall accuracy and performance metrics
- Mode-specific statistics
- Best streak tracking
- Historical game data
- Average time per round
- **Flexible CLI**
- Interactive menu mode (default)
- Quick play mode with command-line arguments
- Stats viewer
- Built-in morse code reference
## Installation
Make sure you have [Deno](https://deno.land/) installed.
```bash
# Clone or download this repository
cd morse-game
# Make the script executable (optional)
chmod +x main.ts
```
## Usage
### Interactive Mode (Default)
Run the game and navigate through the menu:
```bash
deno task start
# or
deno run --allow-read --allow-write --allow-env main.ts
```
### Quick Play Mode
Start a game directly with custom settings:
```bash
# Play 5 rounds of letters mode with 20 seconds per round
deno task start play --mode letters --rounds 5 --time 20
# Play expert mode
deno task start play --mode phrases --rounds 10 --time 45
```
Options:
- `-m, --mode <mode>` - Game mode: `letters`, `numbers`, `words`, or `phrases` (default: letters)
- `-r, --rounds <number>` - Number of rounds (default: 10)
- `-t, --time <seconds>` - Seconds per round (default: 30)
### View Statistics
```bash
deno task start stats
```
### Show Reference
Display a morse code reference chart:
```bash
deno task start reference
```
### Reset Statistics
```bash
deno task start reset
```
## How to Play
1. **Select a difficulty mode** - Choose from letters, numbers, words, or phrases
2. **Configure your game** - Set the number of rounds and time per round
3. **Translate to morse code** - You'll be shown a challenge (letter, number, word, or phrase)
4. **Enter your answer** - Type the morse code using:
- `.` (dot) for short signals
- `-` (dash) for long signals
- Space to separate letters
- `/` to separate words
5. **Get instant feedback** - See if you got it right and track your streak
6. **Review your performance** - After all rounds, view detailed statistics
## Morse Code Basics
- Use `.` for dots and `-` for dashes
- Separate letters with spaces
- Use `/` for word boundaries
- Example: `HELLO` = `.... . .-.. .-.. ---`
- Example: `SOS` = `... --- ...`
## Statistics
The game tracks:
- Total games played
- Total rounds completed
- Correct and incorrect answers
- Overall accuracy percentage
- Average time per round
- Best streak (consecutive correct answers)
- Per-mode statistics
Stats are saved to `~/.morse-game/stats.json`
## Development
```bash
# Run in watch mode for development
deno task dev
# Run with specific permissions
deno run --allow-read --allow-write --allow-env main.ts
```
## Project Structure
```
morse-game/
├── main.ts # CLI entry point and command parser
├── game.ts # Game logic and session management
├── morse.ts # Morse code translation utilities
├── stats.ts # Statistics tracking and persistence
├── ui.ts # TUI interface and menus
├── deno.json # Deno configuration and dependencies
└── README.md # This file
```
## Dependencies
- [@cliffy/command](https://jsr.io/@cliffy/command) - CLI framework
- [@cliffy/prompt](https://jsr.io/@cliffy/prompt) - Interactive prompts
- [@cliffy/ansi](https://jsr.io/@cliffy/ansi) - ANSI colors and formatting
- [@cliffy/table](https://jsr.io/@cliffy/table) - Table rendering
- [@std/path](https://jsr.io/@std/path) - Path utilities
- [@std/fs](https://jsr.io/@std/fs) - File system utilities
## License
MIT
## Contributing
Feel free to open issues or submit pull requests!
---
**73** (Best regards in morse code) 📡

18
deno.json Normal file
View File

@@ -0,0 +1,18 @@
{
"tasks": {
"start": "deno run --allow-read --allow-write --allow-env main.ts",
"dev": "deno run --watch --allow-read --allow-write --allow-env main.ts"
},
"imports": {
"@std/path": "jsr:@std/path@^1.0.0",
"@std/fs": "jsr:@std/fs@^1.0.0",
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7",
"@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.7",
"@cliffy/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.7",
"@cliffy/table": "jsr:@cliffy/table@^1.0.0-rc.7"
},
"compilerOptions": {
"lib": ["deno.window"],
"strict": true
}
}

158
game.ts Normal file
View File

@@ -0,0 +1,158 @@
// Game logic for morse code practice
import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts";
export type GameMode = 'letters' | 'numbers' | 'words' | 'phrases';
export interface GameConfig {
mode: GameMode;
rounds: number;
timePerRound: number; // seconds
}
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;
}
// Word banks for different difficulty levels
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const NUMBERS = '0123456789'.split('');
const WORDS = [
'HELLO', 'WORLD', 'CODE', 'TIME', 'GAME', 'TEST', 'PLAY',
'FAST', 'SLOW', 'HELP', 'GOOD', 'BEST', 'NICE', 'COOL',
'MORSE', 'SIGNAL', 'RADIO', 'SEND', 'MESSAGE', 'QUICK',
'LEARN', 'PRACTICE', 'SKILL', 'MASTER', 'EXPERT',
];
const PHRASES = [
'HELLO WORLD',
'GOOD MORNING',
'HOW ARE YOU',
'THANK YOU',
'SEE YOU SOON',
'HAVE A NICE DAY',
'MORSE CODE',
'QUICK BROWN FOX',
'THE END',
'WELL DONE',
];
/**
* 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 'numbers':
return NUMBERS[Math.floor(Math.random() * NUMBERS.length)];
case 'words':
return WORDS[Math.floor(Math.random() * WORDS.length)];
case 'phrases':
return PHRASES[Math.floor(Math.random() * PHRASES.length)];
}
}
/**
* 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 'numbers':
return 'Single digits 0-9';
case 'words':
return 'Common 4-6 letter words';
case 'phrases':
return 'Short phrases';
}
}

276
main.ts Normal file
View File

@@ -0,0 +1,276 @@
#!/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,
confirmAction,
} from "./ui.ts";
import { createGameSession, isGameComplete } 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 "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,
});
// 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);
});
});
}
/**
* 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", "numbers", "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 any,
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 any,
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, numbers, 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);

95
morse.ts Normal file
View File

@@ -0,0 +1,95 @@
// Morse code translation utilities
export const MORSE_CODE: Record<string, string> = {
'A': '.-',
'B': '-...',
'C': '-.-.',
'D': '-..',
'E': '.',
'F': '..-.',
'G': '--.',
'H': '....',
'I': '..',
'J': '.---',
'K': '-.-',
'L': '.-..',
'M': '--',
'N': '-.',
'O': '---',
'P': '.--.',
'Q': '--.-',
'R': '.-.',
'S': '...',
'T': '-',
'U': '..-',
'V': '...-',
'W': '.--',
'X': '-..-',
'Y': '-.--',
'Z': '--..',
'0': '-----',
'1': '.----',
'2': '..---',
'3': '...--',
'4': '....-',
'5': '.....',
'6': '-....',
'7': '--...',
'8': '---..',
'9': '----.',
' ': '/',
'.': '.-.-.-',
',': '--..--',
'?': '..--..',
'!': '-.-.--',
'-': '-....-',
'/': '-..-.',
'@': '.--.-.',
'(': '-.--.',
')': '-.--.-'
};
// Reverse mapping for morse to text
export const MORSE_TO_TEXT: Record<string, string> = Object.fromEntries(
Object.entries(MORSE_CODE).map(([key, value]) => [value, key])
);
/**
* Convert text to morse code
*/
export function textToMorse(text: string): string {
return text
.toUpperCase()
.split('')
.map(char => MORSE_CODE[char] || char)
.join(' ');
}
/**
* Convert morse code to text
*/
export function morseToText(morse: string): string {
return morse
.split(' ')
.map(code => MORSE_TO_TEXT[code] || code)
.join('');
}
/**
* Normalize morse code input (remove extra spaces, normalize separators)
*/
export function normalizeMorse(input: string): string {
return input
.trim()
.replace(/\s+/g, ' ')
.replace(/\//g, ' / ');
}
/**
* Compare two morse code strings with tolerance for spacing differences
*/
export function compareMorse(expected: string, actual: string): boolean {
const normalizedExpected = normalizeMorse(expected);
const normalizedActual = normalizeMorse(actual);
return normalizedExpected === normalizedActual;
}

126
stats.ts Normal file
View File

@@ -0,0 +1,126 @@
// User statistics tracking and persistence
import { join } from "@std/path";
import { ensureDir } from "@std/fs";
export interface GameStats {
totalGames: number;
totalRounds: number;
correctAnswers: number;
incorrectAnswers: number;
averageTimePerRound: number;
bestStreak: number;
modeStats: {
letters: ModeStats;
numbers: ModeStats;
words: ModeStats;
phrases: ModeStats;
};
lastPlayed?: string;
}
export interface ModeStats {
gamesPlayed: number;
correctAnswers: number;
incorrectAnswers: number;
averageAccuracy: number;
}
export interface GameResult {
mode: 'letters' | 'numbers' | 'words' | 'phrases';
rounds: number;
correct: number;
incorrect: number;
averageTime: number;
streak: number;
}
const DEFAULT_STATS: GameStats = {
totalGames: 0,
totalRounds: 0,
correctAnswers: 0,
incorrectAnswers: 0,
averageTimePerRound: 0,
bestStreak: 0,
modeStats: {
letters: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 },
numbers: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 },
words: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 },
phrases: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 },
},
};
function getStatsPath(): string {
const homeDir = Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || ".";
return join(homeDir, ".morse-game", "stats.json");
}
/**
* Load stats from disk
*/
export async function loadStats(): Promise<GameStats> {
try {
const statsPath = getStatsPath();
const content = await Deno.readTextFile(statsPath);
return JSON.parse(content);
} catch {
return { ...DEFAULT_STATS };
}
}
/**
* Save stats to disk
*/
export async function saveStats(stats: GameStats): Promise<void> {
const statsPath = getStatsPath();
const dir = join(Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || ".", ".morse-game");
await ensureDir(dir);
await Deno.writeTextFile(statsPath, JSON.stringify(stats, null, 2));
}
/**
* Update stats with a new game result
*/
export async function updateStats(result: GameResult): Promise<GameStats> {
const stats = await loadStats();
// Update overall stats
stats.totalGames++;
stats.totalRounds += result.rounds;
stats.correctAnswers += result.correct;
stats.incorrectAnswers += result.incorrect;
stats.averageTimePerRound =
(stats.averageTimePerRound * (stats.totalRounds - result.rounds) +
result.averageTime * result.rounds) / stats.totalRounds;
stats.bestStreak = Math.max(stats.bestStreak, result.streak);
stats.lastPlayed = new Date().toISOString();
// Update mode-specific stats
const modeStats = stats.modeStats[result.mode];
modeStats.gamesPlayed++;
modeStats.correctAnswers += result.correct;
modeStats.incorrectAnswers += result.incorrect;
const totalAnswers = modeStats.correctAnswers + modeStats.incorrectAnswers;
modeStats.averageAccuracy = totalAnswers > 0
? (modeStats.correctAnswers / totalAnswers) * 100
: 0;
await saveStats(stats);
return stats;
}
/**
* Reset all stats
*/
export async function resetStats(): Promise<void> {
await saveStats({ ...DEFAULT_STATS });
}
/**
* Get formatted accuracy percentage
*/
export function getAccuracy(stats: GameStats): number {
const total = stats.correctAnswers + stats.incorrectAnswers;
return total > 0 ? (stats.correctAnswers / total) * 100 : 0;
}

348
ui.ts Normal file
View File

@@ -0,0 +1,348 @@
// UI utilities and game interface
import { colors } from "@cliffy/ansi/colors";
import { Input } from "@cliffy/prompt/input";
import { Select } from "@cliffy/prompt/select";
import { Confirm } from "@cliffy/prompt/confirm";
import { Table } from "@cliffy/table";
import { textToMorse } from "./morse.ts";
import type { GameStats } from "./stats.ts";
import { getAccuracy } from "./stats.ts";
import type { GameSession, RoundResult, GameMode } from "./game.ts";
import {
createGameSession,
getChallenge,
processRound,
isGameComplete,
getGameSummary,
getDifficultyDescription,
} from "./game.ts";
/**
* Clear the terminal screen
*/
export function clearScreen(): void {
console.clear();
}
/**
* Print a banner/header
*/
export function printBanner(): void {
console.log(colors.bold.cyan(`
╔═══════════════════════════════════════╗
║ MORSE CODE PRACTICE GAME ║
║ .... . .-.. .-.. --- ║
╚═══════════════════════════════════════╝
`));
}
/**
* Show main menu and get user selection
*/
export async function showMainMenu(): Promise<string> {
const action = await Select.prompt({
message: "What would you like to do?",
options: [
{ name: "Start New Game", value: "play" },
{ name: "View Statistics", value: "stats" },
{ name: "Morse Code Reference", value: "reference" },
{ name: "Reset Statistics", value: "reset" },
{ name: "Exit", value: "exit" },
],
});
return action;
}
/**
* Select game mode
*/
export async function selectGameMode(): Promise<GameMode> {
const mode = await Select.prompt<GameMode>({
message: "Select difficulty mode:",
options: [
{
name: `${colors.green("Easy")} - Letters (A-Z)`,
value: "letters" as GameMode
},
{
name: `${colors.yellow("Medium")} - Numbers (0-9)`,
value: "numbers" as GameMode
},
{
name: `${colors.magenta("Hard")} - Words`,
value: "words" as GameMode
},
{
name: `${colors.red("Expert")} - Phrases`,
value: "phrases" as GameMode
},
],
});
return mode;
}
/**
* Configure game settings
*/
export async function configureGame(mode: GameMode): Promise<{ rounds: number; timePerRound: number }> {
const roundsInput = await Input.prompt({
message: "How many rounds? (5-50)",
default: "10",
validate: (value) => {
const num = parseInt(value);
if (isNaN(num) || num < 5 || num > 50) {
return "Please enter a number between 5 and 50";
}
return true;
},
});
const timeInput = await Input.prompt({
message: "Seconds per round? (10-60)",
default: "30",
validate: (value) => {
const num = parseInt(value);
if (isNaN(num) || num < 10 || num > 60) {
return "Please enter a number between 10 and 60";
}
return true;
},
});
return {
rounds: parseInt(roundsInput),
timePerRound: parseInt(timeInput),
};
}
/**
* Play a single round of the game
*/
export async function playRound(
session: GameSession,
roundNumber: number
): Promise<RoundResult> {
const challenge = getChallenge(session.config.mode);
const startTime = Date.now();
clearScreen();
printBanner();
console.log(colors.bold(`\nRound ${roundNumber} of ${session.config.rounds}`));
console.log(colors.gray(`Time limit: ${session.config.timePerRound}s | Current streak: ${session.currentStreak}\n`));
console.log(colors.bold.white(`Translate to Morse code: ${colors.yellow(challenge)}\n`));
console.log(colors.dim("Tip: Use dots (.) and dashes (-), separate letters with spaces"));
console.log(colors.dim(" Use / for word spaces\n"));
// Create a promise that rejects after the time limit
const timeoutPromise = new Promise<string>((_, reject) => {
setTimeout(() => {
reject(new Error("Time's up!"));
}, session.config.timePerRound * 1000);
});
// Get user input with timeout
let userInput = "";
try {
const inputPromise = Input.prompt({
message: "Your answer:",
minLength: 0,
});
userInput = await Promise.race([inputPromise, timeoutPromise]);
} catch (error) {
if (error instanceof Error && error.message === "Time's up!") {
console.log(colors.red("\n⏰ Time's up!"));
userInput = ""; // Empty input for timeout
} else {
throw error;
}
}
const timeSpent = Date.now() - startTime;
const result = processRound(session, challenge, userInput, timeSpent);
// Show result
console.log();
if (result.correct) {
console.log(colors.bold.green("✓ Correct!"));
} else {
console.log(colors.bold.red("✗ Incorrect"));
console.log(colors.gray(`Expected: ${result.expectedMorse}`));
console.log(colors.gray(`You entered: ${result.userInput || "(nothing)"}`));
}
// Wait for user to continue
await new Promise((resolve) => setTimeout(resolve, 2000));
return result;
}
/**
* Show game results
*/
export function showGameResults(session: GameSession): void {
clearScreen();
printBanner();
const summary = getGameSummary(session);
console.log(colors.bold.cyan("\n🎮 GAME COMPLETE!\n"));
const table = new Table()
.header([colors.bold("Metric"), colors.bold("Value")])
.body([
["Total Rounds", summary.totalRounds.toString()],
["Correct", colors.green(summary.correct.toString())],
["Incorrect", colors.red(summary.incorrect.toString())],
["Accuracy", `${summary.accuracy.toFixed(1)}%`],
["Average Time", `${(summary.averageTime / 1000).toFixed(1)}s`],
["Best Streak", colors.yellow(summary.streak.toString())],
])
.border(true)
.padding(1);
table.render();
// Show detailed results
console.log(colors.bold("\n📊 Round Details:\n"));
const detailsTable = new Table()
.header([
colors.bold("#"),
colors.bold("Challenge"),
colors.bold("Expected"),
colors.bold("Your Answer"),
colors.bold("Result"),
colors.bold("Time"),
]);
session.results.forEach((result, index) => {
detailsTable.push([
(index + 1).toString(),
result.challenge,
result.expectedMorse,
result.userInput || "-",
result.correct ? colors.green("✓") : colors.red("✗"),
`${(result.timeSpent / 1000).toFixed(1)}s`,
]);
});
detailsTable.border(true).render();
}
/**
* Show statistics
*/
export function showStats(stats: GameStats): void {
clearScreen();
printBanner();
console.log(colors.bold.cyan("\n📈 YOUR STATISTICS\n"));
if (stats.totalGames === 0) {
console.log(colors.yellow("No games played yet! Start playing to see your stats.\n"));
return;
}
// Overall stats
const overallTable = new Table()
.header([colors.bold("Overall Stats"), colors.bold("Value")])
.body([
["Total Games", stats.totalGames.toString()],
["Total Rounds", stats.totalRounds.toString()],
["Correct Answers", colors.green(stats.correctAnswers.toString())],
["Incorrect Answers", colors.red(stats.incorrectAnswers.toString())],
["Overall Accuracy", `${getAccuracy(stats).toFixed(1)}%`],
["Average Time/Round", `${stats.averageTimePerRound.toFixed(1)}s`],
["Best Streak", colors.yellow(stats.bestStreak.toString())],
["Last Played", stats.lastPlayed ? new Date(stats.lastPlayed).toLocaleDateString() : "Never"],
])
.border(true)
.padding(1);
overallTable.render();
// Mode-specific stats
console.log(colors.bold("\n📊 Stats by Mode:\n"));
const modeTable = new Table()
.header([
colors.bold("Mode"),
colors.bold("Games"),
colors.bold("Correct"),
colors.bold("Incorrect"),
colors.bold("Accuracy"),
]);
Object.entries(stats.modeStats).forEach(([mode, modeStats]) => {
if (modeStats.gamesPlayed > 0) {
modeTable.push([
mode.charAt(0).toUpperCase() + mode.slice(1),
modeStats.gamesPlayed.toString(),
colors.green(modeStats.correctAnswers.toString()),
colors.red(modeStats.incorrectAnswers.toString()),
`${modeStats.averageAccuracy.toFixed(1)}%`,
]);
}
});
modeTable.border(true).render();
console.log();
}
/**
* Show morse code reference
*/
export function showReference(): void {
clearScreen();
printBanner();
console.log(colors.bold.cyan("\n📖 MORSE CODE REFERENCE\n"));
console.log(colors.bold("Letters:"));
const letterTable = new Table();
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
// Display in 3 columns
for (let i = 0; i < letters.length; i += 3) {
const row = [];
for (let j = 0; j < 3 && i + j < letters.length; j++) {
const letter = letters[i + j];
row.push(`${colors.yellow(letter)} = ${textToMorse(letter)}`);
}
letterTable.push(row);
}
letterTable.render();
console.log(colors.bold("\nNumbers:"));
const numberTable = new Table();
const numbers = "0123456789".split("");
for (let i = 0; i < numbers.length; i += 5) {
const row = [];
for (let j = 0; j < 5 && i + j < numbers.length; j++) {
const num = numbers[i + j];
row.push(`${colors.yellow(num)} = ${textToMorse(num)}`);
}
numberTable.push(row);
}
numberTable.render();
console.log(colors.bold("\nSpecial:"));
console.log(`${colors.yellow("Space")} = /`);
console.log(`${colors.yellow(".")} = .-.-.-`);
console.log(`${colors.yellow(",")} = --..--`);
console.log(`${colors.yellow("?")} = ..--..`);
console.log();
}
/**
* Confirm action
*/
export async function confirmAction(message: string): Promise<boolean> {
return await Confirm.prompt({ message, default: false });
}