first commit
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal 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
169
README.md
Normal 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
18
deno.json
Normal 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
158
game.ts
Normal 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
276
main.ts
Normal 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
95
morse.ts
Normal 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
126
stats.ts
Normal 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
348
ui.ts
Normal 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 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user