Promise chains with delays and pauses

Introduction

As a part of my recent hobby development of a Javascript-based version of the classic boardgame “Risk” I have played around quite a bit with promises and I wanted to share some stuff that I found useful.

First of all, I am writing code in the recent ES2015 syntax, so the Promises am I using are the newly added native Promises, no framework needed. I am then using Babel to transpile my code thus making it backwards compatible with old browser.

Full demo can be found at the bottom of this article.

Delays

I wished to create an AI opponent to play against for my Risk-game. When it’s the AI players turn it should execute a sequence of actions such as turn in cards, reinforce territories, attack territories and lastly ordering a troop movement (those of you who have played Risk are surely familiar with these turn phases). I could just let the AI execute all of these commands one after another but it would go by so fast that a human player would not be able to see what’s happening. Because of this I want some pauses between each action.

Javascript is of course single-threaded and do not have any equivalents of Thread.sleep (like in Java), but it does have some nifty timing- and interval-functions like setTimeout that executes a function after a delay.

To perform a chain of commands using setTimeout it would look something like this:

const turnInCards = () => { console.log('1') };
const deployment = () => { console.log('2') };
const attack = () => { console.log('3') };
const movement = () => { console.log('4') };

setTimeout(() => {
    turnInCards();
    setTimeout(() => {
        deployment();
        setTimeout(() => {
            attack();
            setTimeout(() => {
                movement();
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

If you run this code in your browsers console it would output 1, 2, 3 and 4 one after another with 1 second delay between each.

Of course this looks rather horrendous, and the longer the chain would become the crappier it would look, and also becomes harder to navigate and manage the code. In Javascript there already exists a nice solution to getting out of many of these callback hells and it is of course use of Promises. By making all of our commands into functions that returns promises we can chain them together and force to execute one after another. To also have a delay between each command we also need to use some kind of delay function. I wrote one that looks
like this:

const delay = ms => new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
});

Delay returns a Promise that uses the setTimeout-function by resolving itself after a given amount of time. Now we can use this delay function in a chain of promises, like this:

const turnInCards = () => { console.log('1'); Promise.resolve(); };
const deployment = () => { console.log('2'); Promise.resolve(); };
const attack = () => { console.log('3'); Promise.resolve(); };
const movement = () => { console.log('4'); Promise.resolve(); };

delay(1000)
.then(turnInCards)
.then(() => delay(1000))
.then(deployment)
.then(() => delay(1000))
.then(attack)
.then(() => delay(1000))
.then(movement);

Try to run this piece of code in your terminal (don’t forget the declared delay function) and it will produce the exact same outcome as the previous snippet, but this one looks so much cleaner because don’t get a nested tree of callbacks. Everything is on the same line. We could also refactor the code to look like this:

const turnInCards = () => { console.log('1'); return delay(1000); };
const deployment = () => { console.log('2'); return delay(1000); };
const attack = () => { console.log('3'); return delay(1000); };
const movement = () => { console.log('4'); return delay(1000); };

delay(1000)
.then(turnInCards)
.then(deployment)
.then(attack)
.then(movement);

We could just make the command functions return the delay function instead, which in turn returns a Promise, so this way the Proimse-chain becomes even cleaner and easier to follow.

Pausing

Next I wanted to be able to pause the game. If the AI opponent is in the middle of his turn I want to be able to press a pause button at which point the chain of commands pauses for as long as I like, until I unpause. For this I created a recursive promise-function called pauser that looks like this:

pauser() {
    if (!isPaused) {
        return;
    }
    return delay(500).then(() => pauser());
}

This function utilizes the delay function I already previously written. When called it checks if the game is not paused, in which case it just returns. If the game is paused however it will delay for 500 ms and then call itself, and it will continue to call itself (since it’s recursive) until the game unpauses. Now I need to add the pauser function between each command in the chain.

const turnInCards = () => { console.log('1'); return delay(1000); };
const deployment = () => { console.log('2'); return delay(1000); };
const attack = () => { console.log('3'); return delay(1000); };
const movement = () => { console.log('4'); return delay(1000); };

delay(1000)
.then(() => pauser())
.then(turnInCards)
.then(() => pauser())
.then(deployment)
.then(() => pauser())
.then(attack)
.then(() => pauser())
.then(movement)
.then(() => pauser());

In this example if you follow the chain delay is always called before pauser, so to clean up the chain a bit I could refactor so that these two are executed in the same function that utilizes both pauser and delay. It could look something like this.

const delayAndPause = () => delay(1000).then(() => pauser());

const turnInCards = () => { console.log('1'); return delayAndPause(); };
const deployment = () => { console.log('2'); return delayAndPause(); };
const attack = () => { console.log('3'); return delayAndPause(); };
const movement = () => { console.log('4'); return delayAndPause(); };

delayAndPause()
.then(turnInCards)
.then(deployment)
.then(attack)
.then(movement);

Now if the window variable isPaused is somewhere along this chain changed to true, the chain will pause and wait for it to become false again. I created a JS-Fiddle to demonstrate here:
https://jsfiddle.net/ToWelie89/dwnb3yh1/

I hope you found this post useful and informative 🙂 Feedback is always appreciated.

Peace out.

Advertisement