Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 142 additions & 2 deletions scrabble.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,155 @@
const LETTER_VALUES =
{
"a": { "points": 1, "tiles": 9 },
"b": { "points": 3, "tiles": 2 },
"c": { "points": 3, "tiles": 2 },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice work tying together point values and tile counts in one data structure - very clever.

"d": { "points": 2, "tiles": 4 },
"e": { "points": 1, "tiles": 12 },
"f": { "points": 4, "tiles": 2 },
"g": { "points": 2, "tiles": 3 },
"h": { "points": 4, "tiles": 2 },
"i": { "points": 1, "tiles": 9 },
"j": { "points": 8, "tiles": 1 },
"k": { "points": 5, "tiles": 1 },
"l": { "points": 1, "tiles": 4 },
"m": { "points": 3, "tiles": 2 },
"n": { "points": 1, "tiles": 6 },
"o": { "points": 1, "tiles": 8 },
"p": { "points": 3, "tiles": 2 },
"q": { "points": 10, "tiles": 1 },
"r": { "points": 1, "tiles": 6 },
"s": { "points": 1, "tiles": 4 },
"t": { "points": 1, "tiles": 6 },
"u": { "points": 1, "tiles": 4 },
"v": { "points": 4, "tiles": 2 },
"w": { "points": 4, "tiles": 2 },
"x": { "points": 8, "tiles": 1 },
"y": { "points": 4, "tiles": 2 },
"z": { "points": 10, "tiles": 1 },
"blank": { "tiles": 2 }
}
const MAX_LENGTH = 7;

const Scrabble = {

isRealWord(word) {
if (!word.match(/^[a-zA-Z]+$/)) {
throw "Not a valid word";
} else if (word.length === 0) {
throw "No word given";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Instead of throwing a string, you should throw an instance of Error. That gives you access to stack trace information. Something like this:

if (condition) {
  throw new Error(`Invalid word ${word}`);
}

} else if (word.length > MAX_LENGTH) {
throw "Scrabble words can only be 7 letters!";
}
},

score(word) {

Scrabble.isRealWord(word);

word = word.toLowerCase().split("");
let wordScore = 0;

word.forEach (function (letter) {
let points = LETTER_VALUES[letter]["points"];
wordScore += points;
});

if (word.length === 7) {
wordScore += 50;
}

return wordScore;
},
breakTie(topWord, word) {
if ( topWord.length === 7 ) {
return topWord;
} else if (word.length === 7) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good use of a helper function to clarify this logic.

return word;
} else if (word.length < topWord.length) {
return word;
} else {
return topWord;
}
},

highestScoreFrom(arrayOfWords) {

},
};
if (arrayOfWords.length === 0) {
throw "No words given";
}

let topWord = arrayOfWords[0];

arrayOfWords.forEach (function (word) {
if ( Scrabble.score(word) > Scrabble.score(topWord)) {
topWord = word;
} else if ( Scrabble.score(word) == Scrabble.score(topWord)) {
topWord = Scrabble.breakTie(topWord, word);
}
});
return topWord;
}
}

Scrabble.Player = class {

constructor(name) {
if (name == null) {
throw("The player needs a name!");
}
this.name = name;
this.plays = [];
}

play(word) {
Scrabble.isRealWord(word);

if (this.hasWon()) {
return false;
} else {
this.plays.push(word);
}
return true;
}

totalScore() {
let score = 0;
this.plays.forEach (function (word){
let wordScore = Scrabble.score(word);
score += wordScore;
});
return score;
}

hasWon() {
let score = this.totalScore();
if (score >= 100) {
return true;
} else {
return false;
}
}

highestScoringWord() {
if (this.plays.length === 0) {
throw "No words played";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You should be reusing the scoring logic from highestScoreFrom here.


let max_word = this.plays[0];
this.plays.forEach (function(word) {
if ( Scrabble.score(word) > Scrabble.score(max_word) ) {
max_word = word;
}
});
return max_word;
}

highestWordScore() {
let word = this.highestScoringWord();
let wordScore = Scrabble.score(word);

return wordScore;
}
};


Expand Down
88 changes: 58 additions & 30 deletions specs/scrabble.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,46 @@ describe('score', () => {
expect(Scrabble.score('pig')).toBe(6);
});

test.skip('adds 50 points for a 7-letter word', () => {
test('adds 50 points for a 7-letter word', () => {
expect(Scrabble.score('academy')).toBe(65);
});

test.skip('throws on bad characters', () => {
test('throws on bad characters', () => {
expect(() => {
Scrabble.score('char^');
}).toThrow();
});

test.skip('handles all upper- and lower-case letters', () => {
test('handles all upper- and lower-case letters', () => {
expect(Scrabble.score('dog')).toBe(5);
expect(Scrabble.score('DOG')).toBe(5);
expect(Scrabble.score('DoG')).toBe(5);
});

test.skip('does not allow words > 7 letters', () => {
test('does not allow words > 7 letters', () => {
expect(() => { Scrabble.score('abcdefgh'); }).toThrow();
});

test.skip('does not allow empty words', () => {
test('does not allow empty words', () => {
expect(() => { Scrabble.score(''); }).toThrow();
});
});

describe('highestScoreFrom', () => {
test.skip('is defined', () => {
test('is defined', () => {
expect(Scrabble.highestScoreFrom).toBeDefined();
});

test.skip('throws if no words were passed', () => {
test('throws if no words were passed', () => {
expect(() => { Scrabble.highestScoreFrom([]); }).toThrow();
expect(() => { Scrabble.highestScoreFrom('not array'); }).toThrow();
});

test.skip('returns the only word in a length-1 array', () => {
test('returns the only word in a length-1 array', () => {
expect(Scrabble.highestScoreFrom(['dog'])).toBe('dog');
});

test.skip('returns the highest word if there are two words', () => {
test('returns the highest word if there are two words', () => {
// Check score assumptions
expect(Scrabble.score('dog')).toBe(5);
expect(Scrabble.score('pig')).toBe(6);
Expand All @@ -60,7 +60,7 @@ describe('highestScoreFrom', () => {
expect(Scrabble.highestScoreFrom(['pig', 'dog'])).toBe('pig');
});

test.skip('if tied, prefer a word with 7 letters', () => {
test('if tied, prefer a word with 7 letters', () => {
const loser = 'zzzzzz';
const winner = 'iiiiddd';

Expand All @@ -73,7 +73,7 @@ describe('highestScoreFrom', () => {
expect(Scrabble.highestScoreFrom([winner, loser])).toBe(winner);
});

test.skip('if tied and no word has 7 letters, prefers the word with fewer letters', () => {
test('if tied and no word has 7 letters, prefers the word with fewer letters', () => {
// Check score assumptions
expect(Scrabble.score('dog')).toBe(5);
expect(Scrabble.score('goat')).toBe(5);
Expand All @@ -83,7 +83,7 @@ describe('highestScoreFrom', () => {
expect(Scrabble.highestScoreFrom(['goat', 'dog'])).toBe('dog');
});

test.skip('returns the first word of a tie with same letter count', () => {
test('returns the first word of a tie with same letter count', () => {
// Check score assumptions
expect(Scrabble.score('i')).toBe(1);
expect(Scrabble.score('dog')).toBe(5);
Expand All @@ -98,27 +98,27 @@ describe('highestScoreFrom', () => {
});

describe('Player', () => {
test.skip('is defined', () => {
test('is defined', () => {
expect(Scrabble.Player).toBeDefined();
});

describe('Constructor', () => {
test.skip('Creates a new player', () => {
test('Creates a new player', () => {
const name = 'test name';
const player = new Scrabble.Player(name);

expect(player.name).toBe(name);
});

test.skip('Requires a name', () => {
test('Requires a name', () => {
expect(() => {
new Scrabble.Player();
}).toThrow();
});
});

describe('play', () => {
test.skip('Records the played word', () => {
test('Records the played word', () => {
const word = 'dog';
const player = new Scrabble.Player('test player');

Expand All @@ -130,7 +130,7 @@ describe('Player', () => {
expect(player.plays[0]).toBe(word);
});

test.skip('Requires a real word', () => {
test('Requires a real word', () => {
const player = new Scrabble.Player('test player');

expect(player.plays.length).toBe(0);
Expand All @@ -142,7 +142,7 @@ describe('Player', () => {
expect(player.plays.length).toBe(0);
});

test.skip('Returns false and does not update plays if the player has already won', () => {
test('Returns false and does not update plays if the player has already won', () => {
const player = new Scrabble.Player('test player');

expect(player.play('zzzzzzz')).toBeTruthy(); // +120 pts
Expand All @@ -155,13 +155,13 @@ describe('Player', () => {
});

describe('totalScore', () => {
test.skip('Is zero if the player has not played anything', () => {
test('Is zero if the player has not played anything', () => {
const player = new Scrabble.Player('test player');

expect(player.totalScore()).toBe(0);
});

test.skip('Is updated by play', () => {
test('Is updated by play', () => {
// Arrange
const player = new Scrabble.Player('test player');
const words = [{word: 'dog', score: 5}, {word: 'cat', score: 5}, {word: 'goat', score: 5}];
Expand All @@ -181,44 +181,72 @@ describe('Player', () => {
});

describe('hasWon', () => {
test.skip('returns false when score < 100', () => {

test('returns false when score < 100', () => {
const player = new Scrabble.Player('test player');
player.play("hello");

expect(player.hasWon()).toBeFalsy();
});

test.skip('returns true when score == 100', () => {
test('returns true when score == 100', () => {
const player = new Scrabble.Player('test player');
player.play("zzzzz");
player.play("zzzzz");

expect(player.hasWon()).toBeTruthy();

});

test.skip('returns true when score > 100', () => {
test('returns true when score > 100', () => {
const player = new Scrabble.Player('test player');
player.play("zzzzzs");
player.play("zzzzzx");

expect(player.hasWon()).toBeTruthy();

});
});

describe('highestScoringWord', () => {
// Tie-breaking logic is already described in the tests
// for highestWordFrom, so we will not repeat it here.
test.skip('returns the highest scoring word played', () => {
test('returns the highest scoring word played', () => {
const player = new Scrabble.Player('test player');
let words = ["whales", "hello", "zzzzzj"];

words.forEach (function(word) {
player.play(word);
});

expect(player.highestScoringWord()).toEqual("zzzzzj");
});

test.skip('throws an error if no words have been played', () => {

test('throws an error if no words have been played', () => {
const player = new Scrabble.Player('test player');
expect(player.plays.length).toEqual(0);

expect(() => { player.highestScoringWord();
}).toThrow();
});
});

describe('highestWordScore', () => {
test.skip('returns the score of the highest scoring word played', () => {
test('returns the score of the highest scoring word played', () => {
const player = new Scrabble.Player('test player');
let words = ["whales", "hello", "zzzzzj"];

words.forEach (function(word) {
player.play(word);
});

});
expect(player.highestWordScore()).toEqual(58);

test.skip('throws an error if no words have been played', () => {
});

test('throws an error if no words have been played', () => {
const player = new Scrabble.Player('test player');
expect(() => { player.highestWordScore();
}).toThrow();

});
});
Expand Down