first commit
This commit is contained in:
348
ui.ts
Normal file
348
ui.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
// UI utilities and game interface
|
||||
|
||||
import { colors } from "@cliffy/ansi/colors";
|
||||
import { Input } from "@cliffy/prompt/input";
|
||||
import { Select } from "@cliffy/prompt/select";
|
||||
import { Confirm } from "@cliffy/prompt/confirm";
|
||||
import { Table } from "@cliffy/table";
|
||||
import { textToMorse } from "./morse.ts";
|
||||
import type { GameStats } 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
|
||||
*/
|
||||
export function clearScreen(): void {
|
||||
console.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a banner/header
|
||||
*/
|
||||
export function printBanner(): void {
|
||||
console.log(colors.bold.cyan(`
|
||||
╔═══════════════════════════════════════╗
|
||||
║ MORSE CODE PRACTICE GAME ║
|
||||
║ .... . .-.. .-.. --- ║
|
||||
╚═══════════════════════════════════════╝
|
||||
`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show main menu and get user selection
|
||||
*/
|
||||
export async function showMainMenu(): Promise<string> {
|
||||
const action = await Select.prompt({
|
||||
message: "What would you like to do?",
|
||||
options: [
|
||||
{ name: "Start New Game", value: "play" },
|
||||
{ name: "View Statistics", value: "stats" },
|
||||
{ name: "Morse Code Reference", value: "reference" },
|
||||
{ name: "Reset Statistics", value: "reset" },
|
||||
{ name: "Exit", value: "exit" },
|
||||
],
|
||||
});
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select game mode
|
||||
*/
|
||||
export async function selectGameMode(): Promise<GameMode> {
|
||||
const mode = await Select.prompt<GameMode>({
|
||||
message: "Select difficulty mode:",
|
||||
options: [
|
||||
{
|
||||
name: `${colors.green("Easy")} - Letters (A-Z)`,
|
||||
value: "letters" as GameMode
|
||||
},
|
||||
{
|
||||
name: `${colors.yellow("Medium")} - Numbers (0-9)`,
|
||||
value: "numbers" as GameMode
|
||||
},
|
||||
{
|
||||
name: `${colors.magenta("Hard")} - Words`,
|
||||
value: "words" as GameMode
|
||||
},
|
||||
{
|
||||
name: `${colors.red("Expert")} - Phrases`,
|
||||
value: "phrases" as GameMode
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure game settings
|
||||
*/
|
||||
export async function configureGame(mode: GameMode): Promise<{ rounds: number; timePerRound: number }> {
|
||||
const roundsInput = await Input.prompt({
|
||||
message: "How many rounds? (5-50)",
|
||||
default: "10",
|
||||
validate: (value) => {
|
||||
const num = parseInt(value);
|
||||
if (isNaN(num) || num < 5 || num > 50) {
|
||||
return "Please enter a number between 5 and 50";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const timeInput = await Input.prompt({
|
||||
message: "Seconds per round? (10-60)",
|
||||
default: "30",
|
||||
validate: (value) => {
|
||||
const num = parseInt(value);
|
||||
if (isNaN(num) || num < 10 || num > 60) {
|
||||
return "Please enter a number between 10 and 60";
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rounds: parseInt(roundsInput),
|
||||
timePerRound: parseInt(timeInput),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a single round of the game
|
||||
*/
|
||||
export async function playRound(
|
||||
session: GameSession,
|
||||
roundNumber: number
|
||||
): Promise<RoundResult> {
|
||||
const challenge = getChallenge(session.config.mode);
|
||||
const startTime = Date.now();
|
||||
|
||||
clearScreen();
|
||||
printBanner();
|
||||
console.log(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(colors.dim("Tip: Use dots (.) and dashes (-), separate letters with spaces"));
|
||||
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!"));
|
||||
}, session.config.timePerRound * 1000);
|
||||
});
|
||||
|
||||
// Get user input with timeout
|
||||
let userInput = "";
|
||||
try {
|
||||
const inputPromise = Input.prompt({
|
||||
message: "Your answer:",
|
||||
minLength: 0,
|
||||
});
|
||||
|
||||
userInput = await Promise.race([inputPromise, timeoutPromise]);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "Time's up!") {
|
||||
console.log(colors.red("\n⏰ Time's up!"));
|
||||
userInput = ""; // Empty input for timeout
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const timeSpent = Date.now() - startTime;
|
||||
const result = processRound(session, challenge, userInput, timeSpent);
|
||||
|
||||
// Show result
|
||||
console.log();
|
||||
if (result.correct) {
|
||||
console.log(colors.bold.green("✓ Correct!"));
|
||||
} else {
|
||||
console.log(colors.bold.red("✗ Incorrect"));
|
||||
console.log(colors.gray(`Expected: ${result.expectedMorse}`));
|
||||
console.log(colors.gray(`You entered: ${result.userInput || "(nothing)"}`));
|
||||
}
|
||||
|
||||
// Wait for user to continue
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show game results
|
||||
*/
|
||||
export function showGameResults(session: GameSession): void {
|
||||
clearScreen();
|
||||
printBanner();
|
||||
|
||||
const summary = getGameSummary(session);
|
||||
|
||||
console.log(colors.bold.cyan("\n🎮 GAME COMPLETE!\n"));
|
||||
|
||||
const table = new Table()
|
||||
.header([colors.bold("Metric"), colors.bold("Value")])
|
||||
.body([
|
||||
["Total Rounds", summary.totalRounds.toString()],
|
||||
["Correct", colors.green(summary.correct.toString())],
|
||||
["Incorrect", colors.red(summary.incorrect.toString())],
|
||||
["Accuracy", `${summary.accuracy.toFixed(1)}%`],
|
||||
["Average Time", `${(summary.averageTime / 1000).toFixed(1)}s`],
|
||||
["Best Streak", colors.yellow(summary.streak.toString())],
|
||||
])
|
||||
.border(true)
|
||||
.padding(1);
|
||||
|
||||
table.render();
|
||||
|
||||
// Show detailed results
|
||||
console.log(colors.bold("\n📊 Round Details:\n"));
|
||||
|
||||
const detailsTable = new Table()
|
||||
.header([
|
||||
colors.bold("#"),
|
||||
colors.bold("Challenge"),
|
||||
colors.bold("Expected"),
|
||||
colors.bold("Your Answer"),
|
||||
colors.bold("Result"),
|
||||
colors.bold("Time"),
|
||||
]);
|
||||
|
||||
session.results.forEach((result, index) => {
|
||||
detailsTable.push([
|
||||
(index + 1).toString(),
|
||||
result.challenge,
|
||||
result.expectedMorse,
|
||||
result.userInput || "-",
|
||||
result.correct ? colors.green("✓") : colors.red("✗"),
|
||||
`${(result.timeSpent / 1000).toFixed(1)}s`,
|
||||
]);
|
||||
});
|
||||
|
||||
detailsTable.border(true).render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show statistics
|
||||
*/
|
||||
export function showStats(stats: GameStats): void {
|
||||
clearScreen();
|
||||
printBanner();
|
||||
|
||||
console.log(colors.bold.cyan("\n📈 YOUR STATISTICS\n"));
|
||||
|
||||
if (stats.totalGames === 0) {
|
||||
console.log(colors.yellow("No games played yet! Start playing to see your stats.\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Overall stats
|
||||
const overallTable = new Table()
|
||||
.header([colors.bold("Overall Stats"), colors.bold("Value")])
|
||||
.body([
|
||||
["Total Games", stats.totalGames.toString()],
|
||||
["Total Rounds", stats.totalRounds.toString()],
|
||||
["Correct Answers", colors.green(stats.correctAnswers.toString())],
|
||||
["Incorrect Answers", colors.red(stats.incorrectAnswers.toString())],
|
||||
["Overall Accuracy", `${getAccuracy(stats).toFixed(1)}%`],
|
||||
["Average Time/Round", `${stats.averageTimePerRound.toFixed(1)}s`],
|
||||
["Best Streak", colors.yellow(stats.bestStreak.toString())],
|
||||
["Last Played", stats.lastPlayed ? new Date(stats.lastPlayed).toLocaleDateString() : "Never"],
|
||||
])
|
||||
.border(true)
|
||||
.padding(1);
|
||||
|
||||
overallTable.render();
|
||||
|
||||
// Mode-specific stats
|
||||
console.log(colors.bold("\n📊 Stats by Mode:\n"));
|
||||
|
||||
const modeTable = new Table()
|
||||
.header([
|
||||
colors.bold("Mode"),
|
||||
colors.bold("Games"),
|
||||
colors.bold("Correct"),
|
||||
colors.bold("Incorrect"),
|
||||
colors.bold("Accuracy"),
|
||||
]);
|
||||
|
||||
Object.entries(stats.modeStats).forEach(([mode, modeStats]) => {
|
||||
if (modeStats.gamesPlayed > 0) {
|
||||
modeTable.push([
|
||||
mode.charAt(0).toUpperCase() + mode.slice(1),
|
||||
modeStats.gamesPlayed.toString(),
|
||||
colors.green(modeStats.correctAnswers.toString()),
|
||||
colors.red(modeStats.incorrectAnswers.toString()),
|
||||
`${modeStats.averageAccuracy.toFixed(1)}%`,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
modeTable.border(true).render();
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show morse code reference
|
||||
*/
|
||||
export function showReference(): void {
|
||||
clearScreen();
|
||||
printBanner();
|
||||
|
||||
console.log(colors.bold.cyan("\n📖 MORSE CODE REFERENCE\n"));
|
||||
|
||||
console.log(colors.bold("Letters:"));
|
||||
const letterTable = new Table();
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
|
||||
// Display in 3 columns
|
||||
for (let i = 0; i < letters.length; i += 3) {
|
||||
const row = [];
|
||||
for (let j = 0; j < 3 && i + j < letters.length; j++) {
|
||||
const letter = letters[i + j];
|
||||
row.push(`${colors.yellow(letter)} = ${textToMorse(letter)}`);
|
||||
}
|
||||
letterTable.push(row);
|
||||
}
|
||||
letterTable.render();
|
||||
|
||||
console.log(colors.bold("\nNumbers:"));
|
||||
const numberTable = new Table();
|
||||
const numbers = "0123456789".split("");
|
||||
|
||||
for (let i = 0; i < numbers.length; i += 5) {
|
||||
const row = [];
|
||||
for (let j = 0; j < 5 && i + j < numbers.length; j++) {
|
||||
const num = numbers[i + j];
|
||||
row.push(`${colors.yellow(num)} = ${textToMorse(num)}`);
|
||||
}
|
||||
numberTable.push(row);
|
||||
}
|
||||
numberTable.render();
|
||||
|
||||
console.log(colors.bold("\nSpecial:"));
|
||||
console.log(`${colors.yellow("Space")} = /`);
|
||||
console.log(`${colors.yellow(".")} = .-.-.-`);
|
||||
console.log(`${colors.yellow(",")} = --..--`);
|
||||
console.log(`${colors.yellow("?")} = ..--..`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm action
|
||||
*/
|
||||
export async function confirmAction(message: string): Promise<boolean> {
|
||||
return await Confirm.prompt({ message, default: false });
|
||||
}
|
||||
Reference in New Issue
Block a user