Cube Matcher Step 21

Step 21 on the Cube Matcher Project (Cube Matcher) was the only step I did not understand on this project nor could I complete on my own.

After several hours of trying to figure this out myself I finally checked the completed code that CodeCademy gives at the end of the projects. The complete code looks like this:

// Helper function to check remaining moves
const remainingMoves = () => {
  // Add code to return true or false at least 1 remaining move in board
  return board.some(col => doesColumnContainValidMoves(col));
}

const doesColumnContainValidMoves = (column) => {
  return column.find(cube => !cube.removed && checkClosest(cube).length > 0) !== undefined;
}

The only other place that remainingMoves() is called is on line 99:

update() {
    // If no more remaining valid moves, end game below
    if (remainingMoves() === false) {
      this.endGame();
    }
  }

(I understand what’s happening in update(), just to be clear.)

What’s confusing me is that I don’t understand, nor do I see what is getting fed to remainingMoves(). Or maybe more specifically, within remainingMoves(), where does the col come from that makes up the call back of board.some()? The thing that was tripping my up when I was trying to do this on my own was figuring out what to feed doesColumnContainValidMoves(). I just don’t get where its parameter/argument is coming from. What does reamainingMoves() take as its argument that is then passed to doesCoumnContainValidMoves() as col? My instinct tells me I’m overthinking it, because when I saw the solution I was surprised with how simple its construction was, but I still don’t understand it.

Full Code of GameScene.js:

// Global variables
const cubeSize = 38;
let board;
let score = 0;

class GameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'GameScene' });
  }

  preload() {
    // Load images for board and cubes
    this.load.spritesheet('blocks', 'https://codecademy-content.s3.amazonaws.com/courses/learn-phaser/cube-matcher/blocks.png', {
      frameWidth: cubeSize,
      frameHeight: cubeSize
    });
    this.load.image('grid', 'https://codecademy-content.s3.amazonaws.com/courses/learn-phaser/cube-matcher/grid.png');
  }

  create() {
    // Add background
    this.add.image(0, 0, 'grid').setOrigin(0).setScale(0.50);
    // Set boundaries of the game
    this.physics.world.setBounds(0, 0, 480, 600);
    // Create a 12 x 12 board
    board = this.makeBoard(12);
    // Create and display score
    score = 0;
    let scoreText = this.add.text(15, 610, `Score: ${score}`, {
      fontSize: '25px',
      fill: '#fff'
    });
    // Start and display a timer
    this.initialTime = 60; // in seconds
    let timerText = this.add.text(
      250,
      610,
      `Time Left: ${formatTime(this.initialTime)}`,
      { fontSize: '25px', fill: '#fff' }
    );
    // Phaser timer event
    this.time.addEvent({
      delay: 1000, // in milliseconds = 1 second
      callback: onTimedEvent,
      callbackScope: this,
      loop: true
    });
    // Helper function to format time in minutes and seconds
    function formatTime(seconds) {
      const minutes = Math.floor(seconds / 60);
      seconds %= 60;
      const secondsString = seconds.toString().padStart(2, '0');
      
      return `${minutes}:${secondsString}`; // 08:00 for example
    }
    // Callback function for timer counts down or ends game
    function onTimedEvent() {
      if (this.initialTime === 0) {
        this.endGame();
      } else {
        this.initialTime--;
        timerText.setText(`Time Left: ${formatTime(this.initialTime)}`);
      }
    }
    // Listener for clicks on cubes
    this.input.on('gameobjectdown', function(pointer, cube, event) {
      // Declare a constant, neighborCubes, below
      const neighborCubes = getNeighbors(cube);
      // Remove matching cubes from game if there's at least 2 of them
      if (neighborCubes.length > 0) {
        // Update score
        score += neighborCubes.length;
        scoreText.setText(`Score: ${score}`);
        // Update each cube in neighborCubes here
        neighborCubes.forEach(neighbor => {
          neighbor.destroy();
          renderCubes(neighbor);
        })
        removeCols();
      }

      // Helper function moves cube sprites down
      function renderCubes(cube) {
        for (let row = 0; row < cube.row; row++) {
          // Move cube sprite down by 30px
          board[cube.col][row].y += cubeSize;
          // Update the row of cube
          board[cube.col][row].row += 1;
        }
        // Update board array
        let removed = board[cube.col].splice(cube.row, 1);
        board[cube.col] = removed.concat(board[cube.col]);
      }
    });
  }

  update() {
    // If no more remaining valid moves, end game below
    if (remainingMoves() === false) {
      this.endGame();
    }
  }

  makeBoard(size) {
    // A nested array, inner arrays represent empty columns
    const board = Array(size).fill(Array(size).fill('x'));
    
    // Add code to fill board array with cube sprites and return it
    return board.map((col, i) => {
      return col.map((row, j) => this.makeCube(i, j));
    });
  }
  
  makeCube(colIndex, rowIndex) {
    const sideMargin = 31;
    const topMargin = 30;
    // Create a Phaser sprite
    const cube = this.physics.add.sprite(
      colIndex * cubeSize + sideMargin,
      rowIndex * cubeSize + topMargin,
      'blocks'
    );
    // Choose color randomly
    const max = 3;
    const min = 0;
    const color = Math.floor(Math.random() * (max - min + 1)) + min;
    // Don't let cube move beyond edges of board
    cube.setCollideWorldBounds(true);
    cube.body.collideWorldBounds = true;
    // Set the cube to a specific color
    cube.setFrame(color);
    // Make the cube clickable
    cube.setInteractive();
    // Add some information to make it easier to find a cube
    cube.col = colIndex;
    cube.row = rowIndex;
    cube.removed = false;

    return cube;
  }
  
  
  endGame() {
    // Stop sprites moving
    this.physics.pause();
    // Transition to end scene w/fade
    this.cameras.main.fade(800, 0, 0, 0, false, function(camera, progress) {
      if (progress > 0.5) {
        this.scene.stop('GameScene');
        this.scene.start('EndScene');
      }
    });
  }
}

// Helper function that only checks the immediate neighbors of a cube
const checkClosest = (cube) => {
  const results = [];
  // Coordinates of up, down, left, right cubes to check
  const directions = [
    { row: 0, col: -1 },
    { row: 0, col: 1 },
    { row: -1, col: 0 },
    { row: 1, col: 0 }
  ];
  const currCol = cube.col;
  const currRow = cube.row;
  const color = cube.frame.name;
  // Look for matching cube in 4 directions
  directions.forEach(direction => {
    // Coordinates of neighbor cube to check
    const newCol = currCol + direction.col;
    const newRow = currRow + direction.row;
    // Exit if the new col or row doesn't exist or will be removed
    if (
      !board[newCol] ||
      !board[newCol][newRow] ||
      board[newCol][newRow].removed
    ) {
      return;
    }
    // Check color of neighboring cube
    if (color === board[newCol][newRow].frame.name) {
      results.push(board[newCol][newRow]);
    }
  });
  
  // Return an array of neighboring cubes with the same color
  return results;
}



// Helper function to get neighborCubes of a block
const getNeighbors = (cube) => {
  // Variables
  let start = cube;
  let cubesToCheck = [start];
  let validNeighborCubes = [];
  // Check cubes in cubesToCheck for valid neighborCubes
  while (cubesToCheck.length > 0) {
    let curr = cubesToCheck.shift();
    // Only collect cubes we haven't already removed as a valid neighbor
    if (curr.removed === false) {
      validNeighborCubes.push(curr);
      curr.removed = true;
    }
    // Add code to get matching cubes, below
    const matches = checkClosest(curr);
    matches.forEach(match => {
      match.removed = true;
      validNeighborCubes.push(match);
      cubesToCheck.push(match);
    });
  }
  // If not enough matching cubes, clear and reset the clicked cube
  if (validNeighborCubes.length === 1) {
    validNeighborCubes[0].removed = false;
    validNeighborCubes = [];
  }

  return validNeighborCubes;
}

// Helper function shifts removes empty columns
const removeCols = () => {
  // Declare a emptyCols here:
  const emptyCols = board.map((col, i) => {
    const isEmpty = col.every(cube => cube.removed);
    return isEmpty ? i : false;
  }).filter(value => value !== false);
  // For each empty column, shift all remaining columns to the left
  emptyCols.forEach(emptyCol => {
    const columnsToMove = board.slice(emptyCol + 1);
    // Update the properties of cubes of moved column
    columnsToMove.forEach(col => {
      col.forEach(cube => {
        cube.x -= cubeSize;
        cube.col--;
      });
    });
  });
  // Remove all empty columns from the board array
  board.splice(emptyCols[0], emptyCols.length);
}

// Helper function to check remaining moves
const remainingMoves = () => {
  // Add code to return true or false at least 1 remaining move in board
  return board.some(col => doesColumnContainValidMoves(col));
}

const doesColumnContainValidMoves = (column) => {
  return column.find(cube => !cube.removed && checkClosest(cube).length > 0) !== undefined;
}

.some() is just another iterator, and as such it is a method in of itself. When you call an iterator method on an array object (in this case board), then whatever you specify as the element represents each element from that array. Therefore by saying board.some(col => doesColumnContainValidMoves(col)) what you are really saying is "For each element col in board, call doesColumnContainValidMoves() with that element as an argument. Therefore remainingMoves() does not need to take any arguments, as it is drawing specifically from the previously defined array board inside of the function.

If you wanted to generalise this function for use with any board, then you could define it as:

const remainingMoves = arr => {
   return arr.some(col => doesColumnContainValidMoves(col))
}

remainingMoves(board)
# This will return the same as the codecademy solution

However in the Codecademy solution the array is specified and so an argument is not needed.

2 Likes

Got it (I think, lol)!

Each element in the array of board gets passed to .some() as col, right?

That’s correct yes! It’s the same situation as all iterators, just in this situation you are using some whereas the lessons covered ones like forEach and map.

1 Like

That makes total sense! I was WAY overthinking it. Thanks so much!