JavaScript: Creating a Generator Function
The following is an experiment in how to represent a physical pack of playing cards using a JavaScript Generator - a special type of object that is iterable - meaning that it can be invoked using .next() or in a for...of loop.
Creating a Generator
In order to instantiate a Generator object we need to use a generator function which is a function defined using function* that returns a value using the yield operator:
<script>
const shuffledPackGenerator = function*() {
// Original JavaScript code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
let pack = [];
/* generate a pack of cards - no jokers */
for(const suit of ["C", "D", "H", "S"]) { /* Clubs, Diamonds, Hearts, Spades */
for(let rank=1; rank <= 13; rank++) {
pack.push(rank + suit);
}
}
/* shuffle the cards */
pack.sort(
() => Math.round(Math.random()) - 0.5
);
/* the return value each time is a single card */
while(pack.length) {
if(1 == pack.length) {
return pack.shift();
} else {
yield pack.shift();
}
}
};
/* create a Generator (iterator) object */
const shuffledPack = shuffledPackGenerator();
</script>
At this stage you might be asking what the heck is going on? How does this work as a generator (iterator) when that while loop seems to just return everything?
The answer is that the yield operator actually pauses execution of the function meaning that:
- the first time it is called, the array is initialised and shuffled;
- each subsequent call returns a single value from the array; until
- a final call returns nothing (there are no cards left).
When we use return rather than yield for the last card it sets the return flag done: true (see below). Otherwise the last card will return with done: false and it won't be clear that the interator has run out of values.
Calling our Generator
The simplest way to invoke the generator is using .next():
let nextCard = shuffledPack.next();
// {value: "10S", done: false}
// {value: "8D", done: false}
// .. .
// {value: "5H", done: true}
// {value: undefined, done: true}
or you can loop through all the values with a for...of loop:
for(const card of shuffledPackGenerator()) {
console.log(card); // 7D, 13H, 10D, ..., 8C
}
Note that the above loop will end automatically when the last card has been returned, because the underlying generator function fails to yield or return any more cards.
A function for dealing cards
Using the above code as a starting point we can create a function for returning a set number of cards from the shuffled deck:
<script>
// Original JavaScript code by Chirp Internet: chirpinternet.eu
// Please acknowledge use of this code by including this header.
const dealCards = (n) => {
let cards = [];
for(let i=0; i < n; i++) {
let nextCard = shuffledPack.next();
cards.push(nextCard.value);
if(nextCard.done) break;
}
return cards;
};
</script>
Which when called repeatedly will deal cards in sets of five until the pack has been depleted:
console.log(dealCards(5)); // ["6C", "9C", "12D", "2C", "12S"]
console.log(dealCards(5)); // ["4D", "13S", "10S", "7D", "11S"]
console.log(dealCards(5)); // ["10C", "10D", "11H", "3S", "12H"]
console.log(dealCards(5)); // ["4S", "5S", "2D", "3C", "4H"]
console.log(dealCards(5)); // ["13D", "13H", "1H", "8D", "9H"]
console.log(dealCards(5)); // ["2S", "9D", "11D", "1S", "6D"]
console.log(dealCards(5)); // ["7H", "10H", "1C", "3H", "7S"]
console.log(dealCards(5)); // ["5D", "11C", "2H", "5C", "5H"]
console.log(dealCards(5)); // ["6H", "13C", "4C", "8C", "8S"]
console.log(dealCards(5)); // ["8H", "6S", "7C", "12C", "3D"]
console.log(dealCards(5)); // ["9S", "1D"]
console.log(dealCards(5)); // []
Working Example
In the space below you can 'deal' a hand of five cards from our shuffled deck, and keep dealing until the pack has been depleted. After that you will need to 'shuffle' the deck in order to continue:
All that is happening here is that we are calling dealCards(5) and displaying the output graphically. Shuffling is then a matter of creating a new Generator (there seems to be no way to 'reset' an existing generator).
Infinite Card Generator
In some cases you will want a setup where you never run out of cards - so when the pack is empty the cards are just re-shuffled automatically for the game to continue.
Here we have wrapped the above code into a JavaScript class using ES6 notation. The class has two separate generators - one for dealing a single pack and one where the cards are re-shuffled:
<script>
class Pack {
// Original JavaScript code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
constructor(type) {
this.cards = [];
this.init();
switch(type)
{
case "infinite":
this.generator = this.infinitePackGenerator();
break;
case "single":
default:
this.generator = this.singlePackGenerator();
}
}
init() {
/* generate a pack of cards - no jokers */
for(const suit of ["C", "D", "H", "S"]) { /* Clubs, Diamonds, Hearts, Spades */
for(let rank=1; rank <= 13; rank++) {
this.cards.push(rank + suit);
}
}
this.shuffle();
}
shuffle() {
/* shuffle the cards */
this.cards.sort(
() => Math.round(Math.random()) - 0.5
);
}
* singlePackGenerator() {
while(this.cards.length) {
if(1 == this.cards.length) {
return this.cards.shift();
} else {
yield this.cards.shift();
}
}
}
* infinitePackGenerator() {
do {
while(this.cards.length) {
yield this.cards.shift();
}
this.init();
} while(true);
}
deal(n) {
/* return an array of n cards */
let cards = [];
for(let i=0; i < n; i++) {
let nextCard = this.generator.next();
cards.push(nextCard.value);
if(nextCard.done) break;
}
return cards;
}
}
</script>
Note that we use * here to identify a generator function rather than function* which we saw earlier for stand-alone generator functions. And in the infinite generator we return the next card using yield every time and never return.
Be aware that the various class elements are by default public and can be called from anywhere, or exposed using console.log(pack.cards); for example, if they are not wrapped in some kind of closure taking them out of the global scope.
Ideally we would make all of the class variables and methods private aside from the constructor and deal methods, but JavaScript objects are tricky in that regard.
Sample usage:
<script>
const pack = new Pack("single");
let cards = pack.deal(5);
...
const pack = new Pack("infinite");
let cards = pack.deal(5);
...
</script>
Using the first example the dealt cards will eventually run out - as demonstrated earlier - while in the second example the cards will keep coming from newly shuffled decks and never run out.
References
- MDN: Generator object
- MDN: generator function
- MDN: yield operator
- Stack Overflow: Writing a generator in a JavaScript class
Related Articles - Games
- JavaScript Amazing Maze Game
- JavaScript Random Maze Generator
- JavaScript Playable Maze Game Generator
- JavaScript Twister Controller with Speech
- JavaScript Memory Card Game with Animation
- JavaScript Graphing Game
- JavaScript Creating a Generator Function