Is a logic error causing incorrect game behavior, or am I missing an advanced coding technique?

I’m trying to make a copy of minesweeper and I made this mechanic where when you click to reveal a non-bomb tile that has surrounding bombs, it will then first move those bombs to empty corners(for now) then reveal the tile.

https://youtu.be/Hpgpr85juQA

The problem: The non-bomb tile don’t have surrounding bombs anymore, and because of that it should reveal it’s new surrounding tiles (similar to the second trial in the video above).

I’ve been stuck on this for quite some time and now I’m here. Thanks to anyone who would help!

Here is the code for the tiles, I’ve done a lot of changes to the code before, but I just revert it back to this:

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

public class Cell : MonoBehaviour
{
    public SpriteRenderer spriteRenderer;
    public bool isBomb = false;
    public bool isRevealed = false;

    private BoxCollider2D boxCollider2D;

    // public int surroundingBombs
    // {
    //     get
    //     {
    //         int bombCount = 0;
    //         foreach (GameObject cell in Neighbors()){
    //             Cell cellScript = cell.GetComponent<Cell>();
    //             if(cellScript.isBomb){
    //                 bombCount++;
    //             }
    //         }

    //         return bombCount;
    //     }

    //     private set {}
    // }

    public int count;

    void Awake()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
        boxCollider2D = GetComponent<BoxCollider2D>();
    }

    void Update()
    {
        count = SurroundingBombs();
    }

    void OnMouseOver()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (!GameManager.isStart)
            {
                Reveal();
            }
            else
            {
                StartCheck();
                Reveal();
            }
        }
    }

    void StartCheck()
    {
        if (SurroundingBombs() == 0)
        {
            Reveal();
        }
        else
        {
            foreach (GameObject cell in Neighbors())
            {
                Cell neighbor = cell.GetComponent<Cell>();
                if (neighbor.isBomb)
                {
                    BombSwap(neighbor);
                }
            }
        }

        GameManager.isStart = false;
    }

    void BombSwap(Cell cellToSwap)
    {
        Tilemaker tilemaker = FindFirstObjectByType<Tilemaker>();

        foreach (var tuple in tilemaker.Corners())
        {
            Cell cornerCell = tilemaker.cellGrid[tuple.x, tuple.y].GetComponent<Cell>();
            if (!cornerCell.isBomb)
            {
                //swap tile position
                Vector2 thisPosition = cellToSwap.gameObject.transform.position;
                cellToSwap.gameObject.transform.position = cornerCell.gameObject.transform.position;
                cornerCell.gameObject.transform.position = thisPosition;

                UpdateNeighborBombCounts(cornerCell);
                UpdateNeighborBombCounts(cellToSwap);
            }
        }
    }

    void UpdateNeighborBombCounts(Cell cell)
    {
        cell.count = cell.SurroundingBombs();
        print(cell.count);
    }

    void Reveal()
    {
        isRevealed = true;
        spriteRenderer.color = Color.blue;

        count = SurroundingBombs();

        //check surrounding 0 cells
        if (count == 0)
        {
            foreach (GameObject cell in Neighbors())
            {
                Cell neighbor = cell.GetComponent<Cell>();
                if (neighbor != this && !neighbor.isRevealed)
                {
                    neighbor.Reveal();
                }
            }
        }
    }

    public int SurroundingBombs()
    {
        int bombCount = 0;
        foreach (GameObject cell in Neighbors())
        {
            Cell cellScript = cell.GetComponent<Cell>();
            if (cellScript.isBomb)
            {
                bombCount++;
            }
        }

        return bombCount;
    }

    List<GameObject> Neighbors()
    {
        int layer = LayerMask.GetMask("Tile");

        RaycastHit2D[] rays = Physics2D.BoxCastAll(boxCollider2D.bounds.center, boxCollider2D.size, 0f, Vector2.zero, Mathf.Infinity, layer);

        return rays.Where(hit => hit.collider.CompareTag("Tile")).Select(hit => hit.collider.gameObject).ToList();
    }
}

Sounds like you wrote a bug… and that means… time to start debugging!

For a minesweeper kinda game, strip it down to a tiny board where you can reason precisely about what the code should do.

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

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

Remember with Unity the code is only a tiny fraction of the problem space. Everything asset- and scene- wise must also be set up correctly to match the associated code and its assumptions.

1 Like

All I can see is that you’re conflating three vastly different systems together and wondering why they misbehave, when all you do is thread a needle through a pretty complicated choreography of variables and states.

The three systems are: tile-based data model, player input, and display.

This is because we don’t actually work like this: everything happens in one place and there are multiple states of overlapping ideas you’re trying to work with, instead you really need to set clear boundaries between different responsibilities. This is not only done to make development easier due to compartmentalization, but you can also narrow down the eventual pain points in the design, make debugging easer, and allow yourself to track your feature set more comprehensively and get an intuitive flow of how things work in the first place.

First, the underlying model of minesweeper is something that you need to nail right. Minesweeper is a game about hidden information, however nothing is hidden from the game itself — so this is how you start: you introduce a tile-based board with two layers of information, ground state and player discovery state.

Ground state is whether a tile has a mine or not, and player discovery is whether the tile is enshrouded in a mystery, revealed, or marked. Sure, you can combine the two into the same data set, but these are technically two overlapping sets of exclusive information.

Now you can probe this model through some standard verbiage like “revealAt(x, y)”, “restart”, or “getNeighbor(East)” and it will basically maintain a valid state for you and let you query whatever is interesting to the outside world, without too much fluff.

Next, you want to separate the controller from the model. The controller aggregates simple player actions like revealing single tiles (i.e. left clicking; and if unlucky, player may step onto a mine, as reported by the model), or marking (right clicking), but also governs advanced gameplay logic, like flood revealing (which happens when conditions are right) and in some variants there was also a special move called chording. Flood revealing, as an example, is a separate piece of logic that runs algorithmically and it’s not something model needs to know about.

At some point you introduce a way to display the game, again as a separate thing that processes information from the model. In the early development the display can be brutally simple, something that waits for a change in the model, and throws all sprites away only to recreate them from scratch. This way you are absolutely sure that the model is always reflected on the screen without errors, but it’s not very performant or extensible. You can even use gizmos to draw lines, just to have some semblance of gameplay. Later in the development you obviously introduce a richer implementation of how the game looks, with all the bells and whistles, perhaps you need meshes and lighting, advanced shading, post processing, animations and so on.

At no point should these visual concerns be entangled with how you reveal the tiles, how you capture mouse input, or whether there is a mine and whatnot. There is a clear jurisdiction of every part of your game, called a separation of concern, thus your primary game logic should be confined to the model, and your gameplay ergonomics (and quality of life features) should be confined to the controller.

Obviously there are countless other, similar ways to organize the code with clear separation of concern in mind, but a Model-View-Controller would be the most typical for a minesweeper-like.

You will be surprised how much longer the momentum can carry you into more complex and better realized features if only you pre-emptively apply some labor and patience into thoughtful organizational structures leading to an overarching codebase architecture.

1 Like

Thanks for the insights! I appreciate how you explained all three systems. I’m actually fairly new to design patterns, and I’m sure this will help me get better.