feat: added features, refactored code, updated readme

This commit is contained in:
2025-11-21 16:41:16 +01:00
parent 64119550f0
commit 19f0ccb126
4 changed files with 87 additions and 59 deletions

View File

@@ -8,8 +8,8 @@ A terminal-based morse code practice game built with Deno. Improve your morse co
- 🟢 Easy: Single letters (A-Z) - 🟢 Easy: Single letters (A-Z)
- 🟡 Medium: Letters + Numbers (A-Z, 0-9) - 🟡 Medium: Letters + Numbers (A-Z, 0-9)
- 🟣 Hard: Letters + Numbers + Punctuation (.,?!-/()@) - 🟣 Hard: Letters + Numbers + Punctuation (.,?!-/()@)
- 🔵 Challenge: Common words - 🔵 Challenge: Common words (from 275k+ English word dictionary)
- 🔴 Expert: Short phrases - 🔴 Expert: Random phrases (2-4 words generated from dictionary)
- **Interactive TUI Interface** - **Interactive TUI Interface**
- Beautiful terminal UI with colors and tables - Beautiful terminal UI with colors and tables
@@ -17,6 +17,7 @@ A terminal-based morse code practice game built with Deno. Improve your morse co
- Dynamic time option for words/phrases (time scales with length) - Dynamic time option for words/phrases (time scales with length)
- Immediate feedback on answers - Immediate feedback on answers
- Progress tracking with streak counter - Progress tracking with streak counter
- Detailed round-by-round results with translations
- **Statistics Tracking** - **Statistics Tracking**
- Overall accuracy and performance metrics - Overall accuracy and performance metrics
@@ -24,12 +25,14 @@ A terminal-based morse code practice game built with Deno. Improve your morse co
- Best streak tracking - Best streak tracking
- Historical game data - Historical game data
- Average time per round - Average time per round
- Average time per character (for words/phrases)
- Stats saved locally in project folder - Stats saved locally in project folder
- **Smart Feedback** - **Smart Feedback**
- Immediate feedback on answers - Immediate feedback on answers
- Shows what your morse code translates to when incorrect - Shows what your morse code translates to when incorrect
- Helps you learn from mistakes - Detailed results table with translations for all rounds
- Helps you learn from mistakes and track progress
- **Flexible CLI** - **Flexible CLI**
- Interactive menu mode (default) - Interactive menu mode (default)
@@ -104,17 +107,21 @@ deno task start reset
- Letters only (A-Z) - Letters only (A-Z)
- Alphanumeric (A-Z, 0-9) - Alphanumeric (A-Z, 0-9)
- Full character set (letters, numbers, punctuation) - Full character set (letters, numbers, punctuation)
- Words or phrases - Words (randomly selected from English dictionary, 3-8 letters)
- Phrases (2-4 words randomly combined from dictionary)
2. **Configure your game** - Set the number of rounds (5-50) and time per round (3-60 seconds) 2. **Configure your game** - Set the number of rounds (5-50) and time per round (3-60 seconds)
3. **Enable dynamic time (optional)** - For words/phrases, time can scale based on character count 3. **Enable dynamic time (optional)** - For words/phrases, time can scale based on character count
4. **Translate to morse code** - You'll be shown a challenge 4. **Translate to morse code** - You'll be shown a challenge from real English words
5. **Enter your answer** - Type the morse code using: 5. **Enter your answer** - Type the morse code using:
- `.` (dot) for short signals - `.` (dot) for short signals
- `-` (dash) for long signals - `-` (dash) for long signals
- Space to separate letters - Space to separate letters
- `/` to separate words - `/` to separate words
6. **Get instant feedback** - See if you got it right and what your input translates to if wrong 6. **Get instant feedback** - See if you got it right and what your input translates to if wrong
7. **Review your performance** - After all rounds, view detailed statistics 7. **Review your performance** - After all rounds, view detailed statistics including:
- Overall accuracy and time metrics
- Average time per character (for words/phrases)
- Complete results table showing what each morse input translates to
## Morse Code Basics ## Morse Code Basics
@@ -172,6 +179,7 @@ morse-game/
- [@cliffy/table](https://jsr.io/@cliffy/table) - Table rendering - [@cliffy/table](https://jsr.io/@cliffy/table) - Table rendering
- [@std/path](https://jsr.io/@std/path) - Path utilities - [@std/path](https://jsr.io/@std/path) - Path utilities
- [@std/fs](https://jsr.io/@std/fs) - File system utilities - [@std/fs](https://jsr.io/@std/fs) - File system utilities
- [an-array-of-english-words](https://www.npmjs.com/package/an-array-of-english-words) - English word dictionary (275k+ words)
## License ## License

View File

@@ -9,7 +9,8 @@
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7", "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7",
"@cliffy/prompt": "jsr:@cliffy/prompt@^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/ansi": "jsr:@cliffy/ansi@^1.0.0-rc.7",
"@cliffy/table": "jsr:@cliffy/table@^1.0.0-rc.7" "@cliffy/table": "jsr:@cliffy/table@^1.0.0-rc.7",
"an-array-of-english-words": "npm:an-array-of-english-words@^2.0.0"
}, },
"compilerOptions": { "compilerOptions": {
"lib": ["deno.window"], "lib": ["deno.window"],

70
game.ts
View File

@@ -1,6 +1,7 @@
// Game logic for morse code practice // Game logic for morse code practice
import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts"; import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts";
import wordsArray from "an-array-of-english-words" with { type: "json" };
export type GameMode = export type GameMode =
| "letters" | "letters"
@@ -38,45 +39,34 @@ const NUMBERS = "0123456789".split("");
const PUNCTUATION = ".,?!-/()@".split(""); const PUNCTUATION = ".,?!-/()@".split("");
const ALPHANUMERIC = [...LETTERS, ...NUMBERS]; const ALPHANUMERIC = [...LETTERS, ...NUMBERS];
const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION]; const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION];
const WORDS = [
"HELLO", // Filter words for morse code practice (3-8 letters, common words)
"WORLD", const ALL_WORDS = wordsArray
"CODE", .filter((word: string) => {
"TIME", const upper = word.toUpperCase();
"GAME", return (
"TEST", word.length >= 3 && word.length <= 8 && /^[A-Z]+$/.test(upper) // Only letters, no special chars
"PLAY", );
"FAST", })
"SLOW", .map((word: string) => word.toUpperCase());
"HELP",
"GOOD", // Randomly select a subset for variety
"BEST", const WORDS = ALL_WORDS.sort(() => Math.random() - 0.5).slice(0, 500);
"NICE",
"COOL", /**
"MORSE", * Generate a random phrase from word combinations
"SIGNAL", */
"RADIO", function generatePhrase(): string {
"SEND", const phraseLength = Math.floor(Math.random() * 3) + 2; // 2-4 words
"MESSAGE", const selectedWords: string[] = [];
"QUICK",
"LEARN", for (let i = 0; i < phraseLength; i++) {
"PRACTICE", const word = WORDS[Math.floor(Math.random() * WORDS.length)];
"SKILL", selectedWords.push(word);
"MASTER", }
"EXPERT",
]; return selectedWords.join(" ");
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 * Get a random challenge based on game mode
@@ -92,7 +82,7 @@ export function getChallenge(mode: GameMode): string {
case "words": case "words":
return WORDS[Math.floor(Math.random() * WORDS.length)]; return WORDS[Math.floor(Math.random() * WORDS.length)];
case "phrases": case "phrases":
return PHRASES[Math.floor(Math.random() * PHRASES.length)]; return generatePhrase();
} }
} }

53
ui.ts
View File

@@ -72,11 +72,11 @@ export async function selectGameMode(): Promise<GameMode> {
value: "full", value: "full",
}, },
{ {
name: `${colors.cyan("Challenge")} - Words`, name: `${colors.cyan("Challenge")} - Words (3-8 letters 🥵)`,
value: "words", value: "words",
}, },
{ {
name: `${colors.red("Expert")} - Phrases`, name: `${colors.red("Expert")} - Phrases (2-4 words 🤯)`,
value: "phrases", value: "phrases",
}, },
], ],
@@ -244,9 +244,9 @@ export async function playRound(
// Longer wait time for words and phrases // Longer wait time for words and phrases
let waitTime = 2000; // Default: 2 seconds let waitTime = 2000; // Default: 2 seconds
if (session.config.mode === "words") { if (session.config.mode === "words") {
waitTime = 4000; // 4 seconds for words waitTime = 6000; // 6 seconds for words
} else if (session.config.mode === "phrases") { } else if (session.config.mode === "phrases") {
waitTime = 6000; // 6 seconds for phrases waitTime = 8000; // 8 seconds for phrases
} }
// Wait for user to continue // Wait for user to continue
@@ -266,16 +266,30 @@ export function showGameResults(session: GameSession): void {
console.log(colors.bold.cyan("\n🎮 GAME COMPLETE!\n")); console.log(colors.bold.cyan("\n🎮 GAME COMPLETE!\n"));
const tableBody: string[][] = [
["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`],
];
// Add average time per character for words/phrases modes
if (session.config.mode === "words" || session.config.mode === "phrases") {
const totalChars = session.results.reduce(
(sum, r) => sum + r.challenge.length,
0
);
const totalTime = session.results.reduce((sum, r) => sum + r.timeSpent, 0);
const avgTimePerChar = totalChars > 0 ? totalTime / totalChars / 1000 : 0;
tableBody.push(["Avg Time/Char", `${avgTimePerChar.toFixed(2)}s`]);
}
tableBody.push(["Best Streak", colors.yellow(summary.streak.toString())]);
const table = new Table() const table = new Table()
.header([colors.bold("Metric"), colors.bold("Value")]) .header([colors.bold("Metric"), colors.bold("Value")])
.body([ .body(tableBody)
["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) .border(true)
.padding(1); .padding(1);
@@ -289,16 +303,31 @@ export function showGameResults(session: GameSession): void {
colors.bold("Challenge"), colors.bold("Challenge"),
colors.bold("Expected"), colors.bold("Expected"),
colors.bold("Your Answer"), colors.bold("Your Answer"),
colors.bold("Translates To"),
colors.bold("Result"), colors.bold("Result"),
colors.bold("Time"), colors.bold("Time"),
]); ]);
session.results.forEach((result, index) => { session.results.forEach((result, index) => {
// Try to decode the user's morse input
let translation = "-";
if (result.userInput && result.userInput.trim()) {
try {
const decoded = morseToText(result.userInput);
if (decoded && decoded !== result.userInput) {
translation = decoded;
}
} catch {
translation = "?";
}
}
detailsTable.push([ detailsTable.push([
(index + 1).toString(), (index + 1).toString(),
result.challenge, result.challenge,
result.expectedMorse, result.expectedMorse,
result.userInput || "-", result.userInput || "-",
translation,
result.correct ? colors.green("✓") : colors.red("✗"), result.correct ? colors.green("✓") : colors.red("✗"),
`${(result.timeSpent / 1000).toFixed(1)}s`, `${(result.timeSpent / 1000).toFixed(1)}s`,
]); ]);