From b446190abe7f22043aa805b88d35c011f5339448 Mon Sep 17 00:00:00 2001 From: Alberto Rigamonti Date: Fri, 21 Nov 2025 16:15:33 +0100 Subject: [PATCH] feat: added new features, refactored code --- game.ts | 105 ++++++++++++++++++++++++++++++++++++------------------- main.ts | 10 +++--- stats.ts | 13 +++++-- ui.ts | 83 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 155 insertions(+), 56 deletions(-) diff --git a/game.ts b/game.ts index b1c2e58..be8b7bd 100644 --- a/game.ts +++ b/game.ts @@ -2,12 +2,18 @@ import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts"; -export type GameMode = 'letters' | 'numbers' | 'words' | 'phrases'; +export type GameMode = + | "letters" + | "alphanumeric" + | "full" + | "words" + | "phrases"; export interface GameConfig { mode: GameMode; rounds: number; timePerRound: number; // seconds + dynamicTime?: boolean; // For words/phrases: multiply time by character count } export interface RoundResult { @@ -26,26 +32,50 @@ export interface GameSession { bestStreak: number; } -// Word banks for different difficulty levels -const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); -const NUMBERS = '0123456789'.split(''); +// Character sets for different difficulty levels +const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); +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', + "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', + "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", ]; /** @@ -53,13 +83,15 @@ const PHRASES = [ */ export function getChallenge(mode: GameMode): string { switch (mode) { - case 'letters': + case "letters": return LETTERS[Math.floor(Math.random() * LETTERS.length)]; - case 'numbers': - return NUMBERS[Math.floor(Math.random() * NUMBERS.length)]; - case 'words': + case "alphanumeric": + return ALPHANUMERIC[Math.floor(Math.random() * ALPHANUMERIC.length)]; + case "full": + return FULL_SET[Math.floor(Math.random() * FULL_SET.length)]; + case "words": return WORDS[Math.floor(Math.random() * WORDS.length)]; - case 'phrases': + case "phrases": return PHRASES[Math.floor(Math.random() * PHRASES.length)]; } } @@ -124,12 +156,13 @@ export function isGameComplete(session: GameSession): boolean { */ export function getGameSummary(session: GameSession) { const totalRounds = session.results.length; - const correct = session.results.filter(r => r.correct).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; + const averageTime = + totalRounds > 0 + ? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds + : 0; return { totalRounds, @@ -146,13 +179,15 @@ export function getGameSummary(session: GameSession) { */ 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'; + case "letters": + return "Single letters A-Z"; + case "alphanumeric": + return "Letters A-Z and numbers 0-9"; + case "full": + return "Letters, numbers, and punctuation"; + case "words": + return "Common 4-6 letter words"; + case "phrases": + return "Short phrases"; } } diff --git a/main.ts b/main.ts index 4cf92f8..76c866f 100644 --- a/main.ts +++ b/main.ts @@ -15,6 +15,7 @@ import { 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"; @@ -65,6 +66,7 @@ class MorseGame { mode, rounds: config.rounds, timePerRound: config.timePerRound, + dynamicTime: config.dynamicTime, }); // Play all rounds @@ -168,7 +170,7 @@ class MorseGame { * Quick play with CLI arguments */ async quickPlay(mode: string, rounds: number, time: number): Promise { - const validModes = ["letters", "numbers", "words", "phrases"]; + const validModes = ["letters", "alphanumeric", "full", "words", "phrases"]; if (!validModes.includes(mode)) { console.error(colors.red(`Invalid mode: ${mode}`)); console.log(`Valid modes: ${validModes.join(", ")}`); @@ -179,7 +181,7 @@ class MorseGame { printBanner(); const session = createGameSession({ - mode: mode as any, + mode: mode as GameMode, rounds, timePerRound: time, }); @@ -203,7 +205,7 @@ class MorseGame { ); const gameResult: GameResult = { - mode: mode as any, + mode: mode as GameMode, rounds: session.results.length, correct: summary.correct, incorrect: summary.incorrect, @@ -250,7 +252,7 @@ await new Command() .command("play", "Start a quick game with specified settings") .option( "-m, --mode ", - "Game mode (letters, numbers, words, phrases)", + "Game mode (letters, alphanumeric, full, words, phrases)", { default: "letters", } diff --git a/stats.ts b/stats.ts index 88945a9..3d32f32 100644 --- a/stats.ts +++ b/stats.ts @@ -12,7 +12,8 @@ export interface GameStats { bestStreak: number; modeStats: { letters: ModeStats; - numbers: ModeStats; + alphanumeric: ModeStats; + full: ModeStats; words: ModeStats; phrases: ModeStats; }; @@ -27,7 +28,7 @@ export interface ModeStats { } export interface GameResult { - mode: "letters" | "numbers" | "words" | "phrases"; + mode: "letters" | "alphanumeric" | "full" | "words" | "phrases"; rounds: number; correct: number; incorrect: number; @@ -49,7 +50,13 @@ const DEFAULT_STATS: GameStats = { incorrectAnswers: 0, averageAccuracy: 0, }, - numbers: { + alphanumeric: { + gamesPlayed: 0, + correctAnswers: 0, + incorrectAnswers: 0, + averageAccuracy: 0, + }, + full: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, diff --git a/ui.ts b/ui.ts index 0e2096b..41de8d4 100644 --- a/ui.ts +++ b/ui.ts @@ -58,19 +58,25 @@ export async function selectGameMode(): Promise { message: "Select difficulty mode:", options: [ { - name: `${colors.green("Easy")} - Letters (A-Z)`, + name: `${colors.green("Easy")} - Letters (A-Z)`, value: "letters", }, { - name: `${colors.yellow("Medium")} - Numbers (0-9)`, - value: "numbers", + name: `${colors.yellow("Medium")} - Letters + Numbers (A-Z, 0-9)`, + value: "alphanumeric", }, { - name: `${colors.magenta("Hard")} - Words`, + name: `${colors.magenta( + "Hard" + )} - Letters + Numbers + Punctuation (🔥) `, + value: "full", + }, + { + name: `${colors.cyan("Challenge")} - Words`, value: "words", }, { - name: `${colors.red("Expert")} - Phrases`, + name: `${colors.red("Expert")} - Phrases`, value: "phrases", }, ], @@ -83,8 +89,26 @@ export async function selectGameMode(): Promise { * Configure game settings */ export async function configureGame( - _mode: GameMode -): Promise<{ rounds: number; timePerRound: number }> { + mode: GameMode +): Promise<{ rounds: number; timePerRound: number; dynamicTime: boolean }> { + // Set default time based on mode + let defaultTime: string; + switch (mode) { + case "letters": + defaultTime = "3"; + break; + case "alphanumeric": + defaultTime = "5"; + break; + case "full": + defaultTime = "7"; + break; + case "words": + case "phrases": + defaultTime = "5"; + break; + } + const roundsInput = await Input.prompt({ message: "How many rounds? (5-50)", default: "10", @@ -97,13 +121,24 @@ export async function configureGame( }, }); + // Ask about dynamic time for words/phrases + let dynamicTime = false; + if (mode === "words" || mode === "phrases") { + dynamicTime = await Confirm.prompt({ + message: "Use dynamic time? (time multiplied by character count)", + default: true, + }); + } + const timeInput = await Input.prompt({ - message: "Seconds per round? (10-60)", - default: "30", + message: dynamicTime + ? "Base seconds per character? (3-60)" + : "Seconds per round? (3-60)", + default: defaultTime, validate: (value) => { const num = parseInt(value); - if (isNaN(num) || num < 10 || num > 60) { - return "Please enter a number between 10 and 60"; + if (isNaN(num) || num < 3 || num > 60) { + return "Please enter a number between 3 and 60"; } return true; }, @@ -112,6 +147,7 @@ export async function configureGame( return { rounds: parseInt(roundsInput), timePerRound: parseInt(timeInput), + dynamicTime, }; } @@ -123,6 +159,17 @@ export async function playRound( roundNumber: number ): Promise { const challenge = getChallenge(session.config.mode); + + // Calculate dynamic time limit for words and phrases if enabled + let timeLimit = session.config.timePerRound; + if ( + session.config.dynamicTime && + (session.config.mode === "words" || session.config.mode === "phrases") + ) { + // Use configured time as multiplier per character + timeLimit = challenge.length * session.config.timePerRound; + } + const startTime = Date.now(); clearScreen(); @@ -132,7 +179,7 @@ export async function playRound( ); console.log( colors.gray( - `Time limit: ${session.config.timePerRound}s | Current streak: ${session.currentStreak}\n` + `Time limit: ${timeLimit}s | Current streak: ${session.currentStreak}\n` ) ); @@ -148,7 +195,7 @@ export async function playRound( const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error("Time's up!")); - }, session.config.timePerRound * 1000); + }, timeLimit * 1000); }); // Get user input with timeout @@ -194,8 +241,16 @@ 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 + } else if (session.config.mode === "phrases") { + waitTime = 6000; // 6 seconds for phrases + } + // Wait for user to continue - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, waitTime)); return result; }