feat: added new features, refactored code

This commit is contained in:
2025-11-21 16:15:33 +01:00
parent 3b0c00ab5f
commit b446190abe
4 changed files with 155 additions and 56 deletions

101
game.ts
View File

@@ -2,12 +2,18 @@
import { textToMorse, compareMorse, normalizeMorse } from "./morse.ts"; 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 { export interface GameConfig {
mode: GameMode; mode: GameMode;
rounds: number; rounds: number;
timePerRound: number; // seconds timePerRound: number; // seconds
dynamicTime?: boolean; // For words/phrases: multiply time by character count
} }
export interface RoundResult { export interface RoundResult {
@@ -26,26 +32,50 @@ export interface GameSession {
bestStreak: number; bestStreak: number;
} }
// Word banks for different difficulty levels // Character sets for different difficulty levels
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const NUMBERS = '0123456789'.split(''); const NUMBERS = "0123456789".split("");
const PUNCTUATION = ".,?!-/()@".split("");
const ALPHANUMERIC = [...LETTERS, ...NUMBERS];
const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION];
const WORDS = [ const WORDS = [
'HELLO', 'WORLD', 'CODE', 'TIME', 'GAME', 'TEST', 'PLAY', "HELLO",
'FAST', 'SLOW', 'HELP', 'GOOD', 'BEST', 'NICE', 'COOL', "WORLD",
'MORSE', 'SIGNAL', 'RADIO', 'SEND', 'MESSAGE', 'QUICK', "CODE",
'LEARN', 'PRACTICE', 'SKILL', 'MASTER', 'EXPERT', "TIME",
"GAME",
"TEST",
"PLAY",
"FAST",
"SLOW",
"HELP",
"GOOD",
"BEST",
"NICE",
"COOL",
"MORSE",
"SIGNAL",
"RADIO",
"SEND",
"MESSAGE",
"QUICK",
"LEARN",
"PRACTICE",
"SKILL",
"MASTER",
"EXPERT",
]; ];
const PHRASES = [ const PHRASES = [
'HELLO WORLD', "HELLO WORLD",
'GOOD MORNING', "GOOD MORNING",
'HOW ARE YOU', "HOW ARE YOU",
'THANK YOU', "THANK YOU",
'SEE YOU SOON', "SEE YOU SOON",
'HAVE A NICE DAY', "HAVE A NICE DAY",
'MORSE CODE', "MORSE CODE",
'QUICK BROWN FOX', "QUICK BROWN FOX",
'THE END', "THE END",
'WELL DONE', "WELL DONE",
]; ];
/** /**
@@ -53,13 +83,15 @@ const PHRASES = [
*/ */
export function getChallenge(mode: GameMode): string { export function getChallenge(mode: GameMode): string {
switch (mode) { switch (mode) {
case 'letters': case "letters":
return LETTERS[Math.floor(Math.random() * LETTERS.length)]; return LETTERS[Math.floor(Math.random() * LETTERS.length)];
case 'numbers': case "alphanumeric":
return NUMBERS[Math.floor(Math.random() * NUMBERS.length)]; return ALPHANUMERIC[Math.floor(Math.random() * ALPHANUMERIC.length)];
case 'words': case "full":
return FULL_SET[Math.floor(Math.random() * FULL_SET.length)];
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 PHRASES[Math.floor(Math.random() * PHRASES.length)];
} }
} }
@@ -124,10 +156,11 @@ export function isGameComplete(session: GameSession): boolean {
*/ */
export function getGameSummary(session: GameSession) { export function getGameSummary(session: GameSession) {
const totalRounds = session.results.length; 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 incorrect = totalRounds - correct;
const accuracy = totalRounds > 0 ? (correct / totalRounds) * 100 : 0; const accuracy = totalRounds > 0 ? (correct / totalRounds) * 100 : 0;
const averageTime = totalRounds > 0 const averageTime =
totalRounds > 0
? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds ? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds
: 0; : 0;
@@ -146,13 +179,15 @@ export function getGameSummary(session: GameSession) {
*/ */
export function getDifficultyDescription(mode: GameMode): string { export function getDifficultyDescription(mode: GameMode): string {
switch (mode) { switch (mode) {
case 'letters': case "letters":
return 'Single letters A-Z'; return "Single letters A-Z";
case 'numbers': case "alphanumeric":
return 'Single digits 0-9'; return "Letters A-Z and numbers 0-9";
case 'words': case "full":
return 'Common 4-6 letter words'; return "Letters, numbers, and punctuation";
case 'phrases': case "words":
return 'Short phrases'; return "Common 4-6 letter words";
case "phrases":
return "Short phrases";
} }
} }

10
main.ts
View File

@@ -15,6 +15,7 @@ import {
confirmAction, confirmAction,
} from "./ui.ts"; } from "./ui.ts";
import { createGameSession, isGameComplete } from "./game.ts"; import { createGameSession, isGameComplete } from "./game.ts";
import type { GameMode } from "./game.ts";
import { loadStats, updateStats, resetStats, getAccuracy } from "./stats.ts"; import { loadStats, updateStats, resetStats, getAccuracy } from "./stats.ts";
import type { GameResult } from "./stats.ts"; import type { GameResult } from "./stats.ts";
@@ -65,6 +66,7 @@ class MorseGame {
mode, mode,
rounds: config.rounds, rounds: config.rounds,
timePerRound: config.timePerRound, timePerRound: config.timePerRound,
dynamicTime: config.dynamicTime,
}); });
// Play all rounds // Play all rounds
@@ -168,7 +170,7 @@ class MorseGame {
* Quick play with CLI arguments * Quick play with CLI arguments
*/ */
async quickPlay(mode: string, rounds: number, time: number): Promise<void> { async quickPlay(mode: string, rounds: number, time: number): Promise<void> {
const validModes = ["letters", "numbers", "words", "phrases"]; const validModes = ["letters", "alphanumeric", "full", "words", "phrases"];
if (!validModes.includes(mode)) { if (!validModes.includes(mode)) {
console.error(colors.red(`Invalid mode: ${mode}`)); console.error(colors.red(`Invalid mode: ${mode}`));
console.log(`Valid modes: ${validModes.join(", ")}`); console.log(`Valid modes: ${validModes.join(", ")}`);
@@ -179,7 +181,7 @@ class MorseGame {
printBanner(); printBanner();
const session = createGameSession({ const session = createGameSession({
mode: mode as any, mode: mode as GameMode,
rounds, rounds,
timePerRound: time, timePerRound: time,
}); });
@@ -203,7 +205,7 @@ class MorseGame {
); );
const gameResult: GameResult = { const gameResult: GameResult = {
mode: mode as any, mode: mode as GameMode,
rounds: session.results.length, rounds: session.results.length,
correct: summary.correct, correct: summary.correct,
incorrect: summary.incorrect, incorrect: summary.incorrect,
@@ -250,7 +252,7 @@ await new Command()
.command("play", "Start a quick game with specified settings") .command("play", "Start a quick game with specified settings")
.option( .option(
"-m, --mode <mode:string>", "-m, --mode <mode:string>",
"Game mode (letters, numbers, words, phrases)", "Game mode (letters, alphanumeric, full, words, phrases)",
{ {
default: "letters", default: "letters",
} }

View File

@@ -12,7 +12,8 @@ export interface GameStats {
bestStreak: number; bestStreak: number;
modeStats: { modeStats: {
letters: ModeStats; letters: ModeStats;
numbers: ModeStats; alphanumeric: ModeStats;
full: ModeStats;
words: ModeStats; words: ModeStats;
phrases: ModeStats; phrases: ModeStats;
}; };
@@ -27,7 +28,7 @@ export interface ModeStats {
} }
export interface GameResult { export interface GameResult {
mode: "letters" | "numbers" | "words" | "phrases"; mode: "letters" | "alphanumeric" | "full" | "words" | "phrases";
rounds: number; rounds: number;
correct: number; correct: number;
incorrect: number; incorrect: number;
@@ -49,7 +50,13 @@ const DEFAULT_STATS: GameStats = {
incorrectAnswers: 0, incorrectAnswers: 0,
averageAccuracy: 0, averageAccuracy: 0,
}, },
numbers: { alphanumeric: {
gamesPlayed: 0,
correctAnswers: 0,
incorrectAnswers: 0,
averageAccuracy: 0,
},
full: {
gamesPlayed: 0, gamesPlayed: 0,
correctAnswers: 0, correctAnswers: 0,
incorrectAnswers: 0, incorrectAnswers: 0,

79
ui.ts
View File

@@ -62,11 +62,17 @@ export async function selectGameMode(): Promise<GameMode> {
value: "letters", value: "letters",
}, },
{ {
name: `${colors.yellow("Medium")} - Numbers (0-9)`, name: `${colors.yellow("Medium")} - Letters + Numbers (A-Z, 0-9)`,
value: "numbers", value: "alphanumeric",
}, },
{ {
name: `${colors.magenta("Hard")} - Words`, name: `${colors.magenta(
"Hard"
)} - Letters + Numbers + Punctuation (🔥) `,
value: "full",
},
{
name: `${colors.cyan("Challenge")} - Words`,
value: "words", value: "words",
}, },
{ {
@@ -83,8 +89,26 @@ export async function selectGameMode(): Promise<GameMode> {
* Configure game settings * Configure game settings
*/ */
export async function configureGame( export async function configureGame(
_mode: GameMode mode: GameMode
): Promise<{ rounds: number; timePerRound: number }> { ): 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({ const roundsInput = await Input.prompt({
message: "How many rounds? (5-50)", message: "How many rounds? (5-50)",
default: "10", 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({ const timeInput = await Input.prompt({
message: "Seconds per round? (10-60)", message: dynamicTime
default: "30", ? "Base seconds per character? (3-60)"
: "Seconds per round? (3-60)",
default: defaultTime,
validate: (value) => { validate: (value) => {
const num = parseInt(value); const num = parseInt(value);
if (isNaN(num) || num < 10 || num > 60) { if (isNaN(num) || num < 3 || num > 60) {
return "Please enter a number between 10 and 60"; return "Please enter a number between 3 and 60";
} }
return true; return true;
}, },
@@ -112,6 +147,7 @@ export async function configureGame(
return { return {
rounds: parseInt(roundsInput), rounds: parseInt(roundsInput),
timePerRound: parseInt(timeInput), timePerRound: parseInt(timeInput),
dynamicTime,
}; };
} }
@@ -123,6 +159,17 @@ export async function playRound(
roundNumber: number roundNumber: number
): Promise<RoundResult> { ): Promise<RoundResult> {
const challenge = getChallenge(session.config.mode); 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(); const startTime = Date.now();
clearScreen(); clearScreen();
@@ -132,7 +179,7 @@ export async function playRound(
); );
console.log( console.log(
colors.gray( 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<string>((_, reject) => { const timeoutPromise = new Promise<string>((_, reject) => {
setTimeout(() => { setTimeout(() => {
reject(new Error("Time's up!")); reject(new Error("Time's up!"));
}, session.config.timePerRound * 1000); }, timeLimit * 1000);
}); });
// Get user input with timeout // 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 // Wait for user to continue
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, waitTime));
return result; return result;
} }