0

Help me assign a value to an object in a nested loop.

The code that I need to actually debug looks like this:

  // Next states
  let nextVal = undefined
  let nextBoard = undefined
  let next_player_value = undefined

  shuffle(Board.rowCols).forEach(c => {
    shuffle(Board.rowCols[c]).forEach(r => {
      if (board[c][r] === undefined) {
        board[c][r] = player_state // let's test the current player moving to this location
        next_player_value = this.getMinMaxPlayerValue(!player_state)
        let minmax = AI.recurseMinMax(next_player_value, board, depth+1)
        let value = minmax[0]
        if (((player_state===this.getState()) && (nextVal === undefined || value > nextVal)) || // its the AI, and the value increases
          ((player_state!==this.getState()) && (nextVal === undefined || value < nextVal))) { // its the player, and the value decreases
          nextBoard = Object.assign({},board)
          nextVal = value
        }
        board[c][r] = undefined
      }
    })
  })
  return [nextVal, nextBoard]

What happens, if you watch the debugger, is that it correctly assigns different possible board positions to nextBoard, but when it exits the outer block on the way to the return statement, nextBoard changes. It doesn't become undefined! Instead, it becomes the current Board.state without any new moves in it (the move that should be there is in the undefined state like every other empty board space).

player_state is never undefined, but it is an argument to recurseMinMax (which is the containing function for this code). It is, in fact, always either true or false. I can't replace it easily — I have tried changing the assignment to !!player_state and Object.assign({}, player_state).

This is not an issue with nested loops or closures. Here's a couple of examples:

from the console

> let a; b = {}; keys=[1]; values =[2];
keys.forEach(key => {
 values.forEach(value => {
  b[key]=value;
  a = Object.assign({},b)
 })
})
< undefined
> a
< Object {1: 2}

Why doesn't this work:

broken example (with array instead of hash)

let nextBoard = undefined
[0,1,2].forEach(i =>{
  [0,1,2].forEach(j =>{
        if (board[i][j] == null) {
            board[i][j] = player;
            var value = recurseMinimax(board, !player)[0];
            if ((player && (nextVal == null || value > nextVal)) || (!player && (nextVal == null || value < nextVal))) {
                nextBoard =  Object.assign({},board)
                nextVal = value
            }
            board[i][j] = null;
        }
    })
})

This breaks just like in my actual code.

It should be the current Board.state but with one additional move in it, which is assigned at board[i][j] = player;.

Now here's the funny thing, I can get this to work pretty easily when we're working with an array like this:

working example (with array instead of nested hash)

[0,1,2].forEach(i =>{
  [0,1,2].forEach(j =>{
        if (board[i][j] == null) {
            board[i][j] = player;
            var value = recurseMinimax(board, !player)[0];
            if ((player && (nextVal == null || value > nextVal)) || (!player && (nextVal == null || value < nextVal))) {
                nextBoard = board.map(function(arr) {
                    return arr.slice();
                });
                nextVal = value
            }
            board[i][j] = null;
        }
    })
})

This one works just fine.

The problem is, my actual board is not a nested array, it is a nested hash:

Board.state = {}; 
['a','b','c'].forEach(c => { 
  Board.state[c]={};
  [1,2,3].forEach(r => Board.state[r]=undefined)
});
roberto tomás
  • 4,435
  • 5
  • 42
  • 71
  • The initial `shuffle(Board.rowCols).forEach(c => { shuffle(Board.rowCols[c]).forEach(r => {` is fairly suspect. `c` will be the entry within `Board.rowCols` (not the *key* of the entry, its value). So using `c` in `Board.rowCols[c]).forEach` immediately raises a red flag. – T.J. Crowder Dec 18 '16 at 15:07
  • @T.J.Crowder - I have run it in debugger and inspected the internal representation of `nextBoard` — it is definitely getting the columns for my board, and the resulting `nextBoard` is appropriate. I am in fact certain I am solving for the mixmax for the move, I'm just losing it on exiting the loop. Note that `Board.rowCols` is a different location in memory than `Board.state`, and we are actually testing copies of `Board.state` – roberto tomás Dec 18 '16 at 15:10
  • I'm just saying, that bit of the code is bizarre and almost certainly wrong. If you have `let array = ['a', 'b', 'c']; array.forEach(n => ...)`, `n` will be `'a'`, then `'b'`, then `'c'`, not `0`, then `1`, then `2`. So `array[n]` will be `undefined`. If you showed what was actually inside `Board.rowCols` we could probably help you better. My guess is it's something that, when `toString` is applied to it, ends up being a number that happens to be within the array bounds. But whether that's the entry you actually meant to get is not clear, and if it is, there are better way so to get there. – T.J. Crowder Dec 18 '16 at 15:14

1 Answers1

1

This happens because Object.assign({},board) takes a shallow copy, i.e. the rows in board will be the same references as the once you get as the return value from Object.assign.

You should take a deep copy.

A hacky way to do that is with JSON:

nextboard = JSON.parse(JSON.stringify(board));

... although this only works if board only contains the basic types supported by JSON (so no functions, sets, maps, ... ).

See How to deep clone in JavaScript for some other solutions

For instance this answer which is of my own hand:

function deepClone(obj, hash = new WeakMap()) {
    if (Object(obj) !== obj) return obj; // primitives
    if (hash.has(obj)) return hash.get(obj); // cyclic reference
    var result = Array.isArray(obj) ? [] 
               : obj.constructor ? new obj.constructor() : Object.create(null);
    hash.set(obj, result);
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(key, deepClone(val, hash)) );
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}

... and once you have that function, you can do:

nextboard = deepClone(board);

Some of the features in the function might be useless to you (e.g. the support for cyclic references and Map), so you could simplify it to your needs.

Community
  • 1
  • 1
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Okay, I will test that out. I am not sure why it matters though. when I am in the nested loop just _before_ it is about to finish, `player_state` is the same as it is by the time that loop finishes. So the reference that is being set to `player_state` .. why is that changing? – roberto tomás Dec 18 '16 at 15:12
  • I cannot use `JSON` because my object has keys whose values are `undefined` – roberto tomás Dec 18 '16 at 15:15
  • See the edit, where I suggest the use of a custom clone function. – trincot Dec 18 '16 at 15:23
  • thanks for all the help. I'll plug it in :) edit: SOLVED. thanks so much – roberto tomás Dec 18 '16 at 15:23