Blackjack in AngularJs

Speaking to a friend a couple of weeks ago, he told me about an interview he did where one of the challenges was to create the blackjack game.

The algorithm itself is not too complicated, but it’s a good test to practice TDD and having never tried it in AngularJs I decided to give it a shot.

The blackjack rules

A dealer competes against a variable number of players and every player competes only against the dealer.

The object of the game is beat the dealer.
Every player can ask cards to the dealer in order to obtain 21 and he can pass the turn when he wants.
If a player exceeds 21, he’s busting and the dealer wins.
Once the player passes the turn is the dealer’s turn.
The dealer can draw all the cards he wants in order to surpass the player score.
If the dealer exceeds 21, he’s busting and the player wins.
If there is a tie, the dealer wins.

To  play blackjack is used a various number of standard poker’s decks removing the jokers.
The face cards (jacks, queens, kings) are worth 10, any other card is worth its own number.
The ace can be calculated as 1 or 11, the calculation is based on the cards on the table, so asking cards it’s possible to recalculate any ace as 1 instead of 11, in order not going to bust.

How in TDD

Recalling the rules of the game, I found natural starting my test from the dealer.
The dealer is responsible drawing the cards and validate the score of every player and his own.

The dealerService

At the beginning I focused my tests on the ability of the dealer in calculating the score based on the cards on the table.

So in order I wrote:

  • It should draw a specific card.
  • It should draw a specific card in a range between 1 and 10
  • It should calculate an ace as 11.

Obviously in a real game the dealer should draw a random card, but to test the logic was useful to specify to the dealer which card to draw.

The following test were:

  • It should draw a random card
  • It should draw a random card with value between 1 and 11

In this way I secured that in a real game was possible to ask to the dealer random cards.

The next step was to ensure that for every card requested the dealer returned the overall score and not only the last card.

it('should count the score', function () {
		expect(target.giveCard(2)).toBe(2);
		expect(target.giveCard(6)).toBe(8);
	});

Then the dealer had to be able to call a busting:

  • It should return 0 if the score goes over 21

And so on:

  • It should be possible start a new turn
  • It should be possible to get all the card drawn

And I left for the last the most complicated:

  • the 1 should value 1 or 11 depending of the score avoiding the busting
	it('the 1 should value 1 or 11 depending of the score without busting', function () {
		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);

	});

Solving the dealer, the rest of the exercise was easy.

The cardDirective

I created a simple directive to represent the card.
Very simple, one test to be sure to have the information needed.

I wrote only:

  • It should be possible to draw it
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());
	});

The boardController

The board was a bit more complicated.
The board for me was the controller to manage the game and the user interactions.
The final result was:

Capture1

Starting from the test, in order:

  • It should be possible ask a card to the dealer
  • It should be possible to get the current score
  • the game should end when you bust
  • it should be possible get all the cards of the player
  • it should be possible get all the cards of the dealer
  • it should be possible to pass and let the dealer play till he wins or busting
  • it should be possible for the player to win the game
  • it should be possible for the player to lose the game
  • the dealer should win in case of tie
  • it should be possible get a score when someone is busting
  • it should be possible re-start a new game

Capture2

In these test I found more difficult to find a way to mock and test the dependencies in the correct way.
The controller itself had few things to do directly, the most of the time it had to ask to the dealer to do the job so I had to ensure that the mock had been called when it was needed and, given a mocked result, the controller did its job correctly.

Conclusion

I’m not 100% satisfied, but trying to resolve the game in a couple of hours I didn’t get much further.
To clarify: I didn’t write those tests sequentially, I jumped between my components, test by test trying to solve time by time the first new problem came up to my mind.

Here the test files:

/// <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;

	beforeEach(function () {
		module('blackjack.services');
		inject(function($injector) {
			target = $injector.get('dealerService');
		});
	});

	it('should draw a random card', function() {
		expect(target.giveCard() > 0).toBeTruthy();
	});

	it('should draw a random card with value between 1 and 11', function () {
		var card = target.giveCard();
		expect(card).toBeGreaterThan(0);
		expect(card).toBeLessThan(12);
	});

	it('should draw a specific card', function () {
		expect(target.giveCard(2)).toBe(2);
	});

	it('should calculate an ace as 11', function () {
		expect(target.giveCard(1)).toBe(11);
	});

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

	it('should count the score', function () {
		expect(target.giveCard(2)).toBe(2);
		expect(target.giveCard(6)).toBe(8);
	});

	it('should return 0 if the score goes over 21', function () {
		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 () {
		expect(target.giveCard(2)).toBe(2);
		expect(target.newTurn()).toBe(0);
	});

	it('should be possible to get all the card drawn', function () {
		target.giveCard(2);
		target.giveCard(4);
		var cards = target.getCards();
		expect(cards.length).toEqual(2);
		expect(cards[0]).toEqual(2);
		expect(cards[1]).toEqual(4);
	});

	it('the 1 should value 1 or 11 depending of the score without busting', function () {
		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);

	});

});
/// <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;

	beforeEach(function () {
		module('blackjack.directives');
		inject(function ($rootScope, $templateCache, $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;

			target = $compile('<card type="value"></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());
	});

});
/// <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/controllers/boardController.js" />

describe('boardController', function () {

	var target, scope, dealerService, spyOnGiveCard, spyOnGetCards, spyOnNewTurn;


	beforeEach(function () {
		module('blackjack.controllers');

		dealerService = {
			giveCard: function () {
			},
			newTurn: function () {
			},
			getCards: function() {
			}
		};

		inject(function ($controller, $rootScope) {
			scope = $rootScope.$new();
			target = $controller('boardController', { '$scope': scope, 'dealerService': dealerService });
		});

		spyOnGiveCard = spyOn(dealerService, 'giveCard');
		spyOnNewTurn = spyOn(dealerService, 'newTurn');
		spyOnGetCards = spyOn(dealerService, 'getCards');
	});

	function askForBust() {
		dealerService.giveCard = function () {
			return 0;
		};
		scope.getCard();
	}

	function makeThePlayerWin() {
		spyOnGiveCard.and.returnValue(21);
		scope.getCard();
	}

	function makeThePlayerLose() {
		spyOnGiveCard.and.returnValue(1);
		scope.getCard();
	}

	function setupDealerGame() {
		spyOnGiveCard.and.returnValue(10);

	}

	function makeDealerLose() {
		spyOnGiveCard.and.returnValue(0);
	}

	function makeDealerWin() {
		spyOnGiveCard.and.returnValue(21);
	}

	it('should be possible ask a card to the dealer', function () {
		spyOnGiveCard.and.returnValue(2);

		var card = scope.getCard();
		expect(card).toEqual(2);
		expect(dealerService.giveCard).toHaveBeenCalled();
	});

	it('should be possible to get the current score', function () {
		spyOnGiveCard.and.returnValue(2);
		scope.getCard();
		expect(scope.score).toEqual(2);
	});

	it('the game should end when you bust', function () {
		expect(scope.isBusted).toBeFalsy();
		askForBust();
		expect(scope.isBusted).toBeTruthy();
		expect(scope.gameIsOver).toBeTruthy();
		expect(scope.getResultMessage()).toEqual("YOU BUST");

	});

	it('should be possible get all the cards of the player', function () {
		spyOnGetCards.and.returnValue([1]);
		scope.getCard();
		expect(scope.playerCards.length).toEqual(1);
	});

	it('should be possible get all the cards of the dealer', function () {
		spyOnGetCards.and.returnValue([1]);

		scope.pass();
		expect(scope.dealerCards.length).toEqual(1);
	});

	it('should be possible re-start a new game', function () {
		spyOnGiveCard.and.returnValue(0);

		scope.start();
		expect(scope.gameIsStarted).toBeTruthy();
		expect(scope.gameIsOver).toBeFalsy();

		expect(scope.score).toEqual(0);
		expect(scope.isBusted).toBeFalsy();
		expect(scope.theDealerWon).toBeFalsy();
		expect(scope.dealerScore).toEqual(0);

		expect(dealerService.newTurn.calls.count()).toEqual(1);
	});

	it('should be possible to pass and let the dealer play till he wins or busting', function () {
		expect(scope.dealerScore).toEqual(0);
		spyOnGiveCard.and.returnValue(2);
		scope.getCard();
		expect(scope.gameIsOver).toBeFalsy();
		setupDealerGame();
		scope.pass();
		expect(scope.dealerScore).toEqual(10);
		expect(scope.theDealerWon).toBeTruthy();
		expect(scope.gameIsOver).toBeTruthy();
	});

	it('should be possible for the player to win the game', function () {
		expect(scope.dealerScore).toEqual(0);
		makeThePlayerWin();
		expect(scope.gameIsOver).toBeFalsy();
		makeDealerLose();
		scope.pass();
		expect(scope.theDealerWon).toBeFalsy();
		expect(scope.gameIsOver).toBeTruthy();
		expect(scope.getResultMessage()).toEqual("YOU WIN");
	});

	it('should be possible for the player to lose the game', function () {
		expect(scope.dealerScore).toEqual(0);
		makeThePlayerLose();
		expect(scope.gameIsOver).toBeFalsy();
		makeDealerWin();
		scope.pass();
		expect(scope.theDealerWon).toBeTruthy();
		expect(scope.gameIsOver).toBeTruthy();
		expect(scope.getResultMessage()).toEqual("YOU LOSE");

	});

	it('the dealer should win in case of tie', function () {
		expect(scope.dealerScore).toEqual(0);
		makeThePlayerWin();
		expect(scope.gameIsOver).toBeFalsy();
		makeDealerWin();
		scope.pass();
		expect(scope.theDealerWon).toBeTruthy();
		expect(scope.gameIsOver).toBeTruthy();
		expect(scope.getResultMessage()).toEqual("TIE: YOU LOSE");

	});

	it('should be possible get a score when someone is busting', function() {
		spyOnGetCards.and.returnValue([8, 8, 6]);
		askForBust();
		

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

Looking forward it would be interesting to implement a proper deck and add more players.

The source code of the project is available here on github

Advertisements

2 thoughts on “Blackjack in AngularJs

  1. Pingback: Blackjack in AngularJs (reprise) | Agile. Angular/Js. Asp.NET & TDD

  2. Pingback: TDD workshop | Agile. Angular/Js. Asp.NET & TDD

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