Blackjack in AngularJs (reprise)

After the first implementation of the game, my friend and I compared our versions.
He focused on the deck model instead of the dealer like I did.

This simple choice drove his design to a complete different path.
He put a lot of effort trying to replicate a realistic deck, implementing the shuffle logic and making impossible to draw the same card twice.

This hypotheses never came up to my mind because I knew that in the real game there are multiple decks in order to make almost impossible for the players the calculation of the drawn cards.

I thought that if the real world would allow the existence of an endless deck, it would have been adopted by every Casino for sure.

Leaving aside my speculation about the need to create a realistic deck, I have to admit that I didn’t implement the deck at all, so the look and feel of my friend’s implementation was far better.
To solve the exercise the deck was for me a detail, in fact it was possible to play a correct game in my implementation; I wonder if the interviewer would have agreed with that.

The Deck model

In order to move further the implementation I started defining the deck, so I wrote my test.

it('should be possible to draw a card', function () {
	for (var i = 0; i < 10000; i++) {
		var card = deck.draw();
		expect(card.value).toBeGreaterThan(0);
		expect(card.value).toBeLessThan(14);
		expect(card.seed).toBeGreaterThan(0);
		expect(card.seed).toBeLessThan(5);
	}
});

Every time an application is based on some random logic there is always a chance to end up in an unexpected behavior.
In my unit test I try to mitigate the problem looping multiple times the algorithm and its expectations.
In this case the randomness of the algorithm was very simple and in a short range as well, so I decided that 10000 iterations could be enough.

The deck was a simple object with a function “draw” that returned an object with only two properties: value and seed.
The value had to be in a range between 1 to 13 in order to represent the face cards as well.
The 11, 12, 13 represented the face cards.
The seed had to be in a range between 1 to 4 to represent at least the classic poker’s deck seeds (diamonds, hearths, clubs, spades).

Mocking the Deck dependency

The second step was to give the dealer a dependency to the deck.
In order to ensure that, I had to change the dealerService test and verify that this dependency was used as expected.

...
...
var target, deckMock, spyOnDeckMock;

	deckMock = {
		draw: function (val) {
			return {
				value: val
			};
		}
	};
...
	beforeEach(function () {

		spyOnDeckMock = spyOn(deckMock, 'draw');
		module('blackjack.services', function ($provide) {
			$provide.factory('deck', function () {
				return deckMock;
			});
		});
...
...
});

	it('should draw a random card', function () {
		spyOnDeckMock.and.returnValue({ value: 4 });
		expect(target.giveCard()).toEqual(4);
		expect(deckMock.draw).toHaveBeenCalled();

	});

I created a mock version of the deck, I created a jasmine spy to manage it and I used it inside a factory called “deck”, so that the dependency could be resolved by angular.

In the previous implementation of the blackjack, to test the algorithm of the calculation I made possible to tell the dealer which card to draw, so now that the dealer had to use the deck object I went back to deck’s test and I added those two

it('should be possible to draw a specific card', function () {
	var card = deck.draw(4, 1);
	expect(card.value).toEqual(4);
	expect(card.seed).toEqual(1);
});

it('should be possible to draw a specific card without specify a seed', function () {
	var card = deck.draw(4);
	expect(card.value).toEqual(4);
	expect(card.seed).toBeGreaterThan(0);
	expect(card.seed).toBeLessThan(5);
});

Adding the deck to the dealerService in the two possible workflows (drawing a random card or a declared card) a lot of test failed excluding the first one that I changed to verify that the dealer was using the deck.
I had to change the test to reflect the new implementation of the dealerService.

/// <reference path="../../../../scripts/jasmine/jasmine.js" />
/// <reference path="../../../../scripts/jquery-2.1.3.min.js" />
/// <reference path="../../../../scripts/angular.js" />
/// <reference path="../../../../scripts/angular-mocks.js" />
/// <reference path="../../app/modules.js" />
/// <reference path="../../app/services/dealerService.js" />

describe('dealerService:', function () {
	var target, deckMock, spyOnDeckMock;

	deckMock = {
		draw: function (val) {
			return {
				value: val
			};
		}
	};

	beforeEach(function () {

		spyOnDeckMock = spyOn(deckMock, 'draw');
		module('blackjack.services', function ($provide) {
			$provide.factory('deck', function () {
				return deckMock;
			});
		});

		inject(function ($injector) {
			target = $injector.get('dealerService');
		});
	});

	it('should draw a random card', function () {
		spyOnDeckMock.and.returnValue({ value: 4 });
		expect(target.giveCard()).toBeGreaterThan(0);
		expect(deckMock.draw).toHaveBeenCalled();

	});

	it('should draw a random card with value between 1 and 10', function () {
		spyOnDeckMock.and.returnValue({ value: 5 });
		var card = target.giveCard();
		expect(card).toBeGreaterThan(0);
		expect(card).toBeLessThan(12);
		expect(deckMock.draw).toHaveBeenCalled();
	});

	it('should draw a specific card', function () {
		spyOnDeckMock.and.callThrough();
		expect(target.giveCard(2)).toBe(2);
		expect(deckMock.draw).toHaveBeenCalledWith(2);

	});

	it('should calculate an ace as 11', function () {
		spyOnDeckMock.and.callThrough();

		expect(target.giveCard(1)).toBe(11);
	});

	it('should draw a specific card in a range between 1 and 10', function () {
		spyOnDeckMock.and.callThrough();
		expect(function () { target.giveCard(14); }).toThrow(new RangeError());
		expect(function () { target.giveCard(0); }).toThrow(new RangeError());
		expect(function () { target.giveCard(1); }).not.toThrow(new RangeError());
		expect(function () { target.giveCard(13); }).not.toThrow(new RangeError());

	});

	it('should count the score', function () {
		spyOnDeckMock.and.callThrough();

		expect(target.giveCard(2)).toBe(2);
		expect(target.giveCard(6)).toBe(8);
	});

	it('should return 0 if the score goes over 21', function () {
		spyOnDeckMock.and.callThrough();

		expect(target.giveCard(2)).toBe(2);
		expect(target.giveCard(6)).toBe(8);
		expect(target.giveCard(6)).toBe(14);
		expect(target.giveCard(6)).toBe(20);
		expect(target.giveCard(6)).toBe(0);
	});

	it('should be possible start a new turn', function () {
		spyOnDeckMock.and.callThrough();

		expect(target.giveCard(2)).toBe(2);
		expect(target.newTurn()).toBe(0);
	});

	it('should be possible to get all the card drawn', function () {
		spyOnDeckMock.and.callThrough();

		target.giveCard(2);
		target.giveCard(4);
		var cards = target.getCards();
		expect(cards.length).toEqual(2);
		expect(cards[0].value).toEqual(2);
		expect(cards[1].value).toEqual(4);
	});

	it('the 1 should value 1 or 11 depending of the score without busting', function () {
		spyOnDeckMock.and.callThrough();

		expect(target.giveCard(1)).toBe(11);
		expect(target.giveCard(7)).toBe(18);
		expect(target.giveCard(1)).toBe(19);
		expect(target.giveCard(1)).toBe(20);

		target.newTurn();
		expect(target.giveCard(1)).toBe(11);
		expect(target.giveCard(6)).toBe(17);
		expect(target.giveCard(6)).toBe(13);

		target.newTurn();
		expect(target.giveCard(1)).toBe(11);
		expect(target.giveCard(1)).toBe(12);
		expect(target.giveCard(6)).toBe(18);
		expect(target.giveCard(6)).toBe(14);

		target.newTurn();
		expect(target.giveCard(1)).toBe(11);
		expect(target.giveCard(1)).toBe(12);
		expect(target.giveCard(1)).toBe(13);
		expect(target.giveCard(6)).toBe(19);

		target.newTurn();
		expect(target.giveCard(1)).toBe(11);
		expect(target.giveCard(6)).toBe(17);
		expect(target.giveCard(1)).toBe(18);
		expect(target.giveCard(1)).toBe(19);

	});
});

Implementing the Face cards’s calculation

Once my test passed again, I realized that the deck was able to draw the cards 11, 12, 13, so the dealer service had to cope with that.

In the blackjack game the face cards are worth 10.
So I added a new test.

it('should calculate face cards as 10 points', function() {
		spyOnDeckMock.and.callThrough();
		expect(target.giveCard(11)).toBe(10);
		target.newTurn();
		expect(target.giveCard(12)).toBe(10);
		target.newTurn();
		expect(target.giveCard(13)).toBe(10);
	});

Updating the other existing components

After to have solved the test I changed the test of the cardDirective to make it consider the seed and I left to the CSS the responsibility of the look and feel.

/// <reference path="../../../../scripts/jasmine/jasmine.js" />
/// <reference path="../../../../scripts/jquery-2.1.3.min.js" />
/// <reference path="../../../../scripts/angular.js" />
/// <reference path="../../../../scripts/angular-mocks.js" />
/// <reference path="../../app/modules.js" />
/// <reference path="../../app/directives/cardDirective.js" />
/// <reference path="../../app/views/directives/cardDirective.html" />

describe('cardDirective:', function() {
	var target, scope, compile;

	beforeEach(function () {
		module('blackjack.directives');
		inject(function ($rootScope, $templateCache, $compile) {
			compile = $compile;
			var view = $templateCache.get('/AppJs/v1.0/app/views/directives/cardDirective.html');
			if (!view) {
				$.ajax({
					async: false,
					url: '../../app/views/directives/cardDirective.html'
				}).done(function (data) {
					$templateCache.put('/AppJs/v1.0/app/views/directives/cardDirective.html', data);
				});
			}
			scope = $rootScope.$new();
			scope.value = 1;
			scope.seed = 1;

			target = $compile('<card value="value" seed="seed"></card>')(scope);
			scope.$digest();
		});
	});

	it('should be possible to draw it', function() {
		expect(target.hasClass('card-1')).toBeTruthy();
		expect(target.hasClass('card')).toBeTruthy();
		expect(target.find('div').html()).toEqual(scope.value.toString());
	});

	it('should be possible draw a card of every seed', function () {
		scope.seed = 1;
		var clubsCard = compile('<card value="value" seed="seed"></card>')(scope);
		scope.$digest();
		expect(clubsCard.hasClass('clubs')).toBeTruthy();

		scope.seed = 2;
		var diamondsCard = compile('<card value="value" seed="seed"></card>')(scope);
		scope.$digest();
		expect(diamondsCard.hasClass('diamonds')).toBeTruthy();

		scope.seed = 3;
		var heartsCard = compile('<card value="value" seed="seed"></card>')(scope);
		scope.$digest();
		expect(heartsCard.hasClass('hearts')).toBeTruthy();

		scope.seed = 4;
		var spadesCard = compile('<card value="value" seed="seed"></card>')(scope);
		scope.$digest();
		expect(spadesCard.hasClass('spades')).toBeTruthy();
	});
});

Then I changed the boardControllerTest to cope with the new card model returned by the deck.

	it('should be possible get a score when someone is busting', function() {
		spyOnGetCards.and.returnValue([
			{ value: 8 },
			{ value: 8 },
			{ value: 6 },
			{ value: 11 },
			{ value: 12 },
			{ value: 13 },
			{ value: 1 }]);

		askForBust();

		expect(scope.getPlayerScore()).toEqual(53);
		expect(scope.getDealerScore()).toEqual(53);
	});

After the implementation of the CSS and the sprites the look and feel was:

blackjack-1.1
E2E test

At the end I wrote two test using the Protractor.

describe('blackjack game', function () {
	it('should be possible to play a match passing immediately the turn and getting a result', function () {
		browser.get('http://localhost:1899/');

		var playerCards = element.all(by.repeater('card in playerCards'));
		var passBtn = element(by.css('.btn.btn-success'));

		expect(playerCards.count()).toEqual(2);

		passBtn.click();

		var playerScore = element(by.css('.player .score')).getText();
		var dealerScore = element(by.css('.dealer .score')).getText();

		playerScore.then(function (pS) {
			dealerScore.then(function (dS) {
				expect(parseInt(pS) <= parseInt(dS)).toBeTruthy();
				element(by.css('.game-over .text')).getText().then(function (endText) {
					if (dS > 21) {
						expect(endText).toEqual('YOU WIN');
					} else if (pS < dS) {
						expect(endText).toEqual('YOU LOSE');
					} else if (pS == dS) {
						expect(endText).toEqual('TIE: YOU LOSE');
					}
				});
			});
		});
	});

	it('should be possible to draw card till going to bust', function () {

		function goingToBust(playerScore, expectation) {
			playerScore.getText().then(function (ps) {
				if (parseInt(ps) <= 21) {
					drawBtn.click();
					expectedPlayerCards++;
					expect(playerCards.count()).toEqual(expectedPlayerCards);
					goingToBust(playerScore, expectation);
				} else {
					element(by.css('.game-over .text')).getText().then(function (endText) {
						expect(endText).toEqual(expectation);
					});
				}
			});

		}

		browser.get('http://localhost:1899/');

		var expectedPlayerCards = 2;
		var playerCards = element.all(by.repeater('card in playerCards'));
		var drawBtn = element(by.css('.btn.btn-danger'));

		expect(playerCards.count()).toEqual(expectedPlayerCards);

		drawBtn.click();
		expectedPlayerCards++;

		expect(playerCards.count()).toEqual(expectedPlayerCards);

		goingToBust(element(by.css('.player .score')), 'YOU BUST');
	});

});

On my github repository you can find the latest version that implements all these changes

Advertisements

One thought on “Blackjack in AngularJs (reprise)

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