feat: changed stats storage location, refactored code

This commit is contained in:
2025-11-21 15:47:56 +01:00
parent 043812606d
commit 052819a7c7
4 changed files with 115 additions and 64 deletions

2
.gitignore vendored
View File

@@ -13,4 +13,4 @@ deno.lock
Thumbs.db Thumbs.db
# User data # User data
.morse-game/ data/

16
main.ts
View File

@@ -46,7 +46,9 @@ class MorseGame {
await this.resetStats(); await this.resetStats();
break; break;
case "exit": case "exit":
console.log(colors.cyan("\nThanks for playing! 73 (Best regards in morse)\n")); console.log(
colors.cyan("\nThanks for playing! 73 (Best regards in morse)\n")
);
return; return;
} }
} }
@@ -226,7 +228,9 @@ class MorseGame {
console.log(colors.bold.cyan("\n📈 QUICK STATS\n")); console.log(colors.bold.cyan("\n📈 QUICK STATS\n"));
console.log(`Games Played: ${colors.bold(stats.totalGames.toString())}`); console.log(`Games Played: ${colors.bold(stats.totalGames.toString())}`);
console.log(`Total Rounds: ${colors.bold(stats.totalRounds.toString())}`); console.log(`Total Rounds: ${colors.bold(stats.totalRounds.toString())}`);
console.log(`Accuracy: ${colors.bold(getAccuracy(stats).toFixed(1) + "%")}`); console.log(
`Accuracy: ${colors.bold(getAccuracy(stats).toFixed(1) + "%")}`
);
console.log(`Best Streak: ${colors.bold(stats.bestStreak.toString())}`); console.log(`Best Streak: ${colors.bold(stats.bestStreak.toString())}`);
console.log(); console.log();
} }
@@ -244,9 +248,13 @@ await new Command()
await app.runInteractive(); await app.runInteractive();
}) })
.command("play", "Start a quick game with specified settings") .command("play", "Start a quick game with specified settings")
.option("-m, --mode <mode:string>", "Game mode (letters, numbers, words, phrases)", { .option(
"-m, --mode <mode:string>",
"Game mode (letters, numbers, words, phrases)",
{
default: "letters", default: "letters",
}) }
)
.option("-r, --rounds <rounds:number>", "Number of rounds", { default: 10 }) .option("-r, --rounds <rounds:number>", "Number of rounds", { default: 10 })
.option("-t, --time <seconds:number>", "Seconds per round", { default: 30 }) .option("-t, --time <seconds:number>", "Seconds per round", { default: 30 })
.action(async (options) => { .action(async (options) => {

View File

@@ -27,7 +27,7 @@ export interface ModeStats {
} }
export interface GameResult { export interface GameResult {
mode: 'letters' | 'numbers' | 'words' | 'phrases'; mode: "letters" | "numbers" | "words" | "phrases";
rounds: number; rounds: number;
correct: number; correct: number;
incorrect: number; incorrect: number;
@@ -43,20 +43,39 @@ const DEFAULT_STATS: GameStats = {
averageTimePerRound: 0, averageTimePerRound: 0,
bestStreak: 0, bestStreak: 0,
modeStats: { modeStats: {
letters: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 }, letters: {
numbers: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 }, gamesPlayed: 0,
words: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 }, correctAnswers: 0,
phrases: { gamesPlayed: 0, correctAnswers: 0, incorrectAnswers: 0, averageAccuracy: 0 }, incorrectAnswers: 0,
averageAccuracy: 0,
},
numbers: {
gamesPlayed: 0,
correctAnswers: 0,
incorrectAnswers: 0,
averageAccuracy: 0,
},
words: {
gamesPlayed: 0,
correctAnswers: 0,
incorrectAnswers: 0,
averageAccuracy: 0,
},
phrases: {
gamesPlayed: 0,
correctAnswers: 0,
incorrectAnswers: 0,
averageAccuracy: 0,
},
}, },
}; };
function getStatsPath(): string { function getStatsPath(): string {
const homeDir = Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || "."; return join(Deno.cwd(), "data", "stats.json");
return join(homeDir, ".morse-game", "stats.json");
} }
/** /**
* Load stats from disk * Load stats from JSON file
*/ */
export async function loadStats(): Promise<GameStats> { export async function loadStats(): Promise<GameStats> {
try { try {
@@ -69,13 +88,13 @@ export async function loadStats(): Promise<GameStats> {
} }
/** /**
* Save stats to disk * Save stats to JSON file
*/ */
export async function saveStats(stats: GameStats): Promise<void> { export async function saveStats(stats: GameStats): Promise<void> {
const statsPath = getStatsPath(); const statsPath = getStatsPath();
const dir = join(Deno.env.get("HOME") || Deno.env.get("USERPROFILE") || ".", ".morse-game"); const dataDir = join(Deno.cwd(), "data");
await ensureDir(dir); await ensureDir(dataDir);
await Deno.writeTextFile(statsPath, JSON.stringify(stats, null, 2)); await Deno.writeTextFile(statsPath, JSON.stringify(stats, null, 2));
} }
@@ -92,7 +111,8 @@ export async function updateStats(result: GameResult): Promise<GameStats> {
stats.incorrectAnswers += result.incorrect; stats.incorrectAnswers += result.incorrect;
stats.averageTimePerRound = stats.averageTimePerRound =
(stats.averageTimePerRound * (stats.totalRounds - result.rounds) + (stats.averageTimePerRound * (stats.totalRounds - result.rounds) +
result.averageTime * result.rounds) / stats.totalRounds; result.averageTime * result.rounds) /
stats.totalRounds;
stats.bestStreak = Math.max(stats.bestStreak, result.streak); stats.bestStreak = Math.max(stats.bestStreak, result.streak);
stats.lastPlayed = new Date().toISOString(); stats.lastPlayed = new Date().toISOString();
@@ -102,9 +122,8 @@ export async function updateStats(result: GameResult): Promise<GameStats> {
modeStats.correctAnswers += result.correct; modeStats.correctAnswers += result.correct;
modeStats.incorrectAnswers += result.incorrect; modeStats.incorrectAnswers += result.incorrect;
const totalAnswers = modeStats.correctAnswers + modeStats.incorrectAnswers; const totalAnswers = modeStats.correctAnswers + modeStats.incorrectAnswers;
modeStats.averageAccuracy = totalAnswers > 0 modeStats.averageAccuracy =
? (modeStats.correctAnswers / totalAnswers) * 100 totalAnswers > 0 ? (modeStats.correctAnswers / totalAnswers) * 100 : 0;
: 0;
await saveStats(stats); await saveStats(stats);
return stats; return stats;

84
ui.ts
View File

@@ -1,22 +1,15 @@
// UI utilities and game interface // UI utilities and game interface
import { colors } from "@cliffy/ansi/colors"; import { colors } from "@cliffy/ansi/colors";
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";
import { Confirm } from "@cliffy/prompt/confirm";
import { Table } from "@cliffy/table"; import { Table } from "@cliffy/table";
import { textToMorse } from "./morse.ts"; import type { GameMode, GameSession, RoundResult } from "./game.ts";
import { getChallenge, getGameSummary, processRound } from "./game.ts";
import { morseToText, textToMorse } from "./morse.ts";
import type { GameStats } from "./stats.ts"; import type { GameStats } from "./stats.ts";
import { getAccuracy } from "./stats.ts"; import { getAccuracy } from "./stats.ts";
import type { GameSession, RoundResult, GameMode } from "./game.ts";
import {
createGameSession,
getChallenge,
processRound,
isGameComplete,
getGameSummary,
getDifficultyDescription,
} from "./game.ts";
/** /**
* Clear the terminal screen * Clear the terminal screen
@@ -29,12 +22,14 @@ export function clearScreen(): void {
* Print a banner/header * Print a banner/header
*/ */
export function printBanner(): void { export function printBanner(): void {
console.log(colors.bold.cyan(` console.log(
colors.bold.cyan(`
╔═══════════════════════════════════════╗ ╔═══════════════════════════════════════╗
║ MORSE CODE PRACTICE GAME ║ ║ MORSE CODE PRACTICE GAME ║
║ .... . .-.. .-.. --- ║ ║ .... . .-.. .-.. --- ║
╚═══════════════════════════════════════╝ ╚═══════════════════════════════════════╝
`)); `)
);
} }
/** /**
@@ -59,35 +54,37 @@ export async function showMainMenu(): Promise<string> {
* Select game mode * Select game mode
*/ */
export async function selectGameMode(): Promise<GameMode> { export async function selectGameMode(): Promise<GameMode> {
const mode = await Select.prompt<GameMode>({ const mode = await Select.prompt({
message: "Select difficulty mode:", message: "Select difficulty mode:",
options: [ options: [
{ {
name: `${colors.green("Easy")} - Letters (A-Z)`, name: `${colors.green("Easy")} - Letters (A-Z)`,
value: "letters" as GameMode value: "letters",
}, },
{ {
name: `${colors.yellow("Medium")} - Numbers (0-9)`, name: `${colors.yellow("Medium")} - Numbers (0-9)`,
value: "numbers" as GameMode value: "numbers",
}, },
{ {
name: `${colors.magenta("Hard")} - Words`, name: `${colors.magenta("Hard")} - Words`,
value: "words" as GameMode value: "words",
}, },
{ {
name: `${colors.red("Expert")} - Phrases`, name: `${colors.red("Expert")} - Phrases`,
value: "phrases" as GameMode value: "phrases",
}, },
], ],
}); });
return mode; return mode as GameMode;
} }
/** /**
* Configure game settings * Configure game settings
*/ */
export async function configureGame(mode: GameMode): Promise<{ rounds: number; timePerRound: number }> { export async function configureGame(
_mode: GameMode
): Promise<{ rounds: number; timePerRound: number }> {
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",
@@ -130,11 +127,21 @@ export async function playRound(
clearScreen(); clearScreen();
printBanner(); printBanner();
console.log(colors.bold(`\nRound ${roundNumber} of ${session.config.rounds}`)); console.log(
console.log(colors.gray(`Time limit: ${session.config.timePerRound}s | Current streak: ${session.currentStreak}\n`)); colors.bold(`\nRound ${roundNumber} of ${session.config.rounds}`)
);
console.log(
colors.gray(
`Time limit: ${session.config.timePerRound}s | Current streak: ${session.currentStreak}\n`
)
);
console.log(colors.bold.white(`Translate to Morse code: ${colors.yellow(challenge)}\n`)); console.log(
console.log(colors.dim("Tip: Use dots (.) and dashes (-), separate letters with spaces")); colors.bold.white(`Translate to Morse code: ${colors.yellow(challenge)}\n`)
);
console.log(
colors.dim("Tip: Use dots (.) and dashes (-), separate letters with spaces")
);
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 // Create a promise that rejects after the time limit
@@ -173,6 +180,18 @@ export async function playRound(
console.log(colors.bold.red("✗ Incorrect")); console.log(colors.bold.red("✗ Incorrect"));
console.log(colors.gray(`Expected: ${result.expectedMorse}`)); console.log(colors.gray(`Expected: ${result.expectedMorse}`));
console.log(colors.gray(`You entered: ${result.userInput || "(nothing)"}`)); console.log(colors.gray(`You entered: ${result.userInput || "(nothing)"}`));
// Show what the user's morse code translates to
if (result.userInput) {
try {
const decoded = morseToText(result.userInput);
if (decoded && decoded !== result.userInput) {
console.log(colors.yellow(`Your morse translates to: ${decoded}`));
}
} catch {
// If decoding fails, just skip showing it
}
}
} }
// Wait for user to continue // Wait for user to continue
@@ -210,8 +229,7 @@ export function showGameResults(session: GameSession): void {
// Show detailed results // Show detailed results
console.log(colors.bold("\n📊 Round Details:\n")); console.log(colors.bold("\n📊 Round Details:\n"));
const detailsTable = new Table() const detailsTable = new Table().header([
.header([
colors.bold("#"), colors.bold("#"),
colors.bold("Challenge"), colors.bold("Challenge"),
colors.bold("Expected"), colors.bold("Expected"),
@@ -244,7 +262,9 @@ export function showStats(stats: GameStats): void {
console.log(colors.bold.cyan("\n📈 YOUR STATISTICS\n")); console.log(colors.bold.cyan("\n📈 YOUR STATISTICS\n"));
if (stats.totalGames === 0) { if (stats.totalGames === 0) {
console.log(colors.yellow("No games played yet! Start playing to see your stats.\n")); console.log(
colors.yellow("No games played yet! Start playing to see your stats.\n")
);
return; return;
} }
@@ -259,7 +279,12 @@ export function showStats(stats: GameStats): void {
["Overall Accuracy", `${getAccuracy(stats).toFixed(1)}%`], ["Overall Accuracy", `${getAccuracy(stats).toFixed(1)}%`],
["Average Time/Round", `${stats.averageTimePerRound.toFixed(1)}s`], ["Average Time/Round", `${stats.averageTimePerRound.toFixed(1)}s`],
["Best Streak", colors.yellow(stats.bestStreak.toString())], ["Best Streak", colors.yellow(stats.bestStreak.toString())],
["Last Played", stats.lastPlayed ? new Date(stats.lastPlayed).toLocaleDateString() : "Never"], [
"Last Played",
stats.lastPlayed
? new Date(stats.lastPlayed).toLocaleDateString()
: "Never",
],
]) ])
.border(true) .border(true)
.padding(1); .padding(1);
@@ -269,8 +294,7 @@ export function showStats(stats: GameStats): void {
// Mode-specific stats // Mode-specific stats
console.log(colors.bold("\n📊 Stats by Mode:\n")); console.log(colors.bold("\n📊 Stats by Mode:\n"));
const modeTable = new Table() const modeTable = new Table().header([
.header([
colors.bold("Mode"), colors.bold("Mode"),
colors.bold("Games"), colors.bold("Games"),
colors.bold("Correct"), colors.bold("Correct"),