The hangman in angularjs

Source Wikipedia:

The Hangman is a paper and pencil guessing game for two or more players. One player thinks of a word, phrase or sentence and the other tries to guess it by suggesting letters or numbers.

The word to guess is represented by a row of dashes, giving the number of letters, numbers and category. If the guessing player suggests a letter or number which occurs in the word, the other player writes it in all its correct positions. If the suggested letter or number does not occur in the word, the other player draws one element of a hanged man stick figure as a tally mark. The game is over when:

  • The guessing player completes the word, or guesses the whole word correctly
  • The other player completes the diagram:

This diagram is, in fact, designed to look like a hanging man. Although debates have arisen about the questionable taste of this picture,[1] it is still in use today. A common alternative for teachers is to draw an apple tree with ten apples, erasing or crossing out the apples as the guesses are used up.

The Arbiter

To realize the game I started designing the player who knows the secret word: the arbiterService.
I set up the test defining a sample word

describe('the arbiterService: ', function() {
	var target;
	beforeEach(function() {
		module('hangman.services');
		inject(function($injector) {
			target = $injector.get('arbiterService');
		});
		target.setWord("myword");
	});
...
...

And I wrote a simple test

	it('should be possible to get a word encrypted', function () {
		expect(target.getSecretWord()).toEqual("______");
	});

The point in the test was to retrieve the word set previously in an encrypted version with a _ for every single letter.

So I solved it in a way straightforward.

angular.module('hangman.services')
	.service('arbiterService', function () {
		var _word = null;
		this.setWord = function (word) {
			_word = word;
		};

		this.getSecretWord = function () {
			var ret = "";
			for (var i = 0, l = _word.length; i < l; i++) {
				ret += "_";
			}
			return ret;
		};

	});

It wasn’t bad, but I knew that it was better to can define more than just one word so I wrote another test

	it('should be possible to get a sentence encrypted', function () {
		target.setWord("my sentence");
		expect(target.getSecretWord()).toEqual("__ ________");

		target.setWord("my sentence more complicated");
		expect(target.getSecretWord()).toEqual("__ ________ ____ ___________");
	});

My previous version of the algorithm failed because it transformed every char in an underscore, so I had to change it slightly.

angular.module('hangman.services')
	.service('arbiterService', function () {
		var _word = null;
		this.setWord = function (word) {
			_word = word;
		};

		this.getSecretWord = function () {
			var ret = "";
			for (var i = 0, l = _word.length; i < l; i++) {
				if (_word[i] == " ") {
					ret += " ";
				} else {
					ret += "_";
				}
			}
			return ret;
		};

	});

The next test made things more interesting

	it('should be possible guess a letter', function () {
		target.try("m");
		expect(target.getSecretWord()).toEqual("m_____");
		target.try("d");
		expect(target.getSecretWord()).toEqual("m____d");
		target.try("q");
		expect(target.getSecretWord()).toEqual("m____d");
	});

I thought a lot about how to solve the test.
At the beginning I tried to add new logic to the existing one, but then I realized that was much easier to rethink entirely the logic in getting the secret word.
I introduced another private var named “_discoveredWord” that I initialized when the word to guess was set and to initialize it I used the same algorithm used in the getSecretWord.
I commented the new test to be sure that my refactoring passed the test already solved.

angular.module('hangman.services')
	.service('arbiterService', function () {
		var _word = null;
		var _discoveredWord = null;
		this.setWord = function (word) {
			_word = word;
			_discoveredWord = init();
		};

		this.getSecretWord = function () {
			return _discoveredWord;
		};

		function init() {
			var ret = "";
			for (var i = 0, l = _word.length; i < l; i++) {
				if (_word[i] == " ") {
					ret += " ";
				} else {
					ret += "_";
				}
			}
			return ret;
		}
	});

The test still passed, so I uncommented the last test and I made it pass

angular.module('hangman.services')
	.service('arbiterService', function () {
		var _word = null;
		var _discoveredWord = null;
		this.setWord = function (word) {
			_word = word;
			_discoveredWord = init();
		};

		this.getSecretWord = function () {
			return _discoveredWord;
		};

		this.try = function (letter) {
			var index = _word.indexOf(letter, 0);
			if (index !== -1) {
				discoverNewLetter(letter, index);
			}
		};

		function init() {
			var ret = "";
			for (var i = 0, l = _word.length; i < l; i++) {
				if (_word[i] == " ") {
					ret += " ";
				} else {
					ret += "_";
				}
			}
			return ret;
		}

		function discoverNewLetter(letter, index) {
			var tempArray = _discoveredWord.split("");
			tempArray.splice(index, 1, letter);

			_discoveredWord = tempArray.join("");
		}
	});

It passed.
In my first version the logic was completely in the try(), but I preferred to move the transformation logic in a private function named discoverNewLetter to improve the readability.

The next test was the toughest to solve

	it("should be possible pick a letter existing multiple times in the word", function() {
		target.setWord("mymyword");
		target.try("m");
		expect(target.getSecretWord()).toEqual("m_m_____");

		target.setWord("mymyworm");
		target.try("m");
		expect(target.getSecretWord()).toEqual("m_m____m");

	});

I knew that to solve the test I would have had to implement some recursive logic and, as usual, the recursion was a pain.
At the end I came out with this:

...
		this.try = function (letter, i) {
			var start = i || 0;
			var index = _word.indexOf(letter, start);
			if (index !== -1) {

				discoverNewLetter(letter, index);

				if (start <= _word.length) {
					this.try(letter, index + 1);
				}

			}
		};

...

It worked.
Just to be sure the algorithm was rock solid I added another test to double check it, but my expectations were to pass it without to change a single line of code

	it("should be possible pick a letter existing multiple times in a sentence", function () {
		target.setWord("my myword");
		target.try("m");
		expect(target.getSecretWord()).toEqual("m_ m_____");

		target.setWord("my my worm");
		target.try("m");
		expect(target.getSecretWord()).toEqual("m_ m_ ___m");

	});

It passed.
The next test was about the possibility to know if a try failed. In the hangman game you don’t have infinite try, so I knew I needed to be notified in case of failing.

	it("the try should notify if a letter is not found at least once", function () {
		target.setWord("my myword");
		
		expect(target.try("k")).toEqual(-1);
	});

I solved it very simply

		this.try = function (letter, i) {
			var start = i || 0;
			var index = _word.indexOf(letter, start);
			if (index !== -1) {

				discoverNewLetter(letter, index);

				if (start <= _word.length) {
					this.try(letter, index + 1);
				}

				return 1;
			}

			return -1;
		};

The test passed and the following one passed though without to add a single line of code.

	it("the try should notify if a letter is found at least once", function () {
		target.setWord("my myword");

		expect(target.try("m")).toEqual(1);
	});

The last test for my arbiter was about the notification that the word/sentence was entirely guessed

it("should be possible to know if the sentence has been discovered", function () {
		target.setWord("my my");

		expect(target.isGuessed()).toEqual(false);
		target.try("m");
		expect(target.isGuessed()).toEqual(false);
		target.try("y");
		expect(target.isGuessed()).toEqual(true);
	});

I solved it very straightforwardly.

		this.isGuessed = function () {
			return _discoveredWord === _word;
		};

The other components of the game

Even considering the arbiter the central object in the hangman game, there were a few things missing.

  • How the player had to input the letter for guessing the word
  • How the game had to show the status of the game to the player
  • How the game had to be instructed with the word to guess

The input

To solve the first problem I thought to put a simple html input text, but I changed my mind and I went for a funnier solution

keyboard

To achieve that result I created two directives: keyboard and key.
They were very simple and there is nothing worth to explain here, you can see the files in the project’s source available on my github.

The only thing that I want to mention is the inputManager.
In order to reuse the keyboard in other projects I decided to give it an attribute named inputManager: an expression to be called passing the key pressed.
So the keyboard itself doesn’t have any logic.

In the hangman game is the main controller who decides how to use the input.
In my implementation I passed the input to the try() function of the arbiter and I disabled the specific key so the user couldn’t click it multiple times, improving a bit the user experience.

keyboard-pressed

The Hangman

To solve the second problem I created another simple directive named “hangman” with a binding to the attributes “word” and “errors”.

Once again was up to the main controller to instruct this directive and update its content.

The main controller

The main controller itself wasn’t too complicated, it had a dependency on the arbiterService and the $element object in order to find and disable the keys on the keyboard.

Here its test:

	it('should have an input manager', function () {
		expect(scope.inputManager).toBeDefined();
	});

	it('should use the arbiter', function () {
		scope.inputManager({ value: 'a' });

		expect(mockArbiter.getSecretWord).toHaveBeenCalled();
		expect(mockArbiter.try).toHaveBeenCalledWith("a");
	});

	it('should track the errors', function () {
		spyOnArbiterTry.and.returnValue(-1);
		expect(scope.errors).toEqual(0);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(1);
	});

	it('should call the gameover if you make more than 4 errors', function () {
		spyOnArbiterTry.and.returnValue(-1);

		expect(scope.gameOver).toBe(false);
		expect(scope.errors).toEqual(0);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(1);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(2);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(3);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(4);
		scope.inputManager({ value: 'a' });
		expect(scope.errors).toEqual(5);
		expect(scope.gameOver).toBe(true);
		expect(scope.message).toEqual('YOU LOSE');
	});

	it('should call the gameover if you guess the word', function () {
		spyOnArbiterisGuessed.and.returnValue(false);

		expect(scope.gameOver).toBe(false);
		scope.inputManager({ value: 'a' });
		spyOnArbiterisGuessed.and.returnValue(true);
		scope.inputManager({ value: 'a' });
		expect(scope.gameOver).toBe(true);
		expect(scope.message).toEqual('YOU WIN');
		expect(mockArbiter.isGuessed).toHaveBeenCalled();
	});

And there the implementation

angular.module('hangman.controllers')
	.controller('mainCtrl', ['$scope', 'arbiterService', '$element'
		, function ($scope, arbiterService, $element) {
			$scope.word = arbiterService.getSecretWord();
			$scope.gameOver = false;
			$scope.message = "";
			$scope.errors = 0;
			$scope.inputManager = function (key) {
				var result = arbiterService.try(key.value);
				$element.find(key.id).prop('disabled', true);

				if (result === -1) {
					$scope.errors++;

					if ($scope.errors > 4) {
						$scope.gameOver = true;
						$scope.message = "YOU LOSE";
						$element.find('button').prop('disabled', true);
					}
				} else {
					if (arbiterService.isGuessed()) {
						$scope.gameOver = true;
						$scope.message = "YOU WIN";
						$element.find('button').prop('disabled', true);
					}
				}

				$scope.word = arbiterService.getSecretWord();
			};
		}]);

The final result

Putting in place all the components I got this

hangman-complete

And still there is a question: where did I set the word to guess?

Normally in the module.js I create the modules, set the dependencies between them and, like in this case, I configure a few things.

In this case I set few constants and I used the angular run() method to set the word defined in a constant in the same file and to bind the pressure of the keys of the real keyboard to the virtual one provided in the game.

here the code in the module.js

angular.module('hangman.directives', []);
angular.module('hangman.controllers', []);
angular.module('hangman.services', []);
angular.module('hangman', [
'hangman.services',
'hangman.directives',
'hangman.controllers'
]).constant('wordToGuess', 'whatever')
  .constant('keymapping', {
		"113": "q",
		"119": "w",
		"101": "e",
		"114": "r",
		"116": "t",
		"121": "y",
		"117": "u",
		"105": "i",
		"111": "o",
		"112": "p",
		"97": "a",
		"115": "s",
		"100": "d",
		"102": "f",
		"103": "g",
		"104": "h",
		"106": "j",
		"107": "k",
		"108": "l",
		"122": "z",
		"120": "x",
		"99": "c",
		"118": "v",
		"98": "b",
		"110": "n",
		"109": "m"
	}).run(['$document', 'keymapping', 'arbiterService', 'wordToGuess'
		, function ($document, keymapping, arbiterService, wordToGuess) {
			arbiterService.setWord(wordToGuess);
			$document.keypress(function (e) {
				var button = $('#k_' + keymapping[e.charCode]);
				if (!button.prop('disabled')) {
					$('#k_' + keymapping[e.charCode]).trigger('click');
				}
			});
		}]);

The reason of the check in the line 41 was because the trigger method in jquery executed the click even if the button was disabled, so in case by mistake you pressed multiple times a missing letter, it was counted all the times as an error.

What’s next?

The game can be improved in a lot of ways, but for sure I’d like to change the way is set now the word to guess.

  • You can make it choose by another player on the same pc
  • You can implement a logic to make to play two player remotely (signalR?)
  • You can make it pick the word randomly from some public service on the www.

And so on.
Also it wouldn’t be bad to put a visual hangman in place of the numbers!

Anyway I swear that in the next example with Angular I won’t use hardcoded data šŸ™‚

As usual you can find the source code on my github

p.s. Before to read the code try to guess my word šŸ˜›

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s