Cube Matcher Project - Cubes Won't Disappear

Hi. I am looking for someone to help me with this project. I think I run into trouble around Steps 12 or 13. I can’t make the cubes disappear. I have double and triple-checked my code and cannot find the problem. Any help is appreciated.

CREATE VIDEO GAMES WITH PHASER.JS

Cube Matcher

For this project, you will be given several files with code to help you start your project. In its current state, Cube Matcher doesn’t work and cannot be played — until we finish it with our knowledge of JavaScript and iterators, which you’ve just learned.

To create our game, we’ve used JavaScript and Phaser.js, a game-development library, in the starting code. Phaser bundles together JavaScript code to create browser-based games, allowing us to develop more quickly and easily.

To complete Cube Matcher you will use the following iterator methods:

  • .forEach()
  • .map()
  • .filter()
  • .some()
  • .every()

As well as the following:

  • Chaining multiple iterator methods to the same array.

If you’d like to, you can play a working version of Cube Matcher to understand what you’ll be building.

Tasks

11/23 Complete

Mark the tasks as complete by checking them off

Getting Started

Before you start writing code of your own, browse the various files of the project to view them in the text editor. Take a moment to look over the structure of the project, in particular, the file GameScene.js. To complete Cube Matcher, you’ll only have to use this file.

There are comments in all the files including GameScene.js to help you understand what the code is doing if you’re interested, however, it’s not necessary to be familiar or know anything specific about the existing code to successfully finish this project. For now, take a moment to look over the comments and code; why do you think this might be more helpful rather than putting all the code in one file?

To finish this step, return to GameScene.js. Familiarize yourself with the general shape of the file, as you will be adding code throughout.

Display the Board

Start by opening the Cube Matcher game in your browser and clicking on the start screen. You should see the game screen with a score displayed and a timer counting down, but with an empty grid with no actual cubes.

Scroll down the GameScene.js file until you find the method called .makeBoard() below the .update() function. .makeBoard() contains all the code needed to create a 12 by 12 board of colored cubes and display them in the browser – exactly what we need. However, .makeBoard() is missing some code.

Find the variable called board in .makeBoard(). This is a nested array that represents a grid of cubes, with each inner array representing a column of cubes. For example, a 2 x 2 grid would be:

[  ['x', 'x'], // column 0  ['x', 'x']  // column 2]

Stuck? Get a hint

In this step, you’ll fill the empty spaces in the columns of the game with cubes.

Because board is a nested array, we need to iterate at each level of nesting to replace the empty spaces of the board with cubes: first, over each column, and then over each empty space in that column.

We’ve already given you code that maps over the columns of the board, board.map((col, i) => {}). However, in order to reach the cubes in each column, we also need to iterate over each cube in the given column, col.

Inside of the body of the callback function given to you in makeBoard(), use .map(), to iterate over each column, col. The callback function in the nested iterator should have 2 parameters: row and j for a cube in the column and j, the index of that cube in the column.

Stuck? Get a hint

Inside the inner .map(), call the helper function this.makeCube() and pass in i and j. Return this value.

Stuck? Get a hint

Save your code and restart the game. Make sure you now see a board filled with colored cubes displayed.

Collect all Matching Neighbor Cubes

Take a look at the board of the game in your browser. Find different groups of connected cubes of the same color; are they all the same number and shape?

In Cube Matcher, groups of cubes will have different numbers of cubes and shapes, generated at random. We’ve provided a function, checkClosest(), that checks the 4 cubes to the left, right, top, and bottom of a selected cube. How can we check larger groups of cubes with irregular shapes?

The code in the getNeighbors() function will do that for us. Scroll down GameScene.js and find getNeighbors() underneath the code for checkClosest(). This function finds all the cubes of the same color that are connected to each other, including the cube the player clicked on.

Below the conditional in getNeighbors(), create a constant variable, matches, and assign it checkClosest(curr). checkClosest() returns an array of cubes of the same color connected to the cube passed into it.

Iterate over each of the matching cubes in the array stored in matches using the .forEach() iterator. The callback passed to the iterator should have a single parameter named match to represent the current matching cube.

Finally, add the following code to the callback’s body to complete getNeighbors():

match.removed = true;validNeighborCubes.push(match);cubesToCheck.push(match);

This code marks each matching cube as no longer playable and collects it in an array with any other matching cubes.

Update the Board

The game is still not playable since we aren’t actually using getNeighbors() yet. In the next steps, we’ll fix this by adding code to the function that runs when a player clicks on a cube.

In GameScene.js, scroll up to find the following line inside the .create() method and after the function onTimedEvent():

this.input.on('gameobjectdown', function(pointer, cube, event) {

This callback function (called an event handler) will be called whenever a cube is clicked. You will complete this callback in the following steps.

Inside of the event handler and after the line you located in the previous step, create a constant variable neighborCubes and store the result of calling getNeighbors() with the clicked cube.

Stuck? Get a hint

Remove the matching cubes from the board. Find the conditional that checks neighborCubes.length > 0 below the line where you declared neighborCubes. We’ve given you the code that updates the score, but the code to update the board by removing matching cubes needs to be added. Under the code for the score, use an iterator on neighborCubes to visit each neighboring cube and for each cube run some code. Pass in a callback function to the iterator with neighbor as the only parameter.

Once you’ve done that, add this code to the callback function’s body:

// Remove neighboring cube from displayneighbor.destroy();

This Phaser code will remove the neighbor cube from the display.

The last step of this section is to move the remaining cubes down after their neighbor is removed from the board:

// Shift remaining cubes downrenderCubes(neighbor);

Check your work by restarting the game and clicking on cubes. Any cube that has at least one other matching cube connected to it should now disappear and any cubes above them should move down the board.

// 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://content.codecademy.com/courses/learn-phaser/cube-matcher/blocks.png', {
      frameWidth: cubeSize,
      frameHeight: cubeSize
    });
    this.load.image('grid', 'https://content.codecademy.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);
    board.map((col, i) => {
      return board.map((row, j) => {
        return this.makeCube(i, j);
      })
    })
    // 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:

  // 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
  
}

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

Sorry for the long post. I wanted to make sure and post all the information.

Hi,
In create() you’ve got some extra code;

    board.map((col, i) => {
      return board.map((row, j) => {
        return this.makeCube(i, j);
      })
    })

which is part of makeBoard() further down (you can delete it).

In makeBoard() you need to add a return so it’s;
return this.makeCube(i, j);

I think, other than that, it’s the same as what I have so hopefully that should work

3 Likes

Thank you for finding that. However, not the problem. I tried running it in a different browser as well with no luck. Going nuts over this code. lol

I copy pasted your code in the online Phaser 3 Sandbox (Phaser 3 Examples).

As @pluginmaybe pointed out, there are two edits that need to be made.
zzzzzabc

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) => {
        return this.makeCube(i, j);          <-------- Added return keyword
      })
    });
  }

Then I played around with the config and after making some changes to the config, I clicked on the matching cubes and the cubes did disappear. So, seems to be working fine.

If the two edits above don’t solve the program, then possibly something may have been inadvertently changed in the config. I am not a subscriber, so I can’t see what config is being used for this game. But as a sample, in the sandbox I used a config like:

var config = { width: 800, height: 800, scene: [ GameScene ], 
physics: {
    default: 'arcade'
},  
canvasStyle: `display: block; width: 100%; height: 100%;`,};

var game = new Phaser.Game(config);

Do any of the files in the project have a config object?

Sorry about that, I removed the extra code, but didn’t catch where you said to add the return keyword until @mtrtmk pointed it out. Do you know how many hours I spent debugging and couldn’t see that. SOOOO frustrating. Thought I was getting better and made such a rookie mistake. THANK YOU THANK YOU THANK YOU!!!

2 Likes

THANK YOU!!! I had removed the extra code, but was in such a hurry to see if it worked, that I still hadn’t added the return. I searched so long for that mistake and it was so simple. Return and thises still mess me up. Thank you so much for looking into this! You are both amazing and I will finally sleep better tonight because of it!!!

And think you for still helping me after I made such a mess of copying the code in the last thread. I am still very much learning. I now know the difference between a backtick and an apostrophe. LOL.

2 Likes