Search code examples
javascriptrecursionweb-worker

Webworker for a recursive function


I have already some elements for my answer, coming from an old post (Trying to implement inline Webworker for a recursive function).

Now I take over this little code implying this issue. This little code uses inline webworkers since I use a recursive function which computes the best hit to play (this is the "computer" Hit). If I don't use webworker and I take a depth too high, the game is hanging in browser.

the source of code is available on [this link][1]

I would like to get a working code. Indeed, I am near to the solution since several bugs have been fixed (for example, the fact to include different functions in webworker to make it do its computations without calling external functions. If I don't include all necessary functions, the webworker will not be able to work since it has its own scope.

So I would like to get help to debug my current version.

The game is available on [this link][2] I am interested only into "computer vs player" mode (black begins to play). Here below you can find the part when I call the recursive function.

Once computation is done with this recursive function, I would like to get back the object representing the current map of game (positions of black/white circles).

To get back the object or the coordinates suggested for hit (Here we are in main function), I did the following code :

// Call the recursive function and get final (a,b) results
    new Promise( resolve => {
        let HitTemp = JSON.parse(JSON.stringify(HitCurrent));
        let firstWorker = new Worker( workerScript );
        firstWorker.onmessage = function ( event ) {
            resolve( event.data ); //{ result: XXX }
            console.log('here5');
        }
        firstWorker.postMessage([HitTemp, HitTemp.playerCurrent, maxNodes]);
    } ).then( ( { result } ) => {
        //let [ a, b ] = result.coordPlayable;
        let [ a, b ] = HitTemp.coordPlayable;
        console.log('result3 : ', result);
        console.log('here3 : ', a, b);
    } );

  // HERE I TRY TO USE a and b in exploreHitLine function
  // who needs the variables a and b BUT I DON'T KNOW
  // IF a AND b ARE KNOWN HERE FROM CODE ABOVE

  for (k = 0; k < 8; k++) {
   exploreHitLine(HitCurrent, a, b, k, 'drawing');
  }

and below the recursive function (inside inline webworker part) :

window.onload = function() {

  // Inline webworker version
  workerScript = URL.createObjectURL( new Blob( [ `
  "use strict";

...
...
// All other variables and functions necessary for inline webworker
...
...
...


        function negaMax(HitCurrent, colorCurrent, depth) {
          // Indices
          var i, j, k;
          // Evaluation
          var arrayTemp, evalFinal, e;
          // Set current color to HitCurrent
          HitCurrent.playerCurrent = colorCurrent;
          // Deep copy of arrayCurrent array
          arrayTemp = JSON.parse(JSON.stringify(HitCurrent.arrayCurrent));
          // If depth equal to 0
          if (depth == 0)
            return evaluation(HitCurrent);
          // Starting evaluation
          evalFinal = -infinity;
          // Compute all playable hits and check if playable hits
          if (computeHit(HitCurrent, 'playable')) {
            // Browse all possible hits
            for (i = 0; i < 8; i++)
              for (j = 0; j < 8; j++)
            if (HitCurrent.arrayPlayable[i][j] == 'playable') {
              for (k = 0; k < 8; k++) {
                // Explore line started from (i,j) with direction "k"
                exploreHitLine(HitCurrent, i, j, k, 'drawing');
              }
              // Recursive call
              e = -negaMax(JSON.parse(JSON.stringify(HitCurrent)), ((JSON.stringify(HitCurrent.playerCurrent) == JSON.stringify(playerBlack)) ? playerWhite : playerBlack), depth-1);
              if (e > evalFinal) {
                HitCurrent.coordPlayable = [i,j];
                evalFinal = e;
              }
              if (e == -infinity) {
                HitCurrent.coordPlayable = [i,j];
              }
              // Restore arrayCurrent array
              HitCurrent.arrayCurrent = JSON.parse(JSON.stringify(arrayTemp));
            }
            // Clean playable hits once double loop is done
            cleanHits('playable', HitCurrent);
          }
              console.log('here2 :', evalFinal);
          return evalFinal;
             }
        onmessage = function ( event ) {
          let params = event.data;
          //postMessage( { result: recursiveFunction(  HitCurrent, HitCurrent.playerCurrent, maxNodes ) } );
          postMessage( { result: negaMax( ...params ) } );
        };
        ` ], { type: "plain/text" } ) );

     main();
    }

I expect to get back the coordinates "a" and "b" of computed value by recursive function but it seems to return nothing.

I don't know how to receive the object HitTemp object or more directly get a and b suggested coordinates ?

Feel free to ask me more precisions if you don't understand my problem in this algorithm.


Solution

  • There are two problems with the code that you presented:

    Problem #1: Assuming that postMessage passes a reference, but instead it serializes/deserializes it.

    When you are using postMessage from within main js into the WebWorker, you are passing the HitTemp object, and later on, in the WebWorker you assume, that if you set properties of that object, the the original object would be modified as well. By that I mean the following code:

    firstWorker.postMessage([
        HitTemp // <-- here you are sending the object to the WebWorker
    , HitTemp.playerCurrent, maxNodes]);
    
    workerScript = URL.createObjectURL( new Blob( [ `
    // ...
        if (e > evalFinal) {
            HitCurrent.coordPlayable = [i,j]; // <-- here you access the object
            evalFinal = e;
        }
    //...
    

    Unfortunately, according to the documentation, when calling postMessage, the original object is serialized by the caller and then deserialized in the WebWorker, so effectively, the WebWorker is operating on a copy. Fortunately, this can be easily avoided by posting the data you are most interested in inside the postMessage back from the WebWorker.

    Problem #2 : Using the a and b variables outside the scope

    I noticed that you are trying to access values that are defined in the .then() result callback outside that callback, like so:

    //...
    } ).then( ( { result } ) => {
        let [ a, b ] = //here you define a and b
    } );
    
    // a and b are no longer in scope here
    for (k = 0; k < 8; k++) {
        exploreHitLine(HitCurrent, a, b, k, 'drawing');
    }
    

    Solution

    To solve the first problem you need to return the values of HitCurrent (which contain coordPlayable that you are probably most interested in) with the postMessage back from the WebWorker. For the second problem, just move the final for loop using the a and b variables inside the .then() callback. The result code is as follows:

    Main js code:

    new Promise( resolve => {
        let HitTemp = JSON.parse(JSON.stringify(HitCurrent));
        let firstWorker = new Worker( workerScript );
        firstWorker.onmessage = function ( event ) {
            resolve( event.data );
            console.log('here5');
        }
        firstWorker.postMessage([HitTemp, HitTemp.playerCurrent, maxNodes]);
    } ).then( ( { result } ) => {
        var HitResult = result.HitResult;
        let [ a, b ] = HitResult.coordPlayable; // <-- get values from result
        console.log('result3 : ', result.eval);
        console.log('here3 : ', a, b);
    
        //move for loop inside the callback
        for (k = 0; k < 8; k++) {
            exploreHitLine(HitCurrent, a, b, k, 'drawing');
        }
    } );
    

    WebWorker:

    window.onload = function() {
    
    // Inline webworker version
    workerScript = URL.createObjectURL( new Blob( [ `
    "use strict";
    
    // ...
    function negaMax(HitCurrent, colorCurrent, depth) {
        // Indices
        var i, j, k;
        // Evaluation
        var arrayTemp, evalFinal, e;
        // Set current color to HitCurrent
        HitCurrent.playerCurrent = colorCurrent;
        // Deep copy of arrayCurrent array
        arrayTemp = JSON.parse(JSON.stringify(HitCurrent.arrayCurrent));
        // If depth equal to 0
        if (depth == 0)
            return evaluation(HitCurrent);
        // Starting evaluation
        evalFinal = -infinity;
        // Compute all playable hits and check if playable hits
        if (computeHit(HitCurrent, 'playable')) {
            // Browse all possible hits
            for (i = 0; i < 8; i++)
                for (j = 0; j < 8; j++)
                    if (HitCurrent.arrayPlayable[i][j] == 'playable') {
                        for (k = 0; k < 8; k++) {
                            // Explore line started from (i,j) with direction "k"
                            exploreHitLine(HitCurrent, i, j, k, 'drawing');
                        }
                        // Recursive call
                        e = -negaMax(JSON.parse(JSON.stringify(HitCurrent)).eval, ((JSON.stringify(HitCurrent.playerCurrent) == JSON.stringify(playerBlack)) ? playerWhite : playerBlack), depth-1); //since negaMax returns an object, don't forget to access the value in the recursive call
    
                        if (e > evalFinal) {
                            HitCurrent.coordPlayable = [i,j];
                            evalFinal = e;
                        }
                        if (e == -infinity) {
                            HitCurrent.coordPlayable = [i,j];
                        }
                        // Restore arrayCurrent array
                        HitCurrent.arrayCurrent = JSON.parse(JSON.stringify(arrayTemp));
                    }
            // Clean playable hits once double loop is done
            cleanHits('playable', HitCurrent);
        }
        console.log('here2 :', evalFinal);
        return {eval: evalFinal, HitResult: HitCurrent }; //<-- send the additional HitCurrent as a result here
    }
    onmessage = function ( event ) {
        let params = event.data;
        postMessage( { result: negaMax( ...params ) } );
    };
    ` ], { type: "plain/text" } ) );
    main();
    }
    

    I decided to return the whole HitCurrent from inside the WebWorker and passed is as HitResult parameter, because based on the fact that other parameters are modified by the recursive method as well (like arrayPlayable and arrayCurrent), then you would also be able to get the modified values after the calculation.