feat: changed stats storage location, refactored code
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,4 +13,4 @@ deno.lock
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# User data
|
# User data
|
||||||
.morse-game/
|
data/
|
||||||
|
|||||||
18
main.ts
18
main.ts
@@ -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(
|
||||||
default: "letters",
|
"-m, --mode <mode:string>",
|
||||||
})
|
"Game mode (letters, numbers, words, phrases)",
|
||||||
|
{
|
||||||
|
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) => {
|
||||||
|
|||||||
49
stats.ts
49
stats.ts
@@ -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;
|
||||||
|
|||||||
110
ui.ts
110
ui.ts
@@ -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,15 +229,14 @@ 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"),
|
colors.bold("Your Answer"),
|
||||||
colors.bold("Your Answer"),
|
colors.bold("Result"),
|
||||||
colors.bold("Result"),
|
colors.bold("Time"),
|
||||||
colors.bold("Time"),
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
session.results.forEach((result, index) => {
|
session.results.forEach((result, index) => {
|
||||||
detailsTable.push([
|
detailsTable.push([
|
||||||
@@ -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,14 +294,13 @@ 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"),
|
colors.bold("Incorrect"),
|
||||||
colors.bold("Incorrect"),
|
colors.bold("Accuracy"),
|
||||||
colors.bold("Accuracy"),
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
Object.entries(stats.modeStats).forEach(([mode, modeStats]) => {
|
Object.entries(stats.modeStats).forEach(([mode, modeStats]) => {
|
||||||
if (modeStats.gamesPlayed > 0) {
|
if (modeStats.gamesPlayed > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user