30

I would like to add a small dice-rolling effect to my Javascript code. I think a good way is to use the setInterval() method. My idea was the following code (just for testing):

function roleDice() {
    var i = Math.floor((Math.random() * 25) + 5);
    var j = i;
    var test = setInterval(function() {
        i--;
        document.getElementById("dice").src = "./images/dice/dice" + Math.floor((Math.random() * 6) + 1) + ".png";
        if (i < 1) {
            clearInterval(test);
        }

    }, 50);
}

Now I would like to wait for the setInterval until it is done. So I added a setTimeout.

setTimeout(function(){alert("test")}, (j + 1) * 50);

This code works quite okay. But in my main code the roleDice() function returns a value. Now I don’t know how I could handle that... I can’t return from the setTimeout(). If I add a return to the end of the function, the return will rise too fast. Does anyone have an idea, of how I could fix that?

Edit Hmm, okay I understand what the callback does and I think I know how it works but I have still the problem. I think it’s more of an "interface" problem... Here is my code:

function startAnimation(playername, callback) {
    var i = Math.floor((Math.random() * 25) + 5);
    var int = setInterval(function() {
        i--;
        var number = Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
        if(i < 1) {
            clearInterval(int);
            number = Math.floor((Math.random() * 6) + 1);
            addText(playername + " rolled " + number);
            document.getElementById("dice").src = "./images/dice/dice" + number + ".png";
            callback(number);
        }
    }, 50);
}

function rnd(playername) {
    var callback = function(value){
        return value; // I knew thats pointless...
    };
    startAnimation(playername, callback);
}

The function rnd() should wait and return the value… I’m a little bit confused. At the moment I have no clue how to going on... The code wait for the var callback... but how I could combine it with the return? I would like to run the animation and return after that the last number with rnd() to another function.

palkenas
  • 19
  • 4
Andre Hofmeister
  • 3,185
  • 11
  • 51
  • 74
  • 8
    I think you wanted to call your function `rollDice`, not `roleDice` ;) – ThiefMaster Jun 15 '12 at 17:40
  • also, not sure why you would set both a set interval and set timeout since your interval knows when it is finished, and any code that needs to occur at the end can happen there. There is no guarantee that a set interval and set timeout with the same amount of time will actually end at the same time. – dqhendricks Jun 15 '12 at 17:44
  • @ThiefMaster Yea, youre right! :) – Andre Hofmeister Jun 15 '12 at 18:14

6 Answers6

50

You stumbled into the pitfall most people hit at some point when they get in touch with asynchronous programming.

You cannot "wait" for an timeout/interval to finish - trying to do so would not work or block the whole page/browser. Any code that should run after the delay needs to be called from the callback you passed to setInterval when it's "done".

function rollDice(callback) {
    var i = Math.floor((Math.random() * 25) + 5);
    var j = i;
    var test = setInterval(function() {
        i--;
        var value = Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + value + ".png";
        if(i < 1) {
            clearInterval(test);
            callback(value);
        }
    }, 50);
}

You then use it like this:

rollDice(function(value) {
    // code that should run when the dice has been rolled
});
Ɖiamond ǤeezeƦ
  • 3,223
  • 3
  • 28
  • 40
ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
  • 1
    Okay, thanks for your reply, but the code from the callback should return a value. Is that possible? Please see my **Edit**. Greetz – Andre Hofmeister Jun 16 '12 at 09:33
  • What you are trying to do is impossible. When working with callbacks and asynchronous functions, any data you want to return after something has finished needs to be passed to another callback function as an argument. – ThiefMaster Jun 16 '12 at 09:35
  • Okay, so I have to find a other way or pass the value with the callback. Thanks. – Andre Hofmeister Jun 16 '12 at 09:40
  • Yes, simply take a callback argument. Instead of writing your code like `foo = blah(); ...` you use `blah(function(foo) { ... });` – ThiefMaster Jun 16 '12 at 09:44
  • There are 2 callbacks: `setinterval()` callback (1st argument) and the `rolldice` argument callback. You can omit the argument to `rolldice` and put code directly in the `ìf` statement. Why complicated.. – Timo Jun 03 '20 at 06:45
  • I forgot the Call: `rollDice()` to my post from just now. – Timo Jun 03 '20 at 06:55
32

You can now use Promises and async/await

Like callbacks, you can use Promises to pass a function that is called when the program is done running. If you use reject you can also handle errors with Promises.

function rollDice() {
  return new Promise((resolve, reject) => {
    const dice = document.getElementById('dice');
    let numberOfRollsLeft = Math.floor(Math.random() * 25 + 5);

    const intervalId = setInterval(() => {
      const diceValue = Math.floor(Math.random() * 6 + 1);

      // Display the dice's face for the new value
      dice.src = `./images/dice/dice${diceValue}.png`;

      // If we're done, stop rolling and return the dice's value
      if (--numberOfRollsLeft < 1) {
        clearInterval(intervalId);
        resolve(diceValue);
      }
    }, 50);
  });
}

Then, you can use the .then() method to run a callback when the promise resolves with your diceValue.

rollDice().then((diceValue) => {
  // display the dice's value to the user via the DOM
})

Or, if you're in an async function, you can use the await keyword.

async function takeTurn() {
  // ...
  const diceValue = await rollDice()
  // ...
}
Andria
  • 4,712
  • 2
  • 22
  • 38
  • how can i return a callback function from rollDice to calee when promise rejects instead of returning error message – kumar Jun 06 '18 at 11:07
  • 1
    @kumar I'm not sure I entirely understand what you're trying to ask but I think what you want is the `.catch` function and `.reject` function. From what I know you should be using the `.reject` function to return an error message and the catch to handle it accordingly, such as logging. – Andria Jun 24 '18 at 02:55
  • @ChrisBrownie55 can you please give an example to test `Promise` with `clearinterval` like [here](https://stackoverflow.com/a/5978545/1705829). – Timo Jun 03 '20 at 08:31
  • @Timo I'm not sure I understand, `clearInterval` is a method for removing an interval. If you're looking for something relating to being able to clear the interval if you've got it in this promise format. You'll just need to either pass the interval id down via `resolve()` or make it global (or the highest scope you need) – Andria Jun 03 '20 at 19:44
  • Thanks! Simple but powerful! Or at least if you know what i mean. Thanks anyway! – Jarrett Dec 28 '20 at 12:04
  • Thank you so much for this! – Kamal Alhomsi Feb 18 '21 at 15:17
2

Orginally your code was all sequential. Here is a basic dice game where two players roll one and they see who has a bigger number. [If a tie, second person wins!]

function roleDice() {
    return Math.floor(Math.random() * 6) + 1;
}

function game(){    
    var player1 = roleDice(),
        player2 = roleDice(),
        p1Win = player1 > player2;
    alert( "Player " + (p1Win ? "1":"2") + " wins!" );
}

game();

The code above is really simple since it just flows. When you put in a asynchronous method like that rolling the die, you need to break up things into chunks to do processing.

function roleDice(callback) {
    var i = Math.floor((Math.random() * 25) + 5);   
    var j = i;
    var test = setInterval(function(){
        i--;
        var die =  Math.floor((Math.random() * 6) + 1);
        document.getElementById("dice").src = "./images/dice/dice" + die + ".png";
        if(i < 1) {
                clearInterval(test);
                callback(die);  //Return the die value back to a function to process it
            }
        }, 50);
}

function game(){
    var gameInfo = {  //defaults
                       "p1" : null,
                       "p2" : null
                   },
        playerRolls = function (playerNumber) { //Start off the rolling
            var callbackFnc = function(value){ //Create a callback that will 
                playerFinishes(playerNumber, value); 
            };
            roleDice( callbackFnc );
        },
        playerFinishes = function (playerNumber, value) { //called via the callback that role dice fires
            gameInfo["p" + playerNumber] = value;
            if (gameInfo.p1 !== null && gameInfo.p2 !== null ) { //checks to see if both rolls were completed, if so finish game
                giveResult();
            }
        },
        giveResult = function(){ //called when both rolls are done
            var p1Win = gameInfo.p1 > gameInfo.p2;
            alert( "Player " + (p1Win ? "1":"2") + " wins!" );
        };            
    playerRolls("1");  //start player 1
    playerRolls("2");  //start player 2
}

game();

The above code could be better in more of an OOP type of way, but it works.

epascarello
  • 204,599
  • 20
  • 195
  • 236
1

There are a few issues for the above solutions to work. Running the program doesn't (at least not in my preferred browser) show any images, so these has to be loaded before running the game.

Also, by experience I find the best way to initiate the callback method in cases like preloading N images or having N players throw a dice is to let each timeout function do a countdown to zero and at that point execute the callback. This works like a charm and does not rely on how many items needing to be processed.

<html><head><script>
var game = function(images){
   var nbPlayers = 2, winnerValue = -1, winnerPlayer = -1;
   var rollDice = function(player,callbackFinish){
      var playerDice = document.getElementById("dice"+player);
      var facesToShow = Math.floor((Math.random() * 25) + 5);   
      var intervalID = setInterval(function(){
         var face =  Math.floor(Math.random() * 6);
         playerDice.src = images[face].src;
         if (--facesToShow<=0) {
            clearInterval(intervalID);
            if (face>winnerValue){winnerValue=face;winnerPlayer=player}
            if (--nbPlayers<=0) finish();
         }
      }, 50);
   }
   var finish = function(){
      alert("Player "+winnerPlayer+" wins!");
   }      
   setTimeout(function(){rollDice(0)},10);
   setTimeout(function(){rollDice(1)},10);
}
var preloadImages = function(images,callback){
   var preloads = [], imagesToLoad = images.length;
   for (var i=0;i<images.length;++i){
      var img=new Image();
      preloads.push(img);
      img.onload=function(){if(--imagesToLoad<=0)callback(preloads)}
      img.src = images[i];
   }
}
preloadImages(["dice1.png","dice2.png","dice3.png","dice4.png","dice5.png","dice6.png"],game);
</script></head><body>
<img src="" id="dice0" /><img src="" id="dice1" /></body></html>
1

To achieve that goal, using vanilla setInterval function is simply impossible. However, there is better alternative to it: setIntervalAsync.

setIntervalAsync offers the same functionality as setInterval, but it guarantees that the function will never executed more than once in a given interval.

npm i set-interval-async

Example:

setIntervalAsync(
  () => {
      console.log('Hello')
      return doSomeWork().then(
        () => console.log('Bye')
      )
  },
  1000
)
smac89
  • 39,374
  • 15
  • 132
  • 179
Figo
  • 11
  • 2
1

Example with Promises & setIntervals.. this is how I created a 'flat' chain of functions that wait until the other is completed...

The below snippet uses a library called RobotJS (here it returns a color at a specific pixel on screen) to wait for a button to change color with setInterval, after which it resolves and allows the code in the main loop to continue running.

So we have a mainChain async function, in which we run functions that we declare below. This makes it clean to scale, by just putting all your await someFunctions(); one after each other:

async function mainChain() {
  await waitForColorChange('91baf1', 1400, 923)

  console.log('this is run after the above finishes')
}




async function waitForColorChange(colorBeforeChange, pixelColorX, pixelColorY) {
  return new Promise((resolve, reject) => {
    let myInterval = setInterval(() => {
      let colorOfNextBtn = robot.getPixelColor(pixelColorX, pixelColorY)

      if (colorOfNextBtn == colorBeforeChange) {
        console.log('waiting for color change')
      } else {
        console.log('color has changed')
        clearInterval(myInterval);
        resolve();
      }
    }, 1000)
  })
}


//Start the main function
mainChain()
AGrush
  • 1,107
  • 13
  • 32