After the first two parts of
this guide we have a single method and a single test. At this point we're going to really pick it up. There should be a lot less "talk" and a lot more "action" in part 3. I'll try to only explain new concepts.
UPDATE: I've completed the tests. Check out Part 4 to see the code coverage provided by blanket.js.
The tests (comments in the code):
describe('Hangman', function() {
describe('buildAlphabetArray()', function() {
it('should populate alphabet with 26 characters', function() {
buildAlphabetArray();
expect(alphabet.length).toBe(26);
});
});
describe('newGame()', function(){
beforeEach(function() {
// functions are added to the window object when they're not explicitly
// by creating a spy like this we're telling Jasmine that we want
// to keep an eye on that method
spyOn(window, 'buildAlphabetDisplay');
spyOn(window, 'getNewWord');
// since our newGame() function is going to manipulate the canvas object in the DOM,
// we need to add it to the DOM before our tests run
var canvas = document.createElement('canvas');
canvas.id = "canvas";
document.body.appendChild(canvas);
});
afterEach(function() {
// since we added the canvas to the DOM before the tests, we want to remove it
// from the DOM after each test
document.body.removeChild(document.getElementById('canvas'));
});
it('should set badGuesses to 0', function() {
badGuesses = 15;
newGame();
expect(badGuesses).toBe(0);
});
it('should set correctGuesses to 0', function() {
correctGuesses = 15;
newGame();
expect(correctGuesses).toBe(0);
});
it('should call getNewWord', function() {
newGame();
// this expectation is possibly because we spied on this function in the beforeEach
expect(window.getNewWord).toHaveBeenCalled();
});
it('should call buildAlphabetDisplay', function() {
newGame();
expect(window.buildAlphabetDisplay).toHaveBeenCalled();
});
});
describe('getNewWord()', function() {
beforeEach(function() {
wordToGuess = '';
// when Math.random() is called we want to spy on it (so we'll know it
// was called), but we also want it to go ahead and return a random number
spyOn(Math, 'random').and.callThrough();
// when Math.floor() is called we want to spy on it and always return a 1
spyOn(Math, 'floor').and.returnValue(1);
spyOn(window, 'buildPlaceholders');
});
it('should call Math.random', function() {
getNewWord();
expect(Math.random).toHaveBeenCalled();
});
it('should call Math.floor', function() {
getNewWord();
expect(Math.floor).toHaveBeenCalled();
});
it('should set wordToGuess to word randomly selected from array', function() {
getNewWord();
expect(wordToGuess).toBe('aberrant');
});
it('should call buildPlaceholders', function() {
getNewWord();
expect(buildPlaceholders).toHaveBeenCalled();
});
});
describe('buildPlaceholders()', function() {
beforeEach(function() {
spyOn(document, 'getElementById').and.callThrough();
var wordDiv = document.createElement('div');
wordDiv.id = "word";
document.body.appendChild(wordDiv);
});
afterEach(function() {
document.body.removeChild(document.getElementById('word'));
});
it('should call document.getElementById for word', function() {
buildPlaceholders();
expect(document.getElementById).toHaveBeenCalledWith('word');
});
it('should add an element for each letter in wordToGuess', function() {
wordToGuess = 'apple';
buildPlaceholders();
var placeholdersDiv = document.getElementById('word');
expect(placeholdersDiv.innerHTML.length).toBe(5);
});
it('should add an underscore for each letter in wordToGuess', function() {
wordToGuess = 'apple';
buildPlaceholders();
var placeholdersDiv = document.getElementById('word');
expect(placeholdersDiv.innerHTML[0]).toBe('_');
});
});
describe('buildAlphabetDisplay()', function() {
beforeEach(function() {
spyOn(window, 'buildAlphabetArray');
spyOn(document, 'getElementById').and.callThrough();
spyOn(document, 'createDocumentFragment').and.callThrough();
spyOn(window, 'buildSingleLetter').and.callThrough();;
var lettersDiv = document.createElement('div');
lettersDiv.id = "letters";
document.body.appendChild(lettersDiv);
});
afterEach(function() {
document.body.removeChild(document.getElementById('letters'));
});
it('should call buildAlphabetArray', function() {
buildAlphabetDisplay();
expect(window.buildAlphabetArray).toHaveBeenCalled();
});
it('should call document.getElementById for letters', function() {
buildAlphabetDisplay();
expect(document.getElementById).toHaveBeenCalledWith('letters');
});
it('should call buildSingleLetter once for each letter in alphabet', function() {
buildAlphabetDisplay();
var lettersDiv = document.getElementById('letters');
// this expectation is to verify that the function (buildSingleLetter) was called exactly 26 times
expect(window.buildSingleLetter.calls.count()).toEqual(26);
});
it('should pass each letter in alphabet once to buildSingleLetter', function() {
buildAlphabetDisplay();
var lettersDiv = document.getElementById('letters');
expect(window.buildSingleLetter.calls.allArgs()).toEqual([['A'],['B'],['C'],['D'],['E'],['F'],['G'],['H'],['I'],['J'],['K'],['L'],['M'],['N'],['O'],['P'],['Q'],['R'],['S'],['T'],['U'],['V'],['W'],['X'],['Y'],['Z']]);
});
it('should call document.createDocumentFragment', function() {
buildAlphabetDisplay();
expect(document.createDocumentFragment).toHaveBeenCalled();
});
it('should add a div for each letter', function() {
buildAlphabetDisplay();
expect(document.getElementById('letters').children.length).toBe(26);
});
});
describe('buildSingleLetter()', function() {
beforeEach(function() {
spyOn(document, 'createElement').and.callThrough();
});
it('should call document.createElement', function() {
buildSingleLetter();
expect(document.createElement).toHaveBeenCalled();
});
it('should set cursor style to pointer', function() {
var div = buildSingleLetter('A');
expect(div.style.cursor).toBe('pointer');
});
it('should set innerHTML to letter passed', function() {
var div = buildSingleLetter('A');
expect(div.innerHTML).toBe('A');
});
it('should set onclick event', function() {
var div = buildSingleLetter('A');
expect(div.onclick).not.toBe(null);
});
});
describe('evaluateGuess()', function() {
beforeEach(function() {
var letterDiv = document.createElement('div');
letterDiv.id = 'A';
letterDiv.innerHTML = 'A';
letterDiv.style.cursor = 'pointer';
document.body.appendChild(letterDiv);
spyOn(document, 'getElementById').and.returnValue(letterDiv);
spyOn(window, 'checkForGuessedLetter');
});
afterEach(function() {
document.body.removeChild(document.getElementById('A'));
});
it('should call checkForGuessedLetter', function() {
evaluateGuess();
expect(window.checkForGuessedLetter).toHaveBeenCalled();
});
it('should set innerHTML of clicked element to non-breaking space', function() {
var letterDiv = document.getElementById('A');
evaluateGuess();
expect(letterDiv.innerHTML).toBe(' ');
});
it('should set cursor style of clicked element to default', function() {
var letterDiv = document.getElementById('A');
evaluateGuess();
expect(letterDiv.style.cursor).toBe('default');
});
it('should set onclick event to null', function() {
var letterDiv = document.getElementById('A');
letterDiv.onclick = function() {};
evaluateGuess();
expect(letterDiv.onclick).toBe(null);
});
});
describe('checkForGuessedLetter()', function() {
beforeEach(function() {
spyOn(document, 'getElementById').and.callThrough();
// we can spy on pretty much anything (I haven't found something I wasn't able to spy on),
// including JavaScript prototype functions like string.split()...
spyOn(String.prototype, 'split').and.callThrough();
spyOn(window, 'draw');
// and Array.indexOf()
spyOn(Array.prototype, 'indexOf').and.callThrough();
var wordDiv = document.createElement('div');
wordDiv.id = "word";
wordDiv.innerHTML = '______';
document.body.appendChild(wordDiv);
wordToGuess = 'Applea';
});
afterEach(function() {
document.body.removeChild(document.getElementById('word'));
});
it('should call document.getElementById', function() {
checkForGuessedLetter('A');
expect(document.getElementById).toHaveBeenCalledWith('word');
});
it('should split string into array', function() {
checkForGuessedLetter('A');
expect(String.prototype.split).toHaveBeenCalled();
});
it('should call Array.indexOf', function() {
checkForGuessedLetter('A');
expect(Array.prototype.indexOf).toHaveBeenCalledWith('A');
});
it('should call draw when letter is not in word', function() {
wordToGuess = 'Apple';
checkForGuessedLetter('Z');
expect(window.draw).toHaveBeenCalled();
});
it('should not call draw when letter is in word', function() {
wordToGuess = 'Apple';
checkForGuessedLetter('A');
expect(window.draw).not.toHaveBeenCalled();
});
it('should replace all underscores with letter when letter matches', function() {
checkForGuessedLetter('A');
var wordDiv = document.getElementById('word');
expect(wordDiv.innerHTML).toBe('A____a');
});
it('should increment badGuesses by one when letter is not found', function() {
badGuesses = 1;
checkForGuessedLetter('Z');
expect(badGuesses).toBe(2);
});
it('should not increment badGuesses when letter is found', function() {
badGuesses = 1;
checkForGuessedLetter('A');
expect(badGuesses).toBe(1);
});
it('should increment correctGuesses when letter is found', function() {
correctGuesses = 1;
checkForGuessedLetter('A');
expect(correctGuesses).toBe(3);
});
it('should not increment correctGuesses when letter is not found', function() {
correctGuesses = 1;
checkForGuessedLetter('Z');
expect(correctGuesses).toBe(1);
});
});
describe('draw()', function() {
var passedContext, passedStart, passedEnd;
beforeEach(function() {
spyOn(document, 'getElementById').and.callThrough();
spyOn(window, 'showResult');
spyOn(HTMLCanvasElement.prototype, 'getContext').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'lineTo').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'stroke').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'beginPath').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'moveTo').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'arc').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'fillText').and.callThrough();
// here we're specifying that when the drawLine function is called we invoke
// an entirely different, anonymous function
spyOn(window, 'drawLine').and.callFake(function(context, start, end) {
passedContext = context;
passedStart = start;
passedEnd = end;
});
var canvas = document.createElement('canvas');
canvas.id = "canvas";
document.body.appendChild(canvas);
var letters = document.createElement('div');
letters.id = "letters";
letters.innerHTML = 'placeholder text';
document.body.appendChild(letters);
wordToGuess = 'Apple';
badGuesses = 0;
correctGuesses = 0;
passedContext = null;
passedStart = [0,0];
passedEnd = [0,0];
});
afterEach(function() {
document.body.removeChild(document.getElementById('canvas'));
document.body.removeChild(document.getElementById('letters'));
});
it('should call document.getElementById', function() {
draw();
expect(document.getElementById).toHaveBeenCalledWith('canvas');
});
it('should call HTMLCanvasElement.getContext', function() {
draw();
expect(HTMLCanvasElement.prototype.getContext).toHaveBeenCalledWith('2d');
});
it('should set line color to black', function() {
draw();
expect(passedContext.fillStyle).toBe('#a52a2a');
});
it('should set line width to 10', function() {
draw();
expect(passedContext.lineWidth).toBe(10);
});
it('should call drawLine', function() {
draw();
expect(window.drawLine).toHaveBeenCalled();
});
it('should call drawLine twice when one bad guess has been made', function() {
badGuesses = 1;
draw();
expect(window.drawLine.calls.count()).toBe(2);
});
it('should pass in coordinates to start gallow pole when one bad guess has been made', function() {
badGuesses = 1;
draw();
// this expectation is checking the arguments passed to the most recent call
// to the drawLine function
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([30,185]);
});
it('should pass in coordinates to end gallow pole when one bad guess has been made', function() {
badGuesses = 1;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([30,10]);
});
it('should draw gallow arm when two bad guesses have been made', function() {
badGuesses = 2;
draw();
expect(CanvasRenderingContext2D.prototype.lineTo).toHaveBeenCalled();
expect(CanvasRenderingContext2D.prototype.stroke).toHaveBeenCalled();
});
it('should call drawLine three times when three bad guesses have been made', function() {
badGuesses = 3;
draw();
expect(window.drawLine.calls.count()).toBe(3);
});
it('should pass in coordinates to start noose when three bad guesses have been made', function() {
badGuesses = 3;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,15]);
});
it('should pass in coordinates to end noose when three bad guesses have been made', function() {
badGuesses = 3;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([145,30]);
});
it('should draw head when three bad guesses have been made', function() {
badGuesses = 3;
draw();
// although most testing experts consider testing multiple expectations in a single
// spec to be bad form, this is one of the situations where it didn't make sense to me to break it out into
// 11 separate tests
expect(CanvasRenderingContext2D.prototype.beginPath.calls.count()).toBe(1);
expect(CanvasRenderingContext2D.prototype.moveTo.calls.count()).toBe(1);
expect(CanvasRenderingContext2D.prototype.moveTo.calls.mostRecent().args[0]).toBe(160);
expect(CanvasRenderingContext2D.prototype.moveTo.calls.mostRecent().args[1]).toBe(45);
expect(CanvasRenderingContext2D.prototype.arc.calls.count()).toBe(1);
expect(CanvasRenderingContext2D.prototype.arc.calls.mostRecent().args[0]).toBe(145);
expect(CanvasRenderingContext2D.prototype.arc.calls.mostRecent().args[1]).toBe(45);
expect(CanvasRenderingContext2D.prototype.arc.calls.mostRecent().args[2]).toBe(15);
expect(CanvasRenderingContext2D.prototype.arc.calls.mostRecent().args[3]).toBe(0);
expect(CanvasRenderingContext2D.prototype.arc.calls.mostRecent().args[4]).toBe((Math.PI/180)*360);
expect(CanvasRenderingContext2D.prototype.stroke.calls.count()).toBe(2);
});
it('should call drawLine four times when four bad guesses have been made', function() {
badGuesses = 4;
draw();
expect(window.drawLine.calls.count()).toBe(4);
});
it('should pass in coordinates to start body when four bad guesses have been made', function() {
badGuesses = 4;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,60]);
});
it('should pass in coordinates to end body when four bad guesses have been made', function() {
badGuesses = 4;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([145,130]);
});
it('should call drawLine five times when five bad guesses have been made', function() {
badGuesses = 5;
draw();
expect(window.drawLine.calls.count()).toBe(5);
});
it('should pass in coordinates to start left arm when five bad guesses have been made', function() {
badGuesses = 5;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,80]);
});
it('should pass in coordinates to end left arm when five bad guesses have been made', function() {
badGuesses = 5;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([110,90]);
});
it('should call drawLine six times when six bad guesses have been made', function() {
badGuesses = 6;
draw();
expect(window.drawLine.calls.count()).toBe(6);
});
it('should pass in coordinates to start right arm when six bad guesses have been made', function() {
badGuesses = 6;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,80]);
});
it('should pass in coordinates to end right arm when six bad guesses have been made', function() {
badGuesses = 6;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([180,90]);
});
it('should call drawLine seven times when seven bad guesses have been made', function() {
badGuesses = 7;
draw();
expect(window.drawLine.calls.count()).toBe(7);
});
it('should pass in coordinates to start left leg when seven bad guesses have been made', function() {
badGuesses = 7;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,130]);
});
it('should pass in coordinates to end left leg when seven bad guesses have been made', function() {
badGuesses = 7;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([130,170]);
});
it('should call drawLine eight times when eight bad guesses have been made', function() {
badGuesses = 8;
draw();
expect(window.drawLine.calls.count()).toBe(8);
});
it('should pass in coordinates to start right leg when eight bad guesses have been made', function() {
badGuesses = 8;
draw();
expect(window.drawLine.calls.mostRecent().args[1]).toEqual([145,130]);
});
it('should pass in coordinates to end right leg when eight bad guesses have been made', function() {
badGuesses = 8;
draw();
expect(window.drawLine.calls.mostRecent().args[2]).toEqual([160,170]);
});
it('should call fillText with Game Over when eight bad guesses have been made', function() {
badGuesses = 8;
draw();
expect(CanvasRenderingContext2D.prototype.fillText).toHaveBeenCalledWith('Game Over!', 45, 110);
});
it('should clear alphabet when eight bad guesses have been made', function() {
badGuesses = 8;
draw();
var letters = document.getElementById('letters');
expect(letters.innerHTML).toBe('');
});
it('should clear alphabet when word has been guessed correctly', function() {
correctGuesses = wordToGuess.length;
draw();
var letters = document.getElementById('letters');
expect(letters.innerHTML).toBe('');
});
it('should call fillText with You Won when word has been guessed correctly', function() {
correctGuesses = wordToGuess.length;
draw();
expect(CanvasRenderingContext2D.prototype.fillText).toHaveBeenCalledWith('You Won!', 45, 110);
});
});
describe('init()', function() {
beforeEach(function() {
var loading = document.createElement('p');
loading.id = 'loading';
document.body.appendChild(loading);
var play = document.createElement('div');
play.id = 'play';
play.style.display = 'none';
play.onclick = null;
document.body.appendChild(play);
var clear = document.createElement('div');
clear.id = 'clear';
clear.style.display = 'none';
clear.onclick = null;
document.body.appendChild(clear);
var help = document.createElement('div');
help.id = 'help';
help.onclick = null;
help.style.display = 'none';
document.body.appendChild(help);
var helpText = document.createElement('div');
helpText.id = 'helpText';
helpText.style.display = 'none';
document.body.appendChild(helpText);
var close = document.createElement('div');
close.id = 'close';
close.onclick = null;
close.style.display = 'none';
document.body.appendChild(close);
});
afterEach(function() {
document.body.removeChild(document.getElementById('loading'));
document.body.removeChild(document.getElementById('play'));
document.body.removeChild(document.getElementById('clear'));
document.body.removeChild(document.getElementById('help'));
document.body.removeChild(document.getElementById('helpText'));
document.body.removeChild(document.getElementById('close'));
});
it('should hide loading div', function() {
init();
expect(document.getElementById('loading').style.display).toBe('none');
});
it('should show play div', function() {
init();
expect(document.getElementById('play').style.display).toBe('inline-block');
});
it('should show clear div', function() {
init();
expect(document.getElementById('clear').style.display).toBe('inline-block');
});
it('should set onclick event of help div', function() {
init();
var help = document.getElementById('help');
expect(help.onclick).not.toBe(null);
});
it('should set onclick event of close help div', function() {
init();
var close = document.getElementById('close');
expect(close.onclick).not.toBe(null);
});
});
describe('showHelp()', function() {
beforeEach(function() {
spyOn(document.body, 'appendChild').and.callThrough();
var help = document.createElement('div');
help.id = 'help';
help.onclick = null;
help.style.display = 'none';
document.body.appendChild(help);
var helpText = document.createElement('div');
helpText.id = 'helpText';
helpText.style.display = 'none';
document.body.appendChild(helpText);
});
afterEach(function() {
document.body.removeChild(document.getElementById('helpText'));
document.body.removeChild(document.getElementById('help'));
document.body.removeChild(document.getElementById('mask'));
});
it('should append mask div to body', function() {
showHelp();
expect(document.body.appendChild.calls.count()).toBe(3);
});
it('should display helpText div', function() {
showHelp();
expect(document.getElementById('helpText').style.display).toBe('block');
});
});
describe('closeHelp()', function() {
beforeEach(function() {
var close = document.createElement('div');
close.id = 'close';
close.onclick = null;
close.style.display = 'none';
document.body.appendChild(close);
var mask = document.createElement('div');
mask.id = 'mask';
document.body.appendChild(mask);
});
afterEach(function() {
document.body.removeChild(document.getElementById('close'));
});
it('should remove mask from body', function() {
closeHelp();
var mask = document.getElementById('mask');
expect(mask).toBe(null);
});
});
describe('showResult()', function() {
beforeEach(function() {
spyOn(document, 'getElementById').and.callThrough();
spyOn(String.prototype, 'split').and.callThrough();
spyOn(Array.prototype, 'join').and.callThrough();
var wordDiv = document.createElement('div');
wordDiv.id = "word";
wordDiv.innerHTML = 'a__l_';
document.body.appendChild(wordDiv);
showResult();
});
afterEach(function() {
document.body.removeChild(document.getElementById('word'));
});
it('should call document.getElementById', function() {
expect(document.getElementById).toHaveBeenCalledWith('word');
});
it('should call String.split', function() {
expect(String.prototype.split).toHaveBeenCalledWith('');
});
it('should call Array.join', function() {
expect(Array.prototype.join).toHaveBeenCalledWith('');
});
it('should replace all underscores with their letter', function() {
var wordDiv = document.getElementById('word');
expect(wordDiv.innerHTML).toBe('a<span style="color:red">P</span><span style="color:red">P</span>l<span style="color:red">E</span>');
});
});
describe('drawLine()', function() {
var context;
beforeEach(function() {
spyOn(CanvasRenderingContext2D.prototype, 'beginPath').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'moveTo').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'lineTo').and.callThrough();
spyOn(CanvasRenderingContext2D.prototype, 'stroke').and.callThrough();
var canvas = document.createElement('canvas');
canvas.id = "canvas";
document.body.appendChild(canvas);
context = canvas.getContext('2d');
drawLine(context, [145,15], [145,30]);
});
afterEach(function() {
document.body.removeChild(document.getElementById('canvas'));
});
it('should invoke context.beginPath', function() {
expect(CanvasRenderingContext2D.prototype.beginPath).toHaveBeenCalled();
});
it('should invoke context.moveTo', function() {
expect(CanvasRenderingContext2D.prototype.moveTo).toHaveBeenCalledWith(145, 15);
});
it('should invoke context.lineTo', function() {
expect(CanvasRenderingContext2D.prototype.lineTo).toHaveBeenCalledWith(145, 30);
});
it('should invoke context.beginPath', function() {
expect(CanvasRenderingContext2D.prototype.stroke).toHaveBeenCalled();
});
});
});
The code:
var alphabet = [];
var badGuesses, correctGuesses;
var wordToGuess = '';
var wordArray = new Array('abate','aberrant','abscond','accolade','acerbic','acumen','adulation','adulterate','aesthetic','aggrandize','alacrity','alchemy','amalgamate','ameliorate','amenable','anachronism','anomaly','approbation','archaic','arduous','ascetic','assuage','astringent','audacious','austere','avarice','aver','axiom','bolster','bombast','bombastic','bucolic','burgeon','cacophony','canon','canonical','capricious','castigation','catalyst','caustic','censure','chary','chicanery','cogent','complaisance','connoisseur','contentious','contrite','convention','convoluted','credulous','culpable','cynicism','dearth','decorum','demur','derision','desiccate','diatribe','didactic','dilettante','disabuse','discordant','discretion','disinterested','disparage','disparate','dissemble','divulge','dogmatic','ebullience','eccentric','eclectic','effrontery','elegy','eloquent','emollient','empirical','endemic','enervate','enigmatic','ennui','ephemeral','equivocate','erudite','esoteric','eulogy','evanescent','exacerbate','exculpate','exigent','exonerate','extemporaneous','facetious','fallacy','fawn','fervent','filibuster','flout','fortuitous','fulminate','furtive','garrulous','germane','glib','grandiloquence','gregarious','hackneyed','halcyon','harangue','hedonism','hegemony','heretical','hubris','hyperbole','iconoclast','idolatrous','imminent','immutable','impassive','impecunious','imperturbable','impetuous','implacable','impunity','inchoate','incipient','indifferent','inert','infelicitous','ingenuous','inimical','innocuous','insipid','intractable','intransigent','intrepid','inured','inveigle','irascible','laconic','laud','loquacious','lucid','luminous','magnanimity','malevolent','malleable','martial','maverick','mendacity','mercurial','meticulous','misanthrope','mitigate','mollify','morose','mundane','nebulous','neologism','neophyte','noxious','obdurate','obfuscate','obsequious','obstinate','obtuse','obviate','occlude','odious','onerous','opaque','opprobrium','oscillation','ostentatious','paean','parody','pedagogy','pedantic','penurious','penury','perennial','perfidy','perfunctory','pernicious','perspicacious','peruse','pervade','pervasive','phlegmatic','pine','pious','pirate','pith','pithy','placate','platitude','plethora','plummet','polemical','pragmatic','prattle','precipitate','precursor','predilection','preen','prescience','presumptuous','prevaricate','pristine','probity','proclivity','prodigal','prodigious','profligate','profuse','proliferate','prolific','propensity','prosaic','pungent','putrefy','quaff','qualm','querulous','query','quiescence','quixotic','quotidian','rancorous','rarefy','recalcitrant','recant','recondite','redoubtable','refulgent','refute','relegate','renege','repudiate','rescind','reticent','reverent','rhetoric','salubrious','sanction','satire','sedulous','shard','solicitous','solvent','soporific','sordid','sparse','specious','spendthrift','sporadic','spurious','squalid','squander','static','stoic','stupefy','stymie','subpoena','subtle','succinct','superfluous','supplant','surfeit','synthesis','tacit','tenacity','terse','tirade','torpid','torque','tortuous','tout','transient','trenchant','truculent','ubiquitous','unfeigned','untenable','urbane','vacillate','variegated','veracity','vexation','vigilant','vilify','virulent','viscous','vituperate','volatile','voracious','waver','zealous');
function buildAlphabetArray() {
alphabet = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'];
}
function buildAlphabetDisplay() {
buildAlphabetArray();
var letters = document.getElementById('letters');
var fragment = document.createDocumentFragment();
letters.innerHTML = '';
for(var i = 0; i < alphabet.length; i++) {
var div = buildSingleLetter(alphabet[i]);
div.id = alphabet[i];
fragment.appendChild(div);
}
letters.appendChild(fragment);
}
function buildPlaceholders() {
var word = document.getElementById('word');
word.innerHTML = '';
for(var i = 0; i < wordToGuess.length; i++){
word.innerHTML += '_';
}
}
function buildSingleLetter(letter) {
var div = document.createElement('div');
div.style.cursor = 'pointer';
div.innerHTML = letter;
div.onclick = evaluateGuess;
return div;
}
function checkForGuessedLetter(letter) {
var placeholders = document.getElementById('word').innerHTML;
// split the placeholders into an array
placeholders = placeholders.split('');
var letterArray = wordToGuess.split('');
if (letterArray.indexOf(letter) === -1 && letterArray.indexOf(letter.toLowerCase()) === -1) {
badGuesses++;
draw();
} else {
for (var i = 0; i < placeholders.length; i++) {
if (wordToGuess.charAt(i).toLowerCase() == letter.toLowerCase()) {
placeholders[i] = wordToGuess.charAt(i);
correctGuesses++;
}
}
if (correctGuesses === wordToGuess.length) {
draw();
}
}
word.innerHTML = placeholders.join('');
}
function closeHelp() {
document.body.removeChild(document.getElementById('mask'));
}
function draw() {
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.lineWidth = 10;
context.fillStyle = 'brown';
// draw the ground
drawLine(context, [20,190], [180,190]);
if (badGuesses > 0) {
drawLine(context, [30,185], [30,10]);
if (badGuesses > 1) {
context.lineTo(150, 10);
context.stroke();
}
if (badGuesses > 2) {
// draw rope
drawLine(context, [145,15], [145,30]);
// draw head
context.beginPath();
context.moveTo(160, 45);
context.arc(145, 45, 15, 0, (Math.PI/180)*360);
context.stroke();
}
if (badGuesses > 3) {
// draw body
drawLine(context, [145,60], [145,130]);
}
if (badGuesses > 4) {
// draw left arm
drawLine(context, [145,80], [110,90]);
}
if (badGuesses > 5) {
// draw right arm
drawLine(context, [145,80], [180,90]);
}
if (badGuesses > 6) {
// draw left leg
drawLine(context, [145,130], [130,170]);
}
if (badGuesses > 7) {
// draw right leg
drawLine(context, [145,130], [160,170]);
// display game over message
context.fillText('Game Over!', 45, 110);
// clear alphabet
document.getElementById('letters').innerHTML = '';
setTimeout(showResult, 200);
}
}
if (correctGuesses == wordToGuess.length) {
document.getElementById('letters').innerHTML = '';
context.fillText('You Won!', 45,110);
}
}
function drawLine(context, from, to) {
context.beginPath();
context.moveTo(from[0], from[1]);
context.lineTo(to[0], to[1]);
context.stroke();
}
function evaluateGuess() {
var letter = document.getElementById(this.id);
checkForGuessedLetter(letter.innerHTML);
letter.innerHTML = ' ';
letter.style.cursor = 'default';
letter.onclick = null;
}
function getNewWord() {
var index = parseInt(Math.floor(Math.random() * wordArray.length));
wordToGuess = wordArray[index];
buildPlaceholders();
}
function init() {
document.getElementById('loading').style.display = 'none';
document.getElementById('play').style.display = 'inline-block';
document.getElementById('clear').style.display = 'inline-block';
document.getElementById('help').onclick = showHelp;
document.getElementById('close').onclick = closeHelp;
document.getElementById('play').onclick = newGame;
}
function newGame() {
badGuesses = 0;
correctGuesses = 0;
getNewWord();
buildAlphabetDisplay();
var canvas = document.getElementById('canvas');
canvas.width = canvas.width;
}
function showHelp() {
var mask = document.createElement('div');
mask.id = 'mask';
document.body.appendChild(mask);
document.getElementById('helpText').style.display = 'block';
}
// When the game is over, display missing letters in red
function showResult() {
var word = document.getElementById('word');
var placeholders = word.innerHTML;
placeholders = placeholders.split('');
for (i = 0; i < placeholders.length; i++) {
if (placeholders[i] == '_') {
placeholders[i] = '<span style="color:red">' + wordToGuess.charAt(i).toUpperCase() + '</span>';
}
}
word.innerHTML = placeholders.join('');
}
The markup:
<!DOCTYPE HTML>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>Hangman</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="styles/hangman.css" rel="stylesheet" type="text/css">
<script src="src/hangman.js"></script>
</head>
<body>
<h1>Hangman</h1>
<div id="help"></div>
<div id="helptext">
<h2>How to Play</h2>
<div id="close"></div>
<p>Hangman is a word-guessing game. Click or tap New Game to display the letters of the alphabet and a row of dashes indicating the number of letters to be guessed. Click or tap a letter. If it's in the word, it replaces the dash(es). Each wrong guess results in a stroke being added to a gallows and its victim. Your role is to guess the word correctly before the victim meets his grisly fate.</p>
</div>
<p id="loading">Game loading. . .</p>
<canvas id="canvas" width="200" height="200">Sorry, your browser needs to support canvas for this game.</canvas>
<div id="play">New Game</div> <div id="clear">Clear Score</div>
<p id="word"></p>
<div id="letters"></div>
<script>
init();
</script>
</body>
</html>
And now you have a working, fully tested, pure JavaScript/HTML version of Hangman.
h/t to David Powers at adobe.com for doing Hangman first. I used a lot of what he wrote (including his CSS), but modified it to be TDD and pure JavaScript.