chore: claude code refactor (7.5/10)

This commit is contained in:
2025-11-21 17:11:22 +01:00
parent ea431c9401
commit 3d5035547d
6 changed files with 143 additions and 44 deletions

View File

@@ -1,7 +1,7 @@
{ {
"tasks": { "tasks": {
"start": "deno run --allow-read --allow-write --allow-env main.ts", "start": "deno run --allow-read --allow-write main.ts",
"dev": "deno run --watch --allow-read --allow-write --allow-env main.ts" "dev": "deno run --watch --allow-read --allow-write main.ts"
}, },
"imports": { "imports": {
"@std/path": "jsr:@std/path@^1.0.0", "@std/path": "jsr:@std/path@^1.0.0",

25
game.ts
View File

@@ -33,6 +33,11 @@ export interface GameSession {
bestStreak: number; bestStreak: number;
} }
// Constants for word filtering
const MIN_WORD_LENGTH = 3;
const MAX_WORD_LENGTH = 8;
const WORD_POOL_SIZE = 500;
// Character sets 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("");
@@ -40,18 +45,32 @@ const PUNCTUATION = ".,?!-/()@".split("");
const ALPHANUMERIC = [...LETTERS, ...NUMBERS]; const ALPHANUMERIC = [...LETTERS, ...NUMBERS];
const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION]; const FULL_SET = [...LETTERS, ...NUMBERS, ...PUNCTUATION];
/**
* Fisher-Yates shuffle algorithm - O(n) complexity
*/
function shuffleArray<T>(array: T[]): T[] {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// Filter words for morse code practice (3-8 letters, common words) // Filter words for morse code practice (3-8 letters, common words)
const ALL_WORDS = wordsArray const ALL_WORDS = wordsArray
.filter((word: string) => { .filter((word: string) => {
const upper = word.toUpperCase(); const upper = word.toUpperCase();
return ( return (
word.length >= 3 && word.length <= 8 && /^[A-Z]+$/.test(upper) // Only letters, no special chars word.length >= MIN_WORD_LENGTH &&
word.length <= MAX_WORD_LENGTH &&
/^[A-Z]+$/.test(upper) // Only letters, no special chars
); );
}) })
.map((word: string) => word.toUpperCase()); .map((word: string) => word.toUpperCase());
// Randomly select a subset for variety // Randomly select a subset for variety using Fisher-Yates
const WORDS = ALL_WORDS.sort(() => Math.random() - 0.5).slice(0, 500); const WORDS = shuffleArray(ALL_WORDS).slice(0, WORD_POOL_SIZE);
/** /**
* Generate a random phrase from word combinations * Generate a random phrase from word combinations

66
main.ts
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env #!/usr/bin/env -S deno run --allow-read --allow-write
import { Command } from "@cliffy/command"; import { Command } from "@cliffy/command";
import { colors } from "@cliffy/ansi/colors"; import { colors } from "@cliffy/ansi/colors";
@@ -104,17 +104,24 @@ class MorseGame {
streak: session.bestStreak, streak: session.bestStreak,
}; };
await updateStats(gameResult); try {
await updateStats(gameResult);
} catch (error) {
console.log(colors.red("\n⚠ Warning: Failed to save statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
// Wait for user to continue // Wait for user to continue
console.log(colors.gray("\nPress Enter to continue...")); console.log(colors.gray("\nPress Enter to continue..."));
await new Promise((resolve) => { await new Promise((resolve) => {
const buf = new Uint8Array(1); const buf = new Uint8Array(1);
Deno.stdin.setRaw(true); Deno.stdin.setRaw(true);
Deno.stdin.read(buf).then(() => { Deno.stdin.read(buf)
Deno.stdin.setRaw(false); .then(() => resolve(undefined))
resolve(undefined); .catch(() => resolve(undefined))
}); .finally(() => Deno.stdin.setRaw(false));
}); });
} }
@@ -130,10 +137,10 @@ class MorseGame {
await new Promise((resolve) => { await new Promise((resolve) => {
const buf = new Uint8Array(1); const buf = new Uint8Array(1);
Deno.stdin.setRaw(true); Deno.stdin.setRaw(true);
Deno.stdin.read(buf).then(() => { Deno.stdin.read(buf)
Deno.stdin.setRaw(false); .then(() => resolve(undefined))
resolve(undefined); .catch(() => resolve(undefined))
}); .finally(() => Deno.stdin.setRaw(false));
}); });
} }
@@ -148,10 +155,10 @@ class MorseGame {
await new Promise((resolve) => { await new Promise((resolve) => {
const buf = new Uint8Array(1); const buf = new Uint8Array(1);
Deno.stdin.setRaw(true); Deno.stdin.setRaw(true);
Deno.stdin.read(buf).then(() => { Deno.stdin.read(buf)
Deno.stdin.setRaw(false); .then(() => resolve(undefined))
resolve(undefined); .catch(() => resolve(undefined))
}); .finally(() => Deno.stdin.setRaw(false));
}); });
} }
@@ -171,8 +178,15 @@ class MorseGame {
); );
if (confirmed) { if (confirmed) {
await resetStats(); try {
console.log(colors.green("\n✓ Statistics have been reset.\n")); await resetStats();
console.log(colors.green("\n✓ Statistics have been reset.\n"));
} catch (error) {
console.log(colors.red("\n⚠ Error: Failed to reset statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
await new Promise((resolve) => setTimeout(resolve, 1500)); await new Promise((resolve) => setTimeout(resolve, 1500));
} }
} }
@@ -224,7 +238,14 @@ class MorseGame {
streak: session.bestStreak, streak: session.bestStreak,
}; };
await updateStats(gameResult); try {
await updateStats(gameResult);
} catch (error) {
console.log(colors.red("\n⚠ Warning: Failed to save statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
} }
/** /**
@@ -284,8 +305,15 @@ await new Command()
); );
if (confirmed) { if (confirmed) {
await resetStats(); try {
console.log(colors.green("\n✓ Statistics have been reset.\n")); await resetStats();
console.log(colors.green("\n✓ Statistics have been reset.\n"));
} catch (error) {
console.log(colors.red("\n⚠ Error: Failed to reset statistics"));
if (error instanceof Error) {
console.log(colors.gray(error.message));
}
}
} else { } else {
console.log(colors.yellow("\nCancelled.\n")); console.log(colors.yellow("\nCancelled.\n"));
} }

View File

@@ -54,24 +54,51 @@ export const MORSE_TO_TEXT: Record<string, string> = Object.fromEntries(
Object.entries(MORSE_CODE).map(([key, value]) => [value, key]) Object.entries(MORSE_CODE).map(([key, value]) => [value, key])
); );
/**
* Check if a character is supported in Morse code
*/
export function isCharacterSupported(char: string): boolean {
return char.toUpperCase() in MORSE_CODE;
}
/**
* Validate if Morse code string is properly formatted
*/
export function isValidMorse(input: string): boolean {
// Valid morse contains only dots, dashes, spaces, and forward slashes
return /^[.\-\s/]*$/.test(input);
}
/** /**
* Convert text to morse code * Convert text to morse code
* Unsupported characters are silently skipped
*/ */
export function textToMorse(text: string): string { export function textToMorse(text: string): string {
return text return text
.toUpperCase() .toUpperCase()
.split('') .split('')
.map(char => MORSE_CODE[char] || char) .map(char => MORSE_CODE[char])
.filter(code => code !== undefined)
.join(' '); .join(' ');
} }
/** /**
* Convert morse code to text * Convert morse code to text
* Invalid morse codes are replaced with '?'
*/ */
export function morseToText(morse: string): string { export function morseToText(morse: string): string {
if (!isValidMorse(morse)) {
return "";
}
return morse return morse
.split(' ') .split(' ')
.map(code => MORSE_TO_TEXT[code] || code) .map(code => {
// Handle empty strings (multiple spaces)
if (code === '') return '';
// Return the decoded character or '?' for unknown codes
return MORSE_TO_TEXT[code] || '?';
})
.join(''); .join('');
} }

View File

@@ -98,11 +98,17 @@ export async function loadStats(): Promise<GameStats> {
* Save stats to JSON file * Save stats to JSON file
*/ */
export async function saveStats(stats: GameStats): Promise<void> { export async function saveStats(stats: GameStats): Promise<void> {
const statsPath = getStatsPath(); try {
const dataDir = join(Deno.cwd(), "data"); const statsPath = getStatsPath();
const dataDir = join(Deno.cwd(), "data");
await ensureDir(dataDir); await ensureDir(dataDir);
await Deno.writeTextFile(statsPath, JSON.stringify(stats, null, 2)); await Deno.writeTextFile(statsPath, JSON.stringify(stats, null, 2));
} catch (error) {
throw new Error(
`Failed to save statistics: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
} }
/** /**

47
ui.ts
View File

@@ -1,6 +1,13 @@
// UI utilities and game interface // UI utilities and game interface
import { colors } from "@cliffy/ansi/colors"; import { colors } from "@cliffy/ansi/colors";
// UI Constants
const WAIT_TIMES = {
DEFAULT: 2000, // 2 seconds for default modes
WORDS: 6000, // 6 seconds for words mode
PHRASES: 8000, // 8 seconds for phrases mode
} as const;
import { Confirm } from "@cliffy/prompt/confirm"; import { Confirm } from "@cliffy/prompt/confirm";
import { Input } from "@cliffy/prompt/input"; import { Input } from "@cliffy/prompt/input";
import { Select } from "@cliffy/prompt/select"; import { Select } from "@cliffy/prompt/select";
@@ -100,10 +107,10 @@ export async function showTranslator(): Promise<void> {
await new Promise((resolve) => { await new Promise((resolve) => {
const buf = new Uint8Array(1); const buf = new Uint8Array(1);
Deno.stdin.setRaw(true); Deno.stdin.setRaw(true);
Deno.stdin.read(buf).then(() => { Deno.stdin.read(buf)
Deno.stdin.setRaw(false); .then(() => resolve(undefined))
resolve(undefined); .catch(() => resolve(undefined))
}); .finally(() => Deno.stdin.setRaw(false));
}); });
} }
@@ -248,23 +255,35 @@ export async function playRound(
); );
console.log(colors.dim(" Use / for word spaces\n")); 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!"));
}, timeLimit * 1000);
});
// Get user input with timeout // Get user input with timeout
let userInput = ""; let userInput = "";
let timeoutId: number | undefined;
try { try {
// Create a promise that rejects after the time limit
const timeoutPromise = new Promise<string>((_, reject) => {
timeoutId = setTimeout(() => {
reject(new Error("Time's up!"));
}, timeLimit * 1000);
});
const inputPromise = Input.prompt({ const inputPromise = Input.prompt({
message: "Your answer:", message: "Your answer:",
minLength: 0, minLength: 0,
}); });
userInput = await Promise.race([inputPromise, timeoutPromise]); userInput = await Promise.race([inputPromise, timeoutPromise]);
// Clear timeout if input was received first
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
} catch (error) { } catch (error) {
// Clear timeout on error
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
if (error instanceof Error && error.message === "Time's up!") { if (error instanceof Error && error.message === "Time's up!") {
console.log(colors.red("\n⏰ Time's up!")); console.log(colors.red("\n⏰ Time's up!"));
userInput = ""; // Empty input for timeout userInput = ""; // Empty input for timeout
@@ -299,11 +318,11 @@ 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: number = WAIT_TIMES.DEFAULT;
if (session.config.mode === "words") { if (session.config.mode === "words") {
waitTime = 6000; // 6 seconds for words waitTime = WAIT_TIMES.WORDS;
} else if (session.config.mode === "phrases") { } else if (session.config.mode === "phrases") {
waitTime = 8000; // 8 seconds for phrases waitTime = WAIT_TIMES.PHRASES;
} }
// Wait for user to continue // Wait for user to continue