From 19f0ccb1268adfabbc773e3025ab854ce8d71fd1 Mon Sep 17 00:00:00 2001 From: Alberto Rigamonti Date: Fri, 21 Nov 2025 16:41:16 +0100 Subject: [PATCH] feat: added features, refactored code, updated readme --- README.md | 20 +++++++++++----- deno.json | 3 ++- game.ts | 70 ++++++++++++++++++++++++------------------------------- ui.ts | 53 +++++++++++++++++++++++++++++++---------- 4 files changed, 87 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index b22af0b..24cda80 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A terminal-based morse code practice game built with Deno. Improve your morse co - 🟢 Easy: Single letters (A-Z) - 🟔 Medium: Letters + Numbers (A-Z, 0-9) - 🟣 Hard: Letters + Numbers + Punctuation (.,?!-/()@) - - šŸ”µ Challenge: Common words - - šŸ”“ Expert: Short phrases + - šŸ”µ Challenge: Common words (from 275k+ English word dictionary) + - šŸ”“ Expert: Random phrases (2-4 words generated from dictionary) - **Interactive TUI Interface** - 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) - Immediate feedback on answers - Progress tracking with streak counter + - Detailed round-by-round results with translations - **Statistics Tracking** - 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 - Historical game data - Average time per round + - Average time per character (for words/phrases) - Stats saved locally in project folder - **Smart Feedback** - Immediate feedback on answers - 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** - Interactive menu mode (default) @@ -104,17 +107,21 @@ deno task start reset - Letters only (A-Z) - Alphanumeric (A-Z, 0-9) - 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) 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: - `.` (dot) for short signals - `-` (dash) for long signals - Space to separate letters - `/` to separate words 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 @@ -172,6 +179,7 @@ morse-game/ - [@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 +- [an-array-of-english-words](https://www.npmjs.com/package/an-array-of-english-words) - English word dictionary (275k+ words) ## License diff --git a/deno.json b/deno.json index 3f2407e..9fcf2d4 100644 --- a/deno.json +++ b/deno.json @@ -9,7 +9,8 @@ "@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" + "@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": { "lib": ["deno.window"], diff --git a/game.ts b/game.ts index be8b7bd..779fb20 100644 --- a/game.ts +++ b/game.ts @@ -1,6 +1,7 @@ // Game logic for morse code practice import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts"; +import wordsArray from "an-array-of-english-words" with { type: "json" }; export type GameMode = | "letters" @@ -38,45 +39,34 @@ const NUMBERS = "0123456789".split(""); const PUNCTUATION = ".,?!-/()@".split(""); const ALPHANUMERIC = [...LETTERS, ...NUMBERS]; const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION]; -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", -]; + +// Filter words for morse code practice (3-8 letters, common words) +const ALL_WORDS = wordsArray + .filter((word: string) => { + const upper = word.toUpperCase(); + return ( + word.length >= 3 && word.length <= 8 && /^[A-Z]+$/.test(upper) // Only letters, no special chars + ); + }) + .map((word: string) => word.toUpperCase()); + +// Randomly select a subset for variety +const WORDS = ALL_WORDS.sort(() => Math.random() - 0.5).slice(0, 500); + +/** + * Generate a random phrase from word combinations + */ +function generatePhrase(): string { + const phraseLength = Math.floor(Math.random() * 3) + 2; // 2-4 words + const selectedWords: string[] = []; + + for (let i = 0; i < phraseLength; i++) { + const word = WORDS[Math.floor(Math.random() * WORDS.length)]; + selectedWords.push(word); + } + + return selectedWords.join(" "); +} /** * Get a random challenge based on game mode @@ -92,7 +82,7 @@ export function getChallenge(mode: GameMode): string { case "words": return WORDS[Math.floor(Math.random() * WORDS.length)]; case "phrases": - return PHRASES[Math.floor(Math.random() * PHRASES.length)]; + return generatePhrase(); } } diff --git a/ui.ts b/ui.ts index 41de8d4..0c58f43 100644 --- a/ui.ts +++ b/ui.ts @@ -72,11 +72,11 @@ export async function selectGameMode(): Promise { value: "full", }, { - name: `${colors.cyan("Challenge")} - Words`, + name: `${colors.cyan("Challenge")} - Words (3-8 letters 🄵)`, value: "words", }, { - name: `${colors.red("Expert")} - Phrases`, + name: `${colors.red("Expert")} - Phrases (2-4 words 🤯)`, value: "phrases", }, ], @@ -244,9 +244,9 @@ export async function playRound( // Longer wait time for words and phrases let waitTime = 2000; // Default: 2 seconds if (session.config.mode === "words") { - waitTime = 4000; // 4 seconds for words + waitTime = 6000; // 6 seconds for words } else if (session.config.mode === "phrases") { - waitTime = 6000; // 6 seconds for phrases + waitTime = 8000; // 8 seconds for phrases } // Wait for user to continue @@ -266,16 +266,30 @@ export function showGameResults(session: GameSession): void { 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() .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())], - ]) + .body(tableBody) .border(true) .padding(1); @@ -289,16 +303,31 @@ export function showGameResults(session: GameSession): void { colors.bold("Challenge"), colors.bold("Expected"), colors.bold("Your Answer"), + colors.bold("Translates To"), colors.bold("Result"), colors.bold("Time"), ]); 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([ (index + 1).toString(), result.challenge, result.expectedMorse, result.userInput || "-", + translation, result.correct ? colors.green("āœ“") : colors.red("āœ—"), `${(result.timeSpent / 1000).toFixed(1)}s`, ]);