Why are the values not assigned correctly to my array?

Viewed 88

I am trying to solve this leetcode problem (game of life): https://leetcode.com/problems/game-of-life

This is my code:

function isLive(board: number[][], row: number, column: number) {
  let liveNeighbours = 0;
  [row - 1, row, row + 1].forEach(r => {
    if (r < 0 || r >= board.length) return;
    [column - 1, column, column + 1].forEach(c => {
      if (c === column && r === row) return;
      if (c < 0 || c >= board[0].length) return;
      if (board[r][c] === 1) liveNeighbours++
    })
  })

  if (board[row][column] === 1 && (liveNeighbours === 3 || liveNeighbours === 2)) return true;
  if (board[row][column] === 0 && liveNeighbours === 3) return true;

  return false;
}


/**
Do not return anything, modify board in-place instead.
*/
function gameOfLife(board: number[][]): void {

  let nextGen = Array(board.length).fill(Array(board[0].length).fill(0));

  for (let i = 0; i < board.length; i++) {
    for (let j = 0; j < board[0].length; j++) {
      console.log("isLive row ", i, " col ", j, isLive(board, i, j))
      if (isLive(board, i, j)) nextGen[i][j] = 1
    }
  }

  console.log("next gen", nextGen);

  board = nextGen;
};

I run the code with this input:

[
 [0,1,0],
 [0,0,1],
 [1,1,1],
 [0,0,0]
]

And it outputs this (same thing basically):


[
 [0,1,0],
 [0,0,1],
 [1,1,1],
 [0,0,0]
]

While it's expecting this:

[
 [0,0,0],
 [1,0,1],
 [0,1,1],
 [0,1,0]
]

Now what really drives me crazy, is the print logs. When I print for each cell whether it should be live or not, it comes back perfectly as it should be. This is the log:

isLive row  0  col  0 false
isLive row  0  col  1 false
isLive row  0  col  2 false
isLive row  1  col  0 true
isLive row  1  col  1 false
isLive row  1  col  2 true
isLive row  2  col  0 false
isLive row  2  col  1 true
isLive row  2  col  2 true
isLive row  3  col  0 false
isLive row  3  col  1 true
isLive row  3  col  2 false

Also, in the last log for printing nextGen, it print this: next gen [ [ 1, 1, 1 ], [ 1, 1, 1 ], [ 1, 1, 1 ], [ 1, 1, 1 ] ] So basically re-assigns all cells to 1, even though I can se not all statements are true.

Which is unlike anything. It doesn't fit the individual return I get for each cell, and doesn't even fit what it claims I'm returning even though it's basically at the end. Am I missing something about how to assign values to arrays?

EDIT: Instead of simply assigning board=nextGen I've also tried the following:

for (let i = 0; i < board.length; i++) {
    for (let j = 0; j < board[0].length; j++) {
        board[i][j] = nextGen[i][j];
    }
}

I now get this as the output [[1,1,1],[1,1,1],[1,1,1],[1,1,1]] (the prit of nextGen).

EDIT 2: I've tried changing the function to simply return the count of neighbors and do the conditioning after. While I haven't got to moving the conditioning, I can see already that the is still an issue with assigning item to the new array.

This is my modified code (again, not complete, but the rest isn't relevant to the issue at this point):

function countNeighbors(board: number[][], row:number, column:number){
    let liveNeighbors = 0;
    [row-1, row, row+1].forEach(r=>{
        if(r<0 || r>= board.length) return;
        [column-1, column, column+1].forEach(c=>{
            if(c === column && r=== row) return;
            if(c<0 || c>= board[0].length) return;
            if(board[r][c]===1) liveNeighbors++
        })
    })

    return liveNeighbors;
}


/**
 Do not return anything, modify board in-place instead.
 */
function gameOfLife(board: number[][]): void {

let neighbors = Array(board.length).fill(Array(board[0].length).fill(0));

    for (let i = 0; i<board.length; i++){
        for (let j = 0; j<board[0].length; j++){
            let numberOfNeighbors = countNeighbors(board,i,j);
            console.log(i, j, numberOfNeighbors)
            neighbors[i][j] = numberOfNeighbors
        }
        console.log("neighbors", neighbors)
    }
};

I've added a console log after each iteration of i, and this is what it prints:

0 0 1
0 1 1
0 2 2
neighbors [ [ 1, 1, 2 ], [ 1, 1, 2 ], [ 1, 1, 2 ], [ 1, 1, 2 ] ]
1 0 3
1 1 5
1 2 3
neighbors [ [ 3, 5, 3 ], [ 3, 5, 3 ], [ 3, 5, 3 ], [ 3, 5, 3 ] ]
2 0 1
2 1 3
2 2 2
neighbors [ [ 1, 3, 2 ], [ 1, 3, 2 ], [ 1, 3, 2 ], [ 1, 3, 2 ] ]
3 0 2
3 1 3
3 2 2
neighbors [ [ 2, 3, 2 ], [ 2, 3, 2 ], [ 2, 3, 2 ], [ 2, 3, 2 ] ]

SO basically each row gets duplicated to all rows. That seems to be what's messing my function in either way I've done it, but why is that happening?

Is it something to do with the behavior of fill?

Edit 3: Ok, so i's about how the new array is created. If I create it like this for my test case: let neighbors = [[],[],[],[]]; Then everything works just fine.

Would love to know though, what is the cause of this behavior? Is there a neat way of creating this array that won't cause this? I can just push new empty arrays for each iteration of i while would work and isn't very consuming, but I thought that how I did it first was more elegant.

1 Answers

Basically what's happening here is that in function gameOfLife(), when you assign board = nextGen;, you are really just assigning the board variable (the name) within the function to refer to the object nextGen but aren't changing the data in board. Basically, you're making the name board refer to nextGen while leaving the data that used to be referred to by board the same.

What you need to do is instead of saying board =, use methods to copy the contained data from nextGen to board. So a simple way to do that is this:

for (let i = 0; i < board.length; i++) {
    board[i] = nextGen[i];
}

Edit:

Given that originally, nextGen seemed to be correct when printing for each cell, that is very strange. I might try ensuring that brackets and semicolons are all included just to ensure control flow for all the if statements work as intended, since there is only one place where nextGen should be able to be set to 1 and the neighboring if statement should sometimes not include it.

Otherwise, how about trying a different strategy altogether that wouldn't use a nextGen object at all. What you could try doing is, instead of making a new board for the next generation, just get the number of neighbors each tile has. Then you can just directly modify the board based on the rules for the number of neighbors and the current life value without needing to worry about copying data. So:

let neighbors = Array(board.length).fill(Array(board[0].length).fill(0));

// ============================================================================================================
// Put the code for finding the number of neighbors here, inserting it into the corresponding spot in neighbors
// ============================================================================================================

for (let i = 0; i < board.length; i++) {
    for (let j = 0; j < board[0].length; j++) {
        if (board[i][j] === 1) { // Only change things if conditions are met for death
            if (neighbors[i][j] < 2 || 3 < neighbors [i][j]) {
                board[i][j]--;
            }
        } else { // === 0; only change things if conditions are met for new life
            if (neighbors[i][j] === 3 {
                board[i][j]++;
            }
        }
    }
}
Related