Exploring ECMAScript 5 with a Simple Game of Poker

IEBlog

Internet Explorer Team Blog

Exploring ECMAScript 5 with a Simple Game of Poker

  • Comments 10

Support for ECMAScript 5, the updated standard of the language commonly known as JavaScript, is part of our promise to ensure that the same markup and same script works across browsers. Over the past few months we’ve devoted more than a few blog posts to the topic including:

With the IE9 RC, we released another Test Drive demo to explore the full benefits of ECMAScript 5 available in IE9. This game of Texas Hold’Em (fondly referred to as T’ECMAScript Hold’Em in Redmond) is intended to be a brief illustration of how to write a reusable class library leveraging many of the features of ES5 to retain more developer control and model object oriented design patterns. Both are necessities as Web standards-based applications grow in size and complexity.

Start with Feature Detection

Because our game of Texas Hold’Em makes liberal use of ECMAScript 5, we employ feature detection to ensure that the app is running in a browser which supports the necessary features. The FeatureDetectES5Properties.js file specifically looks for support of a key ECMAScript 5 feature, defineProperties, to ensure that the rest of the application can proceed without encountering runtime errors. It’s not a comprehensive test conformance test for ES5 support, but its decent shorthand for checking to see that the app is running in a modern browser.

if (!Object.defineProperties || !Object.defineProperty) {
    $FeatureDetect.fail("This demo requires ECMAScript 5 properties API support.");
}

Extending Developer Control

Attributes

One of the most significant changes introduced by ES5 is the addition of direct developer control for property attributes. This means that after perfecting a library you no longer need to worry that consumers might accidentally overwrite a key method.

Attributes provide full control over ECMAScript objects, allowing developers to protect those key methods or values, and with granular control depending on exactly what protection is required.

Attributes are applied using a property descriptor.

new_card_desc = {
    value: {
        configurable: false,
        enumerable: false,
        writable: true,
        value: value
    },
    binval: {
        configurable: false,
        enumerable: false,
        writable: true,
        value: binval
    }
};

This descriptor can be found in the Card.js library attached to our Poker game. The value is a plain text string representing the Suit and Rank of a standard playing card. The binval is a binary representation of the same information which provides an absolute way of comparing any two playing cards.

Many internal methods depend on the existence of these properties, so we cannot risk them being removed. Further, after the card is created there is no reason they should ever change. I set writable:false to prevent new values from being assigned. I set configurable:false to make them essentially permanent. The enumerable attribute hides a property during a for-in inspection of the object. As all references to these properties are made inside other methods there is no real reason to expose them to other libraries. By setting enumerable:false I clean up my object, exposing only those properties which are useful to other libraries.

Developers more familiar with the ES5 additions might note that the default value for any attribute in a descriptor is false. Given this, we can rewrite our descriptor as follows:

new_card_desc = {
    value: {
        value: value
    },
    binval: {
        value: binval
    }
};

This is an extremely simplistic example. The value attribute of a property can be any JavaScript Object, which makes this a handy way to protect entire methods as well. You can see this in Shuffle method in the Deck.js file.

Shuffle: {
    value: function () {
        var i, j, tempi, tempj;
        this.dealt = 0;
        this.discardPile = [];
        for (i = 0; i < this.length; i++) {
            j = Math.floor(Math.random() * (i + 1));
            tempi = this[i];
            tempj = this[j];
            this[i] = tempj;
            this[j] = tempi;
        }
    }
}

With the writable and configurable attributes set to false there is now little chance of accidentally or unscrupulously altering the logic - no automatic pocket aces for you! The Deck object itself is essentially an array of Card objects. With all methods set to enumerable:false iterating over the Deck is the same as iterating over all available playing cards. The methods are still accessible, but do not clutter up the logic in other places.

Setters/Getters

Setters and Getters are a common programming motif in many general purpose programming languages. One of the more powerful benefits this provides is simple, direct access to programmatically generated data – commonly referred to as a “computed value.” Luckily, the Card object representation in the Card.js file provides a perfect example of this:

image: {
    enumerable: true,
    get: function () {
        var iSuit, iRank;
        switch (this.suit) {
            case "S": iSuit = "Spades"; break;
            case "H": iSuit = "Hearts"; break;
            case "D": iSuit = "Diamonds"; break;
            case "C": iSuit = "Clubs"; break;
            default: throw "No such suit: " + this.suit;
        }
        if (isNaN(parseInt(this.rank, 10))) {
            switch (this.rank) {
                case "T": iRank = "10"; break;
                case "J": iRank = "Jack"; break;
                case "Q": iRank = "Queen"; break;
                case "K": iRank = "King"; break;
                case "A": iRank = "Ace"; break;
                default: throw "No such rank: " + this.rank;
            }
        } else if (this.rank > 1) {
            iRank = this.rank;
        } else {
            throw "No such rank: " + this.rank;
        }
        return iSuit + "_" + iRank + ".png";
    }
}

Each Card object has a matching .png in our application for display purposes. Rather than manually iterating over every card and storing each .png’s unique name - we can provide a getter, inherited by every Card in our Deck, which generates this string using the existing unique information for every card. We could have created this as a method call, but in this instance a getter seems to be a better fit - no arguments are needed or expected and no unnecessary method is exposed to users of our library. It’s just a simple data property to consumers.

Object.defineProperty

Object.defineProperty and Object.defineProperties are the methods which allow us to apply the new ES5 attributes. They differ only in the number of properties they allow the user to define in one method call. Every property in our Poker game is defined using these two methods. Of course, this isn’t the only way to declare properties; developers can mix and match their property declarations however they wish. Consider this a useful illustration of the flexibility of these new methods. You can see an example of this in the Card.js file.

var CardPrototype = {};
Object.defineProperties(CardPrototype, card_proto_desc);

Object.seal, Object.freeze

There are alternatives to using descriptors and direct attribute declaration as well. Object.seal and Object.freeze provide a simple way to extend a desired level of property protection to an entire Object. Before we can discuss them we first need to mention one last attribute added by ES5: extensible. Extensible is added not an Object’s properties, but to the object itself. Call Object.preventExtensions to permanently set this flag, preventing any new properties from being defined on that Object; both seal and freeze are supersets of this functionality. Object.seal is a one-step way to lock down extensibility of an Object and configurability of all of its properties. Those properties which are writable may still be modified, but not removed. To achieve further protection, Object.freeze is the final word; it prevents all changes to the Object and its properties including value assignment. This can be handy when declaring constants, such as the arrays holding our Suits and Ranks:

/* All available card suits */
var Suits = {
    "S" : 3, //spade
    "H" : 2, //heart
    "D" : 1, //diamond
    "C" : 0  //club
};
Object.freeze(Suits);

/* All available card ranks */
var Ranks = {
    "2" : 2,
    "3" : 3,
    "4" : 4,
    "5" : 5,
    "6" : 6,
    "7" : 7,
    "8" : 8,
    "9" : 9,
    "T" : 10, //ten
    "J" : 11, //jack
    "Q" : 12, //queen
    "K" : 13, //king
    "A" : 14  //ace
};
Object.freeze(Ranks);

The Object Oriented Approach

Object.create

Object.create is a simple way of saying “Give me a new object with this prototype.” This could be done previously by saying:

<new_object>.prototype = <proto_object>;
return new <new_object>;

Unlike the declaration above, Object.create also provides a concise way of adding new properties to the <new_object>, creating a new Object instance which inherits from a shared prototype but has unique, per-instance values. A simple example can be found at the bottom of Card.js:

/* CONSTRUCTOR */
var NewCard = function (suit, rank) {
    // Setup unique values for unique instance
    var value, binval, new_card_desc;
    value = suit.concat(rank);
    binval = Suits[suit];
    binval = binval << 4;
    binval = binval + Ranks[rank];
    new_card_desc = {
        value: {
            writable: true,
            value: value,
        },
        binval: {
            writable: true,
            value: binval
        }
    };
    // Return unique instance extending Card object
    return Object.create(CardPrototype, new_card_desc);
};

Our CardPrototype contains our shared methods (Comparison method, etc) while our new descriptor contains the unique values for each card instance.

Object.keys

Object.keys returns an Array of every enumerable property on an Object. A simple example from our Deck constructor:

// build deck
suits = Object.keys(Suits);
ranks = Object.keys(Ranks);
for (var s in suits) {
    for (var r in ranks) {
        deckArray[deckArray.length] = NewCard(suits[s], ranks[r]);
    }
}

By iterating over all of the properties on our two Arrays of constants every possible combination of Rank and Suit is hit and full deck compiled.

Object.getOwnPropertyNames

Use Object.getOwnPropertyNames for those cases where you want every method, enumerable or not. Note however that it only returns own property names. This means properties inherited from a prototype will be skipped. This does provide a simple way of separating the two.

Completing the App

With this brief walk-through, we’ve explored many of the features in ES5 and how they contributed to the implementation of this CardGame library. The library itself (contained in the Card and Deck files) are used in conjunction with the portions which are unique to this particular app (the Poker, Player, and Game files which describes how the game board should be laid out.)

To complete the application, I ran the the app through JSLint which caught a couple of errors – spaces between if and the following conditional, usage of postfix unary operators, and declaring var on a separate line instead of in a single line at the top of the containing scope.

Use this example to explore ES5

Hopefully, this simple poker game serves as a good illustration of the broad support IE9 has for ES5. You can use this code to write your own ES5 based card game – perhaps ECMAScript 5-card draw?

Download IE9, code well, have fun.

—Jared Straub, Software Developer Engineer in Test, JavaScript Team

  • Loading...