Shuffling system is broken in a match 3 game

Hello,
I’m trying to make a match 3 game similar to Toon Blast. I added a functionality to the script, which shuffles the dots on the board when there are no more matches left. The shuffling works but it’s broken. When the board is shuffled some of the dots go behind the dots standing above or next to them thus leaving blanks on the board. Like this:

I’m not sure what the source of the problem is. This is the Board script where shuffle method is:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum GameState
{
    wait,
    move
}

public enum TileKind
{
    Breakable,
    Blank,
    Normal
}

[System.Serializable]
public class TileType
{
    public int x;
    public int y;
    public TileKind tileKind;
}


public class Board : MonoBehaviour
{

    public GameState currentState = GameState.move;
    public int width;
    public int height;
    public int offSet;
    public GameObject tilePrefab;
    public GameObject[] dots;
    public GameObject destroyEffect;
    public TileType[] boardLayout;
    private bool[,] blankSpaces;
    public GameObject[,] allDots;

    // Start is called before the first frame update
    void Start()
    {
        blankSpaces = new bool[width, height];
        allDots = new GameObject[width, height];
        SetUp();
    }

    private void SetUp()
    {
        GenerateBlankSpaces();
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (!blankSpaces[i, j])
                {
                    Vector2 tempPosition = new Vector2(i, j + offSet);
                    GameObject backgroundTile = Instantiate(tilePrefab, tempPosition, Quaternion.identity) as GameObject;

                    backgroundTile.transform.parent = this.transform;
                    backgroundTile.name = "(" + i + ", " + j + ")";
                    int dotToUse = Random.Range(0, dots.Length);
                    GameObject dot = Instantiate(dots[dotToUse], tempPosition, Quaternion.identity);
                    dot.GetComponent<Dot>().row = j;
                    dot.GetComponent<Dot>().column = i;
                    dot.transform.parent = this.transform;
                    dot.name = "(" + i + ", " + j + ")";
                    allDots[i, j] = dot;
                    dot.GetComponent<Dot>().board = this;
                }
            }
        }
    }

    public void GenerateBlankSpaces()
    {
        for (int i = 0; i < boardLayout.Length; i++)
        {
            if (boardLayout[i].tileKind == TileKind.Blank)
            {
                blankSpaces[boardLayout[i].x, boardLayout[i].y] = true;
            }
        }
    }

    private void DestroyMatchesAt(int column, int row)
    {
        if (allDots[column, row].GetComponent<Dot>().isMatched)
        {
            GameObject particle = Instantiate(destroyEffect, allDots[column, row].transform.position, Quaternion.identity);
            Destroy(particle, .5f);
            Destroy(allDots[column, row]);
            allDots[column, row] = null;
        }
    }

    public void DestroyMatches()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    DestroyMatchesAt(i, j);
                }
            }
        }
        StartCoroutine(DecreaseRowCo2());
    }

    private IEnumerator DecreaseRowCo2()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                //if the current spot isn't blank and is empty...
                if (!blankSpaces[i, j] && allDots[i, j] == null)
                {
                    //loop from the space above to the top of the column
                    for (int k = j + 1; k < height; k++)
                    {
                        //if a dot is found...
                        if (allDots[i, k] != null)
                        {
                            //move that dot to this empty space
                            allDots[i, k].GetComponent<Dot>().row = j;
                            //set that spot to be null
                            allDots[i, k] = null;
                            //break out of the loop
                            break;
                        }
                    }
                }
            }
        }
        yield return new WaitForSeconds(.4f);
        StartCoroutine(FillBoardCo());
    }

    private IEnumerator DecreaseRowCo()
    {
        int nullCount = 0;
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] == null)
                {
                    nullCount++;
                }
                else if (nullCount > 0)
                {
                    allDots[i, j].GetComponent<Dot>().row -= nullCount;
                    allDots[i, j] = null;
                }
            }
            nullCount = 0;
        }
        yield return new WaitForSeconds(.4f);
        StartCoroutine(FillBoardCo());
    }

    private void RefillBoard()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] == null && !blankSpaces[i, j])
                {
                    Vector2 tempPosition = new Vector2(i, height - 1 + offSet);
                    int dotToUse = Random.Range(0, dots.Length);
                    GameObject piece = Instantiate(dots[dotToUse], tempPosition, Quaternion.identity);
                    allDots[i, j] = piece;
                    piece.GetComponent<Dot>().row = j;
                    piece.GetComponent<Dot>().column = i;
                }
            }
        }
    }



    private bool MatchesOnBoard()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    if (allDots[i, j].GetComponent<Dot>().isMatched)
                    {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private IEnumerator FillBoardCo()
    {
        RefillBoard();
        yield return new WaitForSeconds(.5f);

        while (MatchesOnBoard())
        {
            yield return new WaitForSeconds(.5f);
            DestroyMatches();
        }
        yield return new WaitForSeconds(.5f);

        if (IsDeadLocked())
        {
            ShuffleBoard();
            Debug.Log("Deadlocked");
        }
        currentState = GameState.move;
    }

    private bool CheckForMatches()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    //Make sure that one dot to the right is in the board
                    if (i < width - 1)
                    {
                        //Check if the dots to the right exist
                        if (allDots[i + 1, j] != null)
                        {
                            if (allDots[i + 1, j].tag == allDots[i, j].tag)
                            {
                                return true;
                            }
                        }
                    }
                    if (j < height - 1)
                    {
                        //Check if the dots above exist
                        if (allDots[i, j + 1] != null)
                        {
                            if (allDots[i, j + 1].tag == allDots[i, j].tag)
                            {
                                return true;
                            }
                        }
                    }
                }
            }
        }
        return false;
    }

    private bool IsDeadLocked()
    {
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    if (CheckForMatches())
                    {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    private void ShuffleBoard()
    {
        //Create a list of game objects
        List<GameObject> newBoard = new List<GameObject>();
        //Add every piece to this list
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    int randomIndex = Random.Range(0, newBoard.Count + 1);
                    newBoard.Insert(randomIndex, allDots[i, j]);
                }
            }
        }
        //For every spot on the board...
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                //If this spot shouldn't be blank
                if (!blankSpaces[i, j])
                {
                    //Pick random number
                    int pieceToUse = Random.Range(0, newBoard.Count);
                    //Make a container for the piece
                    Dot piece = newBoard[pieceToUse].GetComponent<Dot>();
                    //Assign the column to the piece
                    piece.column = i;
                    //Assing the row to the piece
                    piece.row = j;
                    //Fill in the dots array with this new piece
                    allDots[i, j] = newBoard[pieceToUse];
                    //Remove it from the list
                    newBoard.Remove(newBoard[pieceToUse]);
                }
            }
        }
        //Check if it's still deadlocked
        if (IsDeadLocked())
        {
            ShuffleBoard();
        }
    }
}

And just in case i share the Dot script too bcs it’s where the matching and movement of the dots are handled:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Dot : MonoBehaviour
{
    public int column;
    public int row;
    public Board board;
    public bool isMatched = false;
    public int targetX;
    public int targetY;
    private Vector2 tempPosition;

    // Start is called before the first frame update
    void Start()
    {
        board = FindAnyObjectByType<Board>();
    }

    // Update is called once per frame
    void Update()
    {
        targetX = column;
        targetY = row;
       
        if (Mathf.Abs(targetY - transform.position.y) > .1)
        {
            //Move Towards the target
            tempPosition = new Vector2(transform.position.x, targetY);
            transform.position = Vector2.Lerp(transform.position, tempPosition, .1f);
            if (board.allDots[column, row] != this.gameObject)
            {
                board.allDots[column, row] = this.gameObject;
            }
        }
        else
        {
            //Directly set the position
            tempPosition = new Vector2(transform.position.x, targetY);
            transform.position = tempPosition;
        }
    }

    private void OnMouseDown()
    {
        if (board.currentState == GameState.move)
        {
            FindMatches();
            board.DestroyMatches();

            // Set the game state to wait
            board.currentState = GameState.wait;
        }
    }


    void FindMatches()
    {
        List<GameObject> matchingDots = new List<GameObject>();

        if (column > 0)
        {
            GameObject leftDot = board.allDots[column - 1, row];
            if (leftDot != null && leftDot.tag == this.gameObject.tag)
            {
                matchingDots.Add(leftDot);
            }
        }

        if (column < board.width - 1)
        {
            GameObject rightDot = board.allDots[column + 1, row];
            if (rightDot != null && rightDot.tag == this.gameObject.tag)
            {
                matchingDots.Add(rightDot);
            }
        }

        if (row > 0)
        {
            GameObject upDot = board.allDots[column, row - 1];
            if (upDot != null && upDot.tag == this.gameObject.tag)
            {
                matchingDots.Add(upDot);
            }
        }

        if (row < board.height - 1)
        {
            GameObject downDot = board.allDots[column, row + 1];
            if (downDot != null && downDot.tag == this.gameObject.tag)
            {
                matchingDots.Add(downDot);
            }
        }

        if (matchingDots.Count > 0)
        {
            isMatched = true;

            foreach (GameObject dot in matchingDots)
            {
                Dot dotScript = dot.GetComponent<Dot>();
                if (!dotScript.isMatched)
                {
                    dotScript.FindMatches();
                }
            }
        }
        else
        {
            isMatched = false;
        }
    }
}

I resorted ChatGPT but still couldn’t find what the problem is. Can somebody please help me?

You are almost certainly getting a massive spew of exceptions at line 292. The newBoard list is empty and yet you’re “inserting” somewhere random in it.

Here are some notes on IndexOutOfRangeException and ArgumentOutOfRangeException:

http://plbm.com/?p=236

Steps to success:

  • find which collection it is and what line of code accesses it <— critical first step!)
  • find out why it has fewer items than you expect
  • fix whatever logic is making the indexing value exceed the collection size
  • remember that a collection with ZERO elements cannot be indexed at all: it is empty
  • remember you might have more than one instance of this script in your scene/prefab
  • remember the collection may be used in more than one location in the code
  • remember that indices start at ZERO (0) and go to the count / length minus 1.

This means with three (3) elements, they are numbered 0, 1, and 2 only.

For a reference shuffle, look up Fisher Yates… most examples show a 1-dimensional array but the same applies to a 2D array, or you could stream the items into a 1D array, shuffle them, then put them back in your 2D.

If that’s not it, time to start debugging! Here is how you can begin your exciting new debugging adventures:

You must find a way to get the information you need in order to reason about what the problem is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is
  • you’re getting an error or warning and you haven’t noticed it in the console window

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the names of the GameObjects or Components involved?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as Debug.Log("Problem!",this);

If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

Visit Google for how to see console output from builds. If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: How To - Capturing Device Logs on iOS or this answer for Android: How To - Capturing Device Logs on Android

If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

“When in doubt, print it out!™” - Kurt Dekker (and many others)

Note: the print() function is an alias for Debug.Log() provided by the MonoBehaviour class.

I’m sorry, that ShuffleBoard method is not the original, it’s the ChatGPT’s modified version. The original method is like this:

private void ShuffleBoard()
    {
        //Create a list of game objects
        List<GameObject> newBoard = new List<GameObject>();
        //Add every piece to this list
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                if (allDots[i, j] != null)
                {
                    newBoard.Add(allDots[i, j]);
                }
            }
        }
        //For every spot on the board...
        for (int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                //If this spot shouldn't be blank
                if (!blankSpaces[i, j])
                {
                    //Pick a random number
                    int pieceToUse = Random.Range(0, newBoard.Count);
                    //Make a container for the piece
                    Dot piece = newBoard[pieceToUse].GetComponent<Dot>();
                    piece.column = i;
                    //Assing the row to the piece
                    piece.row = j;
                    //Fill in the dots array with this new piece
                    allDots[i, j] = newBoard[pieceToUse];
                    //Remove it from the list
                    newBoard.Remove(newBoard[pieceToUse]);
                }
            }
        }
        //Check if it's stil deadlocked
        if (IsDeadLocked())
        {
            ShuffleBoard();
        }
    }

And the original way of debugging has been listed above. Dive in!

Ok one thing before that though, are we sure that the source of the problem is ShuffleBoard method? Should i be focusing on that?

I have absolutely no idea. You are the one reporting the problem.

Follow the data in, follow what your code does with the data, follow the data out.

If you must, simplify it drastically to a 2x2 grid so you can easily see it all at once in the logging you put into the code.

Now I have to know what that means. It sounds as if you wrote the original version and then asked ChatGP to fix it, which sounds awful. Or did ChatGP write the original and you tweaked it (which also sounds awful but in a nicer way).

Anyway, the way I approach things like this is first, not to make “empty” a special thing. You’re writing code to treat “empty” differently, which is messy. I just make “nothing here” as just another thing a square can hold. That way I can swap any two spaces (both with a dot, one without, or even swapping 2 empties – all the same code).

Then as far as a random pattern, you have 25 spaces so just shuffle 25 numbers at the start – have length 25 array Shuffle contain random 0 to 24. Do the nested loop and for each space swap it with row=Shuffle_/5 and column=Shuffle*%5. That’s a basic “put these in random order” tweaked for a 2D array.*_

It’s the first one. If you mean “wrong approach” by awful, my response is “i couldn’t come up with a better way to fix it”.

Can you elaborate this part?

There’s no swapping in my game. Dots are randomly instantiated on the board and you pop any 2 or more adjacent dots with the same color.

Thank you for your input.

I fixed the issue. It was the code in the Update method of the Dot script which handles the movement of dots. I had deleted this if block bcs of a glitch before:

if (Mathf.Abs(targetX - transform.position.x) > .1)
        {
            //Move Towards the target
            tempPosition = new Vector2(targetX, transform.position.y);
            transform.position = Vector2.Lerp(transform.position, tempPosition, .1f);
            if (board.allDots[column, row] != this.gameObject)
            {
                board.allDots[column, row] = this.gameObject;
            }
         
        }

I added it again and the issue is fixed. Sry if i wasted anyone’s time. You can change the status of the topic to solved now

You should be able to edit the thread title yourself to resollved instead of bug?