Skip to content

Commit

Permalink
Add CLI infrastructure and an option to dump stats
Browse files Browse the repository at this point in the history
  • Loading branch information
kiprobinson committed Jan 8, 2022
1 parent 16fec39 commit a6af8f0
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 1 deletion.
31 changes: 31 additions & 0 deletions app/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as process from 'process';
import * as readline from 'readline';
import CliUtils from '../lib/cli-utils';
import dumpStats from "./dump-stats";

const menuOptions:Array<{name:string, handler:Function}> = [
{
name: 'Dump wordlist stats.',
handler: dumpStats,
}
];

/**
* Run the command line interface to this app.
*/
const runCli = async() => {
const rl = readline.createInterface(process.stdin, process.stdout);
console.log(' *- Welcome to the Wordle Solver! -* ');
console.log('-------------------------------------');
console.log('');

const cliPrompts = new CliUtils(rl);
const selection = await cliPrompts.menuPrompt(menuOptions.map(o => o.name));

await menuOptions[selection].handler();

rl.close();
}


export default runCli;
25 changes: 25 additions & 0 deletions app/cli/dump-stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { FrequencyTable } from "../lib/frequency-table";
import { getWordListStats } from "../lib/word-list";

const printSortedTable = (frequencyTable:FrequencyTable) => {
const sortedTable = frequencyTable.getSortedFrequencyTable();
for(const entry of sortedTable)
console.log(`${entry.letter}: ${(100 * entry.percent).toFixed(3).padStart(7)} %`);
}

const dumpStats = async ():Promise<void> => {
const stats = getWordListStats();

console.log('-----------------------------');

console.log('Frequencies for ALL positions:');
printSortedTable(stats.overallFrequencies);

for(let i = 0; i < stats.characterFrequencies.length; i++) {
console.log();
console.log(`Frequencies in character ${i}:`);
printSortedTable(stats.characterFrequencies[i]);
}
}

export default dumpStats;
56 changes: 56 additions & 0 deletions app/lib/cli-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as readline from 'readline';

/**
* A few utilities for command line interfaces.
*/
class CliUtils {
readonly rl:readline.Interface;

/**
* @param rl Instance of node readline interface. This is *not* closed by this app.
*/
constructor(rl:readline.Interface) {
this.rl = rl;
}

/**
* Async version of readline.question()
*/
ask(question: string):Promise<string> {
return new Promise(resolve => {
this.rl.question(question, answer => resolve(answer));
})
}

/**
* Prompts the user with a menu of options on the terminal, and waits for
* them to enter the number of the option. Returns the index of the selected
* option. If user enters invalid option, They are prompted to enter again.
*/
async menuPrompt (options: string[], prompt?:string):Promise<number> {
prompt = prompt || 'What would you like to do?';

//we exit loop by returning, when user enters a valid selection
while(true) {
console.log();
console.log(prompt);
for(let i = 0; i < options.length; i++)
console.log(`${i+1}: ${options[i]}`);

console.log();
let answer = await this.ask(`Enter your selection (1-${options.length}): `);
answer = answer.trim();
if(/^\d+$/.test(answer)) {
let selection = Number(answer);
if(0 < selection && selection <= options.length)
return selection - 1;
}

//if we get here, user has made an invalid selection
console.log();
console.log(`Sorry, that's not a valid option. Try again!`);
}
};
}

export default CliUtils;
82 changes: 82 additions & 0 deletions app/lib/frequency-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@

const ALL_LETTERS = 'abcdefghijklmnopqrstuvwxyz'.split('');

export class FrequencyTableEntry {
count: number = 0;
percent: number = NaN;
}

export type SortedFrequencyTableEntry = {
letter: string;
count: number;
percent: number;
}

export type SortedFrequencyTable = Array<SortedFrequencyTableEntry>;

/**
* Represents the frequency of each letter in a given data set.
*/
export class FrequencyTable {
/** Total characters in frequency table. */
count: number = 0;

/** Flag to indicate if the percentages are accurate or not. */
private percentagesAccurate:boolean = true;

/** Counts for each letter of the alphabet. */
private entries: Record<string, FrequencyTableEntry> = {};

constructor() {
ALL_LETTERS.forEach(c => this.entries[c] = new FrequencyTableEntry());
}

/**
* Increments the count for the given letter.
*/
incrementLetter(letter:string):void {
this.entries[letter].count++;
this.count++;
this.percentagesAccurate = false;
}

/**
* Gets the number of times the given letter was encountered.
*/
getLetterCount(letter:string):number {
return this.entries[letter].count;
}

/**
* Gets the percentage of letters seen that were the given letter.
*/
getLetterPercent(letter:string):number {
if(!this.percentagesAccurate)
this.updatePercentages();
return this.entries[letter].percent;
}

/**
* Updates the percentage field for all numbers in the table.
*/
private updatePercentages():void {
if(this.percentagesAccurate)
return;
ALL_LETTERS.forEach(c => this.entries[c].percent = this.entries[c].count / this.count);
this.percentagesAccurate = true;
}

/**
* Converts this table to a sorted frequency table, with the most common letters
* first and least common letters last.
*/
getSortedFrequencyTable():SortedFrequencyTable {
const ret:SortedFrequencyTable = ALL_LETTERS.map(letter => ({
letter,
count: this.getLetterCount(letter),
percent: this.getLetterPercent(letter),
}));
ret.sort((a,b) => b.percent - a.percent);
return ret;
}
}
40 changes: 40 additions & 0 deletions app/lib/word-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as fs from 'fs';
import { FrequencyTable } from './frequency-table';

type WordListStats = {
overallFrequencies: FrequencyTable;
characterFrequencies: Array<FrequencyTable>;
}

const VALID_WORD_REGEX = /^[a-z]{5}$/;

export const wordList:string[] = fs.readFileSync('app/resources/word-list.txt').toString()
.split(/\s+/)
.filter(s => VALID_WORD_REGEX.test(s));

/**
* Parse the word list to calculate frequencies of each character overall and per-character.
*/
export const getWordListStats = ():WordListStats => {
const stats:WordListStats = {
overallFrequencies: new FrequencyTable(),
characterFrequencies: [
new FrequencyTable(),
new FrequencyTable(),
new FrequencyTable(),
new FrequencyTable(),
new FrequencyTable(),
]
};

for(const word of wordList) {
for(let i = 0; i < 5; i++) {
const letter = word[i];
stats.overallFrequencies.incrementLetter(letter);
stats.characterFrequencies[i].incrementLetter(letter);
}
}

return stats;
}

4 changes: 3 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
console.log('coming soon!');
import runCli from "./app/cli/cli";

runCli();

0 comments on commit a6af8f0

Please sign in to comment.