Skip to content
174 changes: 162 additions & 12 deletions src/adagrams.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,165 @@
export const drawLetters = () => {
// Implement this method for wave 1
};
class Adagrams{
LETTER_POOL = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these shouldn't be edited, I would consider creating them as a static, read-only properties: https://stackoverflow.com/a/35242218

A: 9,
B: 2,
C: 2,
D: 4,
E: 12,
F: 2,
G: 3,
H: 2,
I: 9,
J: 1,
K: 1,
L: 4,
M: 2,
N: 6,
O: 8,
P: 2,
Q: 1,
R: 6,
S: 4,
T: 6,
U: 4,
V: 2,
W: 2,
X: 1,
Y: 2,
Z: 1,
};

export const usesAvailableLetters = (input, lettersInHand) => {
// Implement this method for wave 2
};
LETTER_SCORE = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these constants at the top of the file are only accessed by one function each, it would be reasonable to place the objects inside the functions rather than as a constant. There are tradeoffs, the structure would clutter the function some, but it keeps the data as close as possible to where it's being used, and would mean that other functions who shouldn't need it couldn't access it.

A: 1,
B: 3,
C: 3,
D: 2,
E: 1,
F: 4,
G: 2,
H: 4,
I: 1,
J: 8,
K: 5,
L: 1,
M: 3,
N: 1,
O: 1,
P: 3,
Q: 10,
R: 1,
S: 1,
T: 1,
U: 1,
V: 4,
W: 4,
X: 8,
Y: 4,
Z: 10,
};

constructor() {
this.input = "";
this.word = "";
this.highest = {
word: "",
score: 0
};

this.lettersInHand = new Array();

};

drawLetters() {
while (this.lettersInHand.length < 10) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice use of the hand length to control the while loop

let letter = String.fromCharCode(Math.floor(Math.random() * 26) + 65);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will give us an even chance of picking any single letter in the alphabet without going over the number of each tile we have. This is slightly different than what the README asks - we won't accurately represent the distribution of tiles because we pick a letter from 1-26, when the chances of picking some letters should be higher than others. How could we update the algorithm to account for this?


if (this.LETTER_POOL[letter] > 0) {
this.LETTER_POOL[letter] -= 1;
this.lettersInHand.push(letter);
}
Comment on lines +76 to +79

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are directly editing this.LETTER_POOL, what will happen after we call drawLetters on the same instance many times in a row?

letter = String.fromCharCode(Math.floor(Math.random() * 26) + 65);

};
return this.lettersInHand;
};

isValid(word) {
if(word.length < 1) return false;

for(let i=0; i < word.length; ++i){

if (!(/^[A-Z]$/.test(word[i])) ){

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool use of a regular expression!

return false;
}
}
return true;
};

usesAvailableLetters(input, lettersInHand) {
this.input = input.toUpperCase();
if (!this.isValid(this.input)){
return false;
}
this.lettersInHand = lettersInHand;

let letterFreq = {};
for (let i=0; i < this.lettersInHand.length; i++) {
let letter = this.lettersInHand[i]
letterFreq[letter] = (letterFreq[letter] || 0) +1 ;
};
Comment on lines +105 to +109

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice frequency map!


for (let i=0; i < this.input.length; i++) {
let letter = this.input[i];
if ((letter in letterFreq)&&(letterFreq[letter] > 0)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small style best practice note: we should include a blank space on either side of operators like &&.

letterFreq[letter] -= 1;
} else {
return false;
}
};
return true;
};

scoreWord(word) {
this.word = word.toUpperCase();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't typically hold onto the input of a function like this as an attribute on a class. If it isn't important to persist between function calls, then we should re-evaluate if it's something the class itself should hold.

if (!this.isValid(this.word)){
return 0;
}
let point = 0;

for (let i=0; i < this.word.length; ++i){

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we won't be reassigning the loop variable in the body of a loop, I recommend using const over let to declare the variable.

point += this.LETTER_SCORE[this.word[i]];
}
Comment on lines +127 to +131

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could simplify the syntax here a little with a for...of loop:

for (const letter of this.word) {
    point += this.LETTER_SCORE[letter];
}

Comment on lines +127 to +131

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have an unexpected result if the string word has characters that aren't in LETTER_SCORE . If we have an input like "ABC DEF" then point will hold NaN (Not a Number) after the loop. If we wanted to skip non-alphabetic characters or characters that aren't in LETTER_SCORE , how could we do that?


if(this.word.length >= 7){
point += 8;
}

return point;
};

highestScoreFrom(words) {

for (let word of words) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solution to tie break in a single loop!

let point = this.scoreWord(word);

if (point > this.highest.score){
this.highest.word = word;
this.highest.score = point;
} else if (point === this.highest.score) {
if(this.highest.word.length === 10){
continue;
} else if ((word.length === 10)||(this.highest.word.length > word.length)){
this.highest.word = word;
this.highest.score = point;
};
};
};

return this.highest
};

;}

export default Adagrams;

export const scoreWord = (word) => {
// Implement this method for wave 3
};

export const highestScoreFrom = (words) => {
// Implement this method for wave 4
};
16 changes: 3 additions & 13 deletions src/demo/adagrams.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import {
drawLetters,
usesAvailableLetters,
scoreWord,
highestScoreFrom,
} from "adagrams";

const Real = {
drawLetters,
usesAvailableLetters,
scoreWord,
highestScoreFrom,
};
import Adagrams from "../adagrams";

const Real = new Adagrams();

const Stub = {
drawLetters() {
Expand Down
3 changes: 1 addition & 2 deletions src/demo/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class Model {
["APPLE", "PAPA", "LEAP"], // round 1
["WALK", "WALKER", "RAKE"], // round 2
],

playerTwo: [
["PALE", "PELT"], // round 1
["REAL", "WALTER", "TALKER"], // round 2
Expand Down Expand Up @@ -137,4 +136,4 @@ class Model {
}
}

export default Model;
export default Model;
66 changes: 33 additions & 33 deletions test/adagrams.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
drawLetters,
usesAvailableLetters,
scoreWord,
highestScoreFrom,
} from "adagrams";
import Adagrams from "../src/adagrams"

const LETTER_POOL = {
A: 9,
Expand Down Expand Up @@ -37,13 +32,13 @@ const LETTER_POOL = {
describe("Adagrams", () => {
describe("drawLetters", () => {
it("draws ten letters from the letter pool", () => {
const drawn = drawLetters();
const drawn = new Adagrams().drawLetters();

expect(drawn).toHaveLength(10);
});

it("returns an array, and each item is a single-letter string", () => {
const drawn = drawLetters();
const drawn = new Adagrams().drawLetters();

expect(Array.isArray(drawn)).toBe(true);
drawn.forEach((l) => {
Expand All @@ -53,7 +48,7 @@ describe("Adagrams", () => {

it("does not draw a letter too many times", () => {
for (let i = 0; i < 1000; i++) {
const drawn = drawLetters();
const drawn = new Adagrams().drawLetters();
const letter_freq = {};
for (let letter of drawn) {
if (letter in letter_freq) {
Expand All @@ -75,33 +70,34 @@ describe("Adagrams", () => {
const drawn = ["D", "O", "G", "X", "X", "X", "X", "X", "X", "X"];
const word = "DOG";

const isValid = usesAvailableLetters(word, drawn);
const isValid = new Adagrams().usesAvailableLetters(word, drawn);
expect(isValid).toBe(true);
});

it("returns false when word contains letters not in the drawn letters", () => {
const drawn = ["D", "O", "X", "X", "X", "X", "X", "X", "X", "X"];
const word = "DOG";

const isValid = usesAvailableLetters(word, drawn);
const isValid = new Adagrams().usesAvailableLetters(word, drawn);
expect(isValid).toBe(false);
});

it("returns false when word contains repeated letters more than in the drawn letters", () => {
const drawn = ["D", "O", "G", "X", "X", "X", "X", "X", "X", "X"];
const word = "GOOD";

const isValid = usesAvailableLetters(word, drawn);
const isValid = new Adagrams().usesAvailableLetters(word, drawn);
expect(isValid).toBe(false);
});
});

describe("scoreWord", () => {
const expectScores = (wordScores) => {
Object.entries(wordScores).forEach(([word, score]) => {
expect(scoreWord(word)).toBe(score);
});
};
const expectScore = new Adagrams().scoreWord(word);
expect(expectScore).toBe(score);
});
}

it("returns an accurate numerical score according to the score chart", () => {
expectScores({
Expand All @@ -120,7 +116,10 @@ describe("Adagrams", () => {
});

it("returns a score of 0 if given an empty input", () => {
throw "Complete test";
expectScores({
"": 0
});
// throw "Complete test";
});

it("adds an extra 8 points if word is 7 or more characters long", () => {
Expand All @@ -133,24 +132,25 @@ describe("Adagrams", () => {
});
});

describe.skip("highestScoreFrom", () => {
describe("highestScoreFrom", () => {
it("returns a hash that contains the word and score of best word in an array", () => {
const words = ["X", "XX", "XXX", "XXXX"];
const correct = { word: "XXXX", score: scoreWord("XXXX") };
const correct = { word: "XXXX", score: new Adagrams().scoreWord("XXXX") };

expect(highestScoreFrom(words)).toEqual(correct);
expect(new Adagrams().highestScoreFrom(words)).toEqual(correct);
});

it("accurately finds best scoring word even if not sorted", () => {
const words = ["XXX", "XXXX", "X", "XX"];
const correct = { word: "XXXX", score: scoreWord("XXXX") };

throw "Complete test by adding an assertion";
const correct = { word: "XXXX", score: new Adagrams().scoreWord("XXXX") };

expect(new Adagrams().highestScoreFrom(words)).toEqual(correct);
// throw "Complete test by adding an assertion";
});

describe("in case of tied score", () => {
const expectTie = (words) => {
const scores = words.map((word) => scoreWord(word));
const scores = words.map((word) => new Adagrams().scoreWord(word));
const highScore = scores.reduce((h, s) => (h < s ? s : h), 0);
const tiedWords = scores.filter((s) => s == highScore);

Expand All @@ -162,37 +162,37 @@ describe("Adagrams", () => {
const words = ["AAAAAAAAAA", "BBBBBB"];
const correct = {
word: "AAAAAAAAAA",
score: scoreWord("AAAAAAAAAA"),
score: new Adagrams().scoreWord("AAAAAAAAAA"),
};
expectTie(words);

expect(highestScoreFrom(words)).toEqual(correct);
expect(highestScoreFrom(words.reverse())).toEqual(correct);
expect(new Adagrams().highestScoreFrom(words)).toEqual(correct);
expect(new Adagrams().highestScoreFrom(words.reverse())).toEqual(correct);
});

it("selects the word with fewer letters when neither are 10 letters", () => {
const words = ["MMMM", "WWW"];
const correct = { word: "WWW", score: scoreWord("WWW") };
const correct = { word: "WWW", score: new Adagrams().scoreWord("WWW") };
expectTie(words);

expect(highestScoreFrom(words)).toEqual(correct);
expect(highestScoreFrom(words.reverse())).toEqual(correct);
expect(new Adagrams().highestScoreFrom(words)).toEqual(correct);
expect(new Adagrams().highestScoreFrom(words.reverse())).toEqual(correct);
});

it("selects the first word when both have same length", () => {
const words = ["AAAAAAAAAA", "EEEEEEEEEE"];
const first = {
word: "AAAAAAAAAA",
score: scoreWord("AAAAAAAAAA"),
score: new Adagrams().scoreWord("AAAAAAAAAA"),
};
const second = {
word: "EEEEEEEEEE",
score: scoreWord("EEEEEEEEEE"),
score: new Adagrams().scoreWord("EEEEEEEEEE"),
};
expectTie(words);

expect(highestScoreFrom(words)).toEqual(first);
expect(highestScoreFrom(words.reverse())).toEqual(second);
expect(new Adagrams().highestScoreFrom(words)).toEqual(first);
expect(new Adagrams().highestScoreFrom(words.reverse())).toEqual(second);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions test/demo/model.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Model from 'demo/model';
import Adagrams from 'demo/adagrams';

describe.skip('Game Model', () => {
describe('Game Model', () => {
const config = {
players: [
'Player A',
Expand Down Expand Up @@ -335,4 +335,4 @@ describe.skip('Game Model', () => {
});
});
});
});
});