feat: added new features, refactored code
This commit is contained in:
105
game.ts
105
game.ts
@@ -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,12 +156,13 @@ 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 =
|
||||||
? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds
|
totalRounds > 0
|
||||||
: 0;
|
? session.results.reduce((sum, r) => sum + r.timeSpent, 0) / totalRounds
|
||||||
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalRounds,
|
totalRounds,
|
||||||
@@ -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
10
main.ts
@@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
13
stats.ts
13
stats.ts
@@ -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,
|
||||||
|
|||||||
83
ui.ts
83
ui.ts
@@ -58,19 +58,25 @@ export async function selectGameMode(): Promise<GameMode> {
|
|||||||
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",
|
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",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `${colors.red("Expert")} - Phrases`,
|
name: `${colors.red("Expert")} - Phrases`,
|
||||||
value: "phrases",
|
value: "phrases",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user