0

First disclaimer: I am still extremely new to Typescript/javascript/front-end development

Background: I have a set of images (which represent a hand of cards). When it is the AI's turn to play a card, I am trying to show a simulation of "thinking" instead of immediately playing the card. My simulation is to iterate through the hand of cards and "select" each one (by "select", I mean move the image slightly to the left and then back to the right).

I am using Visual Studio Code and I am debugging in Chrome. My current code is below. The premise is that an API call is made (which is really where the AI logic is performed). Then the presentation iterates through the hand of cards and for each one it waits 1/4 second, shifts the card to the left, waits another 1/4 second, and shifts the card back to the right. After all cards have been "selected", then the actual card is played. I currently have everything in callback functions to keep it synchronous, but I'm not sure that I even need to do that.

 // Call in to the API, which will perform the AI logic
 // The API will return a slot that was played, which contains the card placed into that slot
 opponentTurn(callback)
 {
   this.boardService.opponentTurn()
       .subscribe(
       cardPlayed =>
       {
         // Set the card played
         let cardData = [];
         cardData = JSON.parse(cardPlayed.toString());

        // To add a slight hint of "thinking", let's "select" (slightly move the card to the left) each card down, up, and back down to the card that the API selected
        this.selectCard(function(_thisagain) {
          // Done
          // Now simulate the move
          _thisagain.dragService.simulateDragDrop(cardData);
        });        

         callback(this);
       }
     );
 }

 // Call this to "select" a card; used by the AI "thinking" simulation
 selectCard(callback)
 {
  for (var i = 0; i < this.players[1].cardHand.length; i++)
  {
    let imgObj = document.getElementById(this.players[1].name + i);     

    if (imgObj != null)
    {
      this.moveCard(imgObj, '400px', function(_thisagain) {
        _thisagain.moveCard(imgObj, '350px', function() {
          // Done
        });
      });
    }
  }

  callback(this);
 }

 moveCard(imgObj, position, callback)
 {
    this.wait(250, function() {
      imgObj.style.right = position;
    });
    callback(this);
 }

 wait(ms, callback)
 {
   var start = new Date().getTime();
   var end = start;
   while(end < start + ms)
    {
      end = new Date().getTime();
    }
    callback(this);
 }

So the struggle I am having is that the code works, but only when I put a breakpoint on it. For example, if I put a breakpoint on the "_thisagain.moveCard(imgObj, '350px', function() {" line and then debug it, I can see each card shift to the left and back to the right as I would expect every time I 'Continue'. If I remove the breakpoint, the cards don't shift at all (yet I still get the wait before the card is played on the board).

Still being new to Typescript/javascript, I'm not really sure what is going on. It seems that when I have the breakpoint set, a redraw occurs to show the card shift. Without the breakpoint, it seems that no redraw is occurring. I'm not sure how to correct that, though, thus why I am here.

  • It sounds like the debugger is causing it to slow down and let you watch the animation. When there is no breakpoint the wait still occurs but the animations may be instant. Try defining `wait()` as async? – James Jul 18 '17 at 17:43
  • That makes sense to me, James, but unfortunately defining async wait() did not change anything; still the same behavior. – Joseph Robinson Jul 18 '17 at 17:53
  • The problem may be caused by the fact that your wait and animation are tightly coupled, i.e. in the same function. Try having one function that runs the animation, and one that calls a "wait" period, and use them both in an asynchronous function rather than a callback. Callbacks only operate when the caller function completes so a caller and callback cannot operate async together. – James Jul 18 '17 at 18:05

1 Answers1

0

So after more research and a lot of trial-and-error, I have it working. The post that really got me on the right track is this: DOM refresh on long running function

In that question, there was an answer posted which stated: "Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser."

Turns out that was the key I was missing. I was processing far too much in a single statement and the js was not releasing back to the browser, thus the DOM/html was not being updated to show the "animation".

I then used the jsfiddle example also posted in an answer there to cobble together a "worker" thread that breaks up the process into chunks based on a procedural "status" and gives the control back to the browser with setTimeout(). Each worker thread is still set up as a callback function to ensure that the processing in each chunk finishes before moving on. For the intent of my original question, everything is working. I'm still refactoring and I'm sure there is probably a better way to achieve the results, but for now I am content.

For the sake of maybe helping someone in the future that might stumble across my question, this is what I have in my worker thread. Of course, my choice of terminology may be incorrect, but the gist of it is this. When I want to start a process flow, I can set the "status" text and call in to the worker. The worker then processes the flow as needed.

doHeavyWork() {
    if (this.status == 'finish')
    {
      // All steps done 
    }
    else
    {
      let _this = this;
      switch (_this.status)
      {
        case 'start':
          _this.status = 'working';
          _this.opSelectCard = 0;
          this.opponentTurn(function(response) {
            _this.opCardData = response;
            _this.status = 'selectCard';
          });
          break;

        case 'selectCard':
          _this.status = 'working';
          this.selectCard(_this.opSelectCard, function(response) {
            _this.opImgObj = response;
            _this.status = 'moveCardLeft';
          });
          break;

        case 'moveCardLeft':
          _this.status = 'working';
          this.moveCardLeft(_this.opImgObj, '-25px', function(root) {
            _this.status = 'moveCardRight';
          });
          break;

        case 'moveCardRight':
          _this.status = 'working';
          this.moveCardRight(_this.opImgObj, '1px', function(root) {
            _this.opSelectCard++;
            if (_this.opSelectCard < _this.players[1].cardHand.length)
            {
              _this.status = 'selectCard';
            }
            else
            {
              _this.status = 'simulateDragDrop';
            }
          });
          break;

        case 'simulateDragDrop':
          _this.status = 'working';
          this.dragService.simulateDragDrop(_this.opCardData, function(root) {
            _this.status = 'refreshDisplay';
          });
          break;

        case 'refreshDisplay':
          _this.status = 'working';
          this.refreshCardDisplay(function(root) {
            _this.status = 'refreshDisplay';
          });
          break;

        case 'refreshBoard':
          _this.status = 'working';
          this.refreshBoard(_this.boardCards, function(root) {
            _this.status = 'updateScore';
          });
          break;

        case 'updateScore':
          _this.status = 'working';
          this.updateScore(_this.boardCards, function(_this) {
            _this.status = 'switchTurns';
          });
          break;

        case 'switchTurns':
          _this.status = 'working';
          this.switchTurns(function(_this) {
            if (_this.turn == 1)
            {
              _this.status = 'finish';
            }
            else
            {
              _this.status = 'start';
            }
          });
          break;
      }

      setTimeout(function() {
        _this.doHeavyWork();
      }, 0);
    }
  }