Angular Project Blackjack: 9 – Game Service

(This post is part of my “from scratch” AngularJS project. If you are feeling lost, the first post is here.)

Back to it

We’ve got a good setup now, with our gulp file building and running as necessary. Let’s move back to working on our code!

A Game Service

In our GameController we have the ability to deal a hand of cards but we can’t really do anything with it yet. To do so, we need to start putting the blackjack game rules into our application. Let’s create a GameService to start handling these rules for us. The main thing our GameService will do for now will be to take a hand of cards and return the numeric value of that hand. We can then use that value in our controller to enable/disable actions a user can take on the hand.

Here is our GameService:

(function(){
    'use strict';

    angular
        .module('blackjack.game')
        .factory('GameService', GameService);

    function GameService(){
        var service = {
            handValue: handValue
        };

        return service;

        ////////////////////


        function handValue(hand){
            var aces = 0;
            var totalValue = 0;
            var faceRanks = ['J','Q','K'];

            //Get the values of each card (counting 1 for each ace)
            hand.forEach(function(card){
                //Face Cards
                if(faceRanks.indexOf(card.rank) !== -1){
                    totalValue += 10;
                }
                //Aces
                else if(card.rank === 'A'){
                    totalValue += 1;
                    aces++;
                }
                //Number Cards
                else {
                    totalValue += Number(card.rank);
                }
            });

            //Loop through aces and try to add 10 to get highest value of hand
            // We are adding 10 here because we already added 1 for the ace above
            for(var i=0; i<aces; i++){
                if(totalValue <= 11){
                    totalValue += 10;
                }
            }

            return totalValue;
        }

    }
})();

Playing the Game

As I started writing the game playing portion of the GameController, a conversation I had about state based programming with @JoelMartinez kept popping into my head. When I broke it down into basics, our controller could be in one of four basic states:

  • Game with no player (started = false, canDeal = false, showResults = false)
  • Game with a player, nothing dealt (started = true, canDeal = true, showResults = false)
  • Game with a player, game in progress (started = true, canDeal = false, showResults = false)
  • Game with a player, game over (started = true, canDeal = true, showResults = true)

Breaking it down this way, you can see what your controller variables are and what they should be during each state. You can also easily see what you need to do to change states. Let’s update our GameController to handle these states:

(function(){
    'use strict';

    angular
        .module('blackjack.game')
        .controller('GameController',GameController);

    GameController.$inject = ['PlayerService', 'CardService', 'GameService'];

    function GameController(PlayerService, CardService, GameService){
        var game = this;

        /**
         * Our game can have multiple states:
         * 1) Game with no player (started = false, canDeal = false, showResults = false)
         * 2) Game with a player, nothing dealt (started = true, canDeal = true, showResults = false)
         * 3) Game with a player, game in progress (started = true, canDeal = false, showResults = false)
         * 4) Game with a player, game over (started = true, canDeal = true, showResults = true)
         */

        /**
         * Initialize our controller data
         */
        game.init = function () {
            game.maxValue = 21;
            game.canDeal = false;
            game.started = false;
            game.showResults = false;
            game.deck = CardService.newDeck();
        };

        /**
         * Starts a game by creating a new player
         */
        game.start = function () {
            game.player = PlayerService.newPlayer('Ringo', 100);
            game.started = true;
            game.canDeal = true;
            game.showResults = false;
        };

        /**
         * Deals a new hand by 'paying' from the score,
         * shuffles the deck, and deals two cards to the
         * player.
         */
        game.deal = function () {
            //Initialize values each game
            game.busted = false;
            game.started = true;
            game.canDeal = false;
            game.showResults = false;

            //Our bet defaults to 100
            game.player.changeScore(-100);

            //Shuffle before dealing
            game.deck.shuffle();

            //Empty our dealt card array
            game.playerCards = [];

            //Deal the cards
            game.hit();
            game.hit();

            //Calculate value of hand
            game.getHandValue();
        };

        /**
         * Adds a card to our hand and calculates value.
         */
        game.hit = function () {
            game.playerCards.push(game.deck.deal());
            game.getHandValue();
        };

        /**
         * Ends the game for the current hand. Checks for wins
         * and 'pays' to player score
         */
        game.end = function () {
            //Since we have no dealer, we win if we don't bust
            if(!game.busted) {
                game.player.changeScore(200);
                game.results = "YOU WON!";
            }
            else{
                game.results = "BUSTED";
            }
            game.canHit = false;
            game.canDeal = true;
            game.showResults = true;
        };

        /**
         * Resets our player's score and re-inits
         */
        game.reset = function () {
            game.player = null;
            game.init();
        };

        /**
         * Calculates value of player's hand via GameService
         * Determines if player can still hit or if busted.
         */
        game.getHandValue = function () {
            game.handValue = GameService.handValue(game.playerCards);
            game.canHit = game.handValue < game.maxValue;
            game.busted = game.handValue > game.maxValue;
            if(game.handValue >= game.maxValue){
                game.end();
            }
        };

        game.init();
    }
})();

As you can see, our first state is when the controller is called via init(). We move to the next state with the start() function. Once we deal() a hand, we are in the third state and we continue that way until end() is called. Our controller is now using our CardService to deal cards and our GameService to play the game. This really keeps our controller neat and makes our services reusable and testable. (Side note: I had a lot of internal debate on keeping the canHit and busted calculations insdie the GameController. In the end, I’ve left it there, but don’t be surprised if in another post or commit you see them moved into the GameService.)

Testing Injections

If you run our tests now, you will see a failure for a provider not found. This is due to the fact that our controller needs the GameService and CardService to function. Let’s go into our game.controller.spec.js file and inject those services (as well as write a few more tests).
Here is the top of our test:

describe('GameController Unit Tests', function () {
    var gameController, CardService, PlayerService;
    beforeEach(function () {
        module('blackjack.game');
        module('blackjack.player');
        module('blackjack.card');
        inject(function ($controller, _CardService_, _PlayerService_) {
            CardService = _CardService_;
            PlayerService = _PlayerService_;
            gameController = $controller('GameController', {
                CardService: CardService,
                PlayerService: PlayerService
            });
        });
    });

The biggest change you’ll see here is the inject(function ($controller, _CardService_, _PlayerService_) { line. We didn’t make a typo by putting underscores before and after our service names, this is a testing feature in AngularJS that allows us to get a localized copy of each service it is injecting into the test.

The line up top: var gameController, CardService, PlayerService;is what sets up our local variables for the instances. Now we can use those variables to allow us to do fun things like watch and mock functions of those services. For now, we’ll wait on writing tests with those advanced features and save it for another blog post.

Updating the Display

The last thing we need to do is to update our game directive view with all of our new GameController logic. We’re still using Bootstrap for our layout and we’ll divide our game display into four separate Bootstrap Panels:

  • Scoreboard
  • Card Table
  • Game Actions
  • Results

With the use of our state programming, we can hide or show each panel as needed with an ng-if statement on each panel. We are also using ng-disabled to control our action buttons based on state. Here is our game.directive.html:

<div>
    <!-- Scoreboard -->
    <div class="panel panel-default" ng-if="game.started">
        <div class="panel-heading">Player Info</div>
        <div class="panel-body">
            <div class="row">
                <div class="col-md-6">
                    Player: {{game.player.name}}
                </div>
                <div class="col-md-6">
                    Score: {{game.player.score}}
                </div>
            </div>
        </div>
    </div>

    <!-- Card Table -->
    <div class="panel panel-default">
        <div class="panel-heading">
            <div ng-if="game.started">
                Game In Progress
            </div>
            <div ng-if="!game.started">
                Press "Start" to play
            </div>
        </div>
        <div class="panel-body">
            <div class="row">
                <div class="col-md-12">
                    <div class="card" ng-repeat="card in game.playerCards">{{card.name()}}</div>
                </div>
            </div>
        </div>
        <div class="panel-footer" ng-if="game.started">
            <p>Hand Value: {{game.handValue}}</p>
        </div>
    </div>

    <!-- Game Actions -->
    <div class="panel panel-default">
        <div class="row" ng-if="game.started">
            <div class="col-md-6 col-md-offset-3">
                <div class="btn-group btn-group-justified">
                    <div class="btn-group" role="group">
                        <button class="btn btn-primary" ng-click="game.deal()" ng-disabled="!game.canDeal">Deal</button>
                    </div>
                    <div class="btn-group" role="group">
                        <button class="btn btn-warning" ng-click="game.hit()" ng-disabled="!game.canHit">HIT</button>
                    </div>
                    <div class="btn-group" role="group">
                        <button class="btn btn-danger" ng-click="game.end()" ng-disabled="!game.canHit">STAY</button>
                    </div>
                </div>
            </div>
        </div>
        <div class="row" ng-if="!game.started">
            <button class="btn btn-primary" ng-click="game.start()">Start</button>
        </div>
    </div>

    <!-- Game Results -->
    <div class="panel panel-default" ng-if="game.showResults">
        <div class="panel-heading">Results</div>
        <div class="panel-body">
            {{game.results}}
        </div>
    </div>

    <button class="btn btn-danger btn-sm" ng-click="game.reset()">Reset Game</button>

</div>

Wrapping Up

We’ve now got our GameService and GameController working properly and displaying correctly. You should now be able to run gulp buildDev and then http-server and play a game or two of blackjack! We’ve also added the concept of ‘losing’ a game (by busting), so bust a few hands and watch your points dwindle. You can ‘game’ the system though, but simply ‘STAY’-ing after dealing your initial hand. We need an opponent to keep us honest, and in casinos around the world, that opponent is the Dealer!

Up Next: The Dealer

Leave a comment