Computer Player for Tetris like project

I have some code for a tetris type project below. I’m finding difficulty in setting up a computer controlled player. Any resources would be great. Specifically, my AnalyzeBoard() function is crashing Unity. I’m not sure how to move forward. Any recommendations?

Com.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;



public class Com : MonoBehaviour
{
    public enum AIState
    {
        AnalyzingBoard,
        ChoosingPiecePlacement,
        RotatingPiece,
        MovingPiece,
        DroppingPiece
    }

    private AIState currentState;
    //public GameObject board;
    private Board _board;
    private Piece currentPiece;

    // New fields for board analysis
    private int boardWidth;
    private int boardHeight;

    private int targetRotation;
    private int targetX;
    private float moveTimer;
    private float rotateTimer;
    private float dropTimer;

    private const float MOVE_DELAY = 0.1f;
    private const float ROTATE_DELAY = 0.2f;
    private const float DROP_DELAY = 0.05f;

    private void Start()
    {
        currentState = AIState.AnalyzingBoard;
        _board = this.GetComponent<Board>();
        boardWidth = _board.boardSize.x;
        boardHeight = _board.boardSize.y;
        currentPiece = this.GetComponent<Piece>();


    }

    private void Update()
    {

        if (_board != null && currentPiece != null)
        {
            AnalyzeBoard();
        }
        else
        {
            Debug.LogError("_board or currentPiece is null. Please check the setup.");
            return;
        }
        /*
        switch (currentState)
        {
            case AIState.AnalyzingBoard:
                AnalyzeBoard();
                break;

            case AIState.ChoosingPiecePlacement:
                ChoosePiecePlacement();
                break;

            case AIState.RotatingPiece:
                moveTimer = 0f;
                rotateTimer += Time.deltaTime;
                if (rotateTimer >= ROTATE_DELAY)
                {
                    rotateTimer = 0f;
                    if (currentPiece.getRotation != targetRotation)
                    {
                        currentPiece.Rotate(true); // Assuming Rotate(bool clockwise) method exists
                    }
                    else
                    {
                        currentState = AIState.MovingPiece;
                    }
                }
                break;

            case AIState.MovingPiece:
                rotateTimer = 0f;
                moveTimer += Time.deltaTime;
                if (moveTimer >= MOVE_DELAY)
                {
                    moveTimer = 0f;
                    if (currentPiece.X < targetX)
                    {
                        currentPiece.Move(1, 0); // Move right
                    }
                    else if (currentPiece.X > targetX)
                    {
                        currentPiece.Move(-1, 0); // Move left
                    }
                    else
                    {
                        currentState = AIState.DroppingPiece;
                    }
                }
                break;

            case AIState.DroppingPiece:
                moveTimer = 0f;
                rotateTimer = 0f;
                dropTimer += Time.deltaTime;
                if (dropTimer >= DROP_DELAY)
                {
                    dropTimer = 0f;
                    if (_board.CanMove(currentPiece, 0, -1))
                    {
                        currentPiece.Move(0, -1); // Move down
                    }
                    else
                    {
                        board.PlacePiece(currentPiece);
                        currentState = AIState.AnalyzingBoard;
                    }
                }
                break;
        }*/
    }


    private void AnalyzeBoard()
    {

        int[,] gaps = FindGaps();
        int totalGaps = CountTotalGaps(gaps);
        float averageHeight = CalculateAverageHeight();


        Debug.Log($"Total gaps: {totalGaps}, Average height: {averageHeight}");

        // Use this information to make decisions in ChoosingPiecePlacement
        currentState = AIState.ChoosingPiecePlacement;

    }

    private Vector2Int AccessGaps()
    {

        // Iterate through all positions within the bounds
        for (int x = _board.GetRectInt().yMin; x < _board.GetRectInt().yMax; x++)
        {
            for (int y = _board.GetRectInt().xMin; y < _board.GetRectInt().xMax; y++)
            {
                Vector2Int position = new Vector2Int(x, y);

                // Check if the position is a gap (e.g., not occupied)
                if (!IsOccupied(position.x, position.y))
                {
                    return position; // Return the first gap found
                }
            }
        }

        // If no gaps were found, return a default position
        return new Vector2Int(0, 0);
    }

    public bool IsOccupied(int x, int y)
    {
        // Check if the coordinates are within the board boundaries
        if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight)
        {
            // Treat out-of-bounds as occupied
            return true;
        }

        // Return true if the cell value is not 0 (empty)
        if (AccessGaps().x != 0 || AccessGaps().y != 0)
        {
            return true;
        }

        return false;
    }

    private int[,] FindGaps()
    {
        int[,] gaps = new int[boardWidth, boardHeight];

        for (int x = 0; x < boardWidth; x++)
        {
            bool foundBlock = false;
            for (int y = boardHeight - 1; y >= 0; y--)
            {
                if (IsOccupied(x, y))
                {
                    foundBlock = true;
                }
                else if (foundBlock)
                {
                    gaps[x, y] = 1; // Mark as a gap
                }
            }
        }

        return gaps;
    }

    private int CountTotalGaps(int[,] gaps)
    {
        int total = 0;
        for (int x = 0; x < boardWidth; x++)
        {
            for (int y = 0; y < boardHeight; y++)
            {
                total += gaps[x, y];
            }
        }
        return total;
    }

    private float CalculateAverageHeight()
    {
        int totalHeight = 0;
        for (int x = 0; x < boardWidth; x++)
        {
            totalHeight += GetColumnHeight(x);
        }
        return (float)totalHeight / boardWidth;
    }

    private int GetColumnHeight(int x)
    {
        for (int y = boardHeight - 1; y >= 0; y--)
        {
            if (IsOccupied(x, y))
            {
                return y + 1;
            }
        }
        return 0;
    }
    private void ChoosePiecePlacement()
    {
        // Find the leftmost and rightmost valid positions for the piece
        int leftmost = 0;
        int rightmost = boardWidth - 1;
        Vector3 piecePosition = currentPiece.getPosition();
        int posY = (int)piecePosition.y;
        for (int x = 0; x < boardWidth; x++)
        {

            if (IsOccupied(x, posY))
            {
                leftmost = x;
                break;
            }
        }

        for (int x = boardWidth - 1; x >= 0; x--)
        {
            if (IsOccupied(x, posY))
            {
                rightmost = x;
                break;
            }
        }

        // Choose a target X position that is centered between the valid positions
        targetX = (leftmost + rightmost) / 2;

        // Adjust targetX if necessary to avoid collisions
        while (IsOccupied(targetX, posY))
        {
            targetX++;
        }
    }

    private int CalculateBestRotation()
    {
        // Implement logic to determine the best rotation
        // For now, we'll just return a random rotation
        return Random.Range(0, 4);
    }

    private int CalculateBestXPosition()
    {
        // Implement logic to determine the best X position
        // For now, we'll just return a random position
        return Random.Range(0, boardWidth);
    }

}
Board.cs
using UnityEngine;
using UnityEngine.Tilemaps;
using System.Collections.Generic;

[DefaultExecutionOrder(-1)]
public class Board : MonoBehaviour
{
    public Tilemap tilemap { get; private set; }
    public Piece activePiece { get; private set; }

    public TetrominoData[] tetrominoes;
    public Vector2Int boardSize = new Vector2Int(16, 20);
    public Vector3Int spawnPosition = new Vector3Int(-1, 8, 0);
    public bool isPlayerCom = false;

    //
    private List<Vector3Int> topmostPieces;
    private Color[] originalColors;
    private int currentIndex = -1;
    private float timer = 0f;
    private bool isChangingColors = false;
    private bool isAlive = true;
    private int totalLinesCleared = 0;
    private int stackHeight = 0;


    //GUI//
    private int fontSize = 24;
    public Font font;
    public Color textColor = Color.white;
    private GUIStyle style;


    public void StartSequentialColorChange()
    {
        if (!isChangingColors)
        {
            topmostPieces = GetTopmostPieces();
            originalColors = new Color[topmostPieces.Count];
            for (int i = 0; i < topmostPieces.Count; i++)
            {
                originalColors[i] = tilemap.GetColor(topmostPieces[i]);
            }
            currentIndex = -1;
            timer = 0f;
            isChangingColors = true;
        }
    }

    private void Update()
    {
        if (isChangingColors)
        {
            timer += Time.deltaTime;

            if (timer >= 1f)
            {
                timer = 0f;

                // Revert the color of the previous piece
                if (currentIndex >= 0)
                {
                    Vector3Int prevPosition = topmostPieces[currentIndex];
                    tilemap.SetColor(prevPosition, originalColors[currentIndex]);
                }

                currentIndex++;

                // If we've gone through all pieces, stop the color change
                if (currentIndex >= topmostPieces.Count)
                {
                    isChangingColors = false;
                    return;
                }

                // Change the color of the current piece
                Vector3Int currentPosition = topmostPieces[currentIndex];
                tilemap.SetTileFlags(currentPosition, TileFlags.None);
                tilemap.SetColor(currentPosition, Color.red);
            }
        }
    }
    /*
        private List<Vector3Int> GetTopmostPieces()
        {
            List<Vector3Int> pieces = new List<Vector3Int>();
            RectInt bounds = Bounds;

            for (int col = bounds.xMin; col < bounds.xMax; col++)
            {
                for (int row = bounds.yMax - 1; row >= bounds.yMin; row--)
                {
                    Vector3Int position = new Vector3Int(col, row, 0);
                    if (tilemap.HasTile(position))
                    {
                        pieces.Add(position);
                        break; // Move to the next column after finding the topmost piece
                    }
                }
            }

            return pieces;
        }
    */
    public RectInt GetRectInt(){
        RectInt bounds = Bounds;
        return bounds;
    }

    private List<Vector3Int> GetTopmostPieces()
    {
        List<Vector3Int> pieces = new List<Vector3Int>();
        RectInt bounds = Bounds;
        int highestRow = bounds.yMin - 1; // Initialize to below the bottom of the board

        for (int col = bounds.xMin; col < bounds.xMax; col++)
        {
            for (int row = bounds.yMax - 1; row >= bounds.yMin; row--)
            {
                Vector3Int position = new Vector3Int(col, row, 0);
                if (tilemap.HasTile(position) && !IsActivePiecePosition(position))
                {
                    pieces.Add(position);
                    highestRow = Mathf.Max(highestRow, row); // Update the highest row
                    break; // Move to the next column after finding the topmost piece
                }
            }
        }

        // Calculate the stack height
        stackHeight = highestRow >= bounds.yMin ? highestRow - bounds.yMin + 1 : 0;

        return pieces;
    }
    int GetStackHeight()
    {
        GetTopmostPieces(); // This will update the stackHeight
        return stackHeight;
    }
    private float CalculateStackHeightPercentage()
    {
        int currentHeight = GetStackHeight();
        float totalHeight = boardSize.y; // This is the total height of the board
        float percentage = (currentHeight / totalHeight) * 100f;
        return Mathf.Clamp(percentage, 0f, 100f); // Ensure the percentage is between 0 and 100
    }
    private bool IsActivePiecePosition(Vector3Int position)
    {
        if (activePiece == null)
            return false;

        for (int i = 0; i < activePiece.cells.Length; i++)
        {
            if (activePiece.cells[i] + activePiece.position == position)
                return true;
        }
        return false;
    }
    public int GetTotalPiecesOnBoard()
    {
        int totalPieces = 0;
        RectInt bounds = Bounds;

        for (int row = bounds.yMin; row < bounds.yMax; row++)
        {
            for (int col = bounds.xMin; col < bounds.xMax; col++)
            {
                Vector3Int position = new Vector3Int(col, row, 0);
                if (tilemap.HasTile(position) && !IsActivePiecePosition(position))
                {
                    totalPieces++;
                }
            }
        }

        return totalPieces / 4;
    }

    public RectInt Bounds
    {
        get
        {
            Vector2Int position = new Vector2Int(-boardSize.x / 2, -boardSize.y / 2);
            return new RectInt(position, boardSize);
        }
    }

    private void Awake()
    {
        tilemap = GetComponentInChildren<Tilemap>();
        activePiece = GetComponentInChildren<Piece>();

        for (int i = 0; i < tetrominoes.Length; i++)
        {
            tetrominoes[i].Initialize();
        }
    }

    private void Start()
    {
        //this.GetComponent<Com>().enabled = isPlayerCom;

        SpawnPiece();

        // Initialize the GUIStyle
        style = new GUIStyle();
        style.fontSize = fontSize;
        style.normal.textColor = textColor;

        if (font != null)
        {
            style.font = font;
        }
    }
    void OnGUI()
    {
        //Player State//
        string playerState = isAlive ? "Alive" : "Dead";
        GUI.Label(new Rect(10, 10, Screen.width, Screen.height), "Player: " + playerState, style);

        // Display stack height as a percentage
        float stackPercentage = CalculateStackHeightPercentage();
        GUI.Label(new Rect(10, 30, Screen.width, Screen.height), $"Stack Height: {stackPercentage:F1}%", style);

        // Display total lines cleared
        GUI.Label(new Rect(10, 50, Screen.width, Screen.height), $"Total Lines Cleared: {GetTotalLinesCleared()}", style);

        // Display total pieces on board
        GUI.Label(new Rect(10, 70, Screen.width, Screen.height), $"Total Pieces on Board: {GetTotalPiecesOnBoard()}", style);

    }

    public void SpawnPiece()
    {
        int random = Random.Range(0, tetrominoes.Length);
        TetrominoData data = tetrominoes[random];

        activePiece.Initialize(this, spawnPosition, data);

        if (IsValidPosition(activePiece, spawnPosition))
        {
            Set(activePiece);
        }
        else
        {
            GameOver();
        }
    }

    public void GameOver()
    {
        tilemap.ClearAllTiles();
        isAlive = false;

        if (isAlive == false)
        {
            print("Player Dead");
        }
    }

    public void Set(Piece piece)
    {
        for (int i = 0; i < piece.cells.Length; i++)
        {
            Vector3Int tilePosition = piece.cells[i] + piece.position;
            tilemap.SetTile(tilePosition, piece.data.tile);
        }
    }

    public void Clear(Piece piece)
    {
        for (int i = 0; i < piece.cells.Length; i++)
        {
            Vector3Int tilePosition = piece.cells[i] + piece.position;
            tilemap.SetTile(tilePosition, null);
        }

    }

    public bool IsValidPosition(Piece piece, Vector3Int position)
    {
        RectInt bounds = Bounds;

        // The position is only valid if every cell is valid
        for (int i = 0; i < piece.cells.Length; i++)
        {
            Vector3Int tilePosition = piece.cells[i] + position;

            // An out of bounds tile is invalid
            if (!bounds.Contains((Vector2Int)tilePosition))
            {
                return false;
            }

            // A tile already occupies the position, thus invalid
            if (tilemap.HasTile(tilePosition))
            {
                return false;
            }
        }

        return true;
    }

    public void ClearLines()
    {
        RectInt bounds = Bounds;
        int row = bounds.yMin;

        // Clear from bottom to top
        while (row < bounds.yMax)
        {
            // Only advance to the next row if the current is not cleared
            // because the tiles above will fall down when a row is cleared
            if (IsLineFull(row))
            {
                LineClear(row);
            }
            else
            {
                row++;
            }
        }
    }

    public bool IsLineFull(int row)
    {
        RectInt bounds = Bounds;
        DetectTopBlocks();
        for (int col = bounds.xMin; col < bounds.xMax; col++)
        {
            Vector3Int position = new Vector3Int(col, row, 0);
            Debug.Log("pos: " + position);
            // The line is not full if a tile is missing
            if (!tilemap.HasTile(position))
            {
                return false;
            }
        }

        return true;
    }

    //
    public void DetectTopBlocks()
    {
        int i = 0;
        this.StartSequentialColorChange();


        foreach (Vector3Int position in this.GetTopmostPieces())
        {
            // Do something with the topmost piece position
            // Debug.Log($"Topmost piece at: {position}");
            i++;
            // print("TOTAL:" + i);
            // print($"Topmost piece at: {position}");
            // this.ChangeColorOfTopmostPieces(Color.red);

        }
    }
    //

    public void LineClear(int row)
    {
        RectInt bounds = Bounds;

        // Clear all tiles in the row
        for (int col = bounds.xMin; col < bounds.xMax; col++)
        {
            Vector3Int position = new Vector3Int(col, row, 0);
            tilemap.SetTile(position, null);
        }

        // Shift every row above down one
        while (row < bounds.yMax)
        {
            for (int col = bounds.xMin; col < bounds.xMax; col++)
            {
                Vector3Int position = new Vector3Int(col, row + 1, 0);
                TileBase above = tilemap.GetTile(position);

                position = new Vector3Int(col, row, 0);
                tilemap.SetTile(position, above);
            }

            row++;
        }

        // Increment the total lines cleared
        totalLinesCleared++;
    }

    public int GetTotalLinesCleared()
    {
        return totalLinesCleared;
    }

}

Glancing over the code above, line 274 in Com.cs is almost certainly your lockup. It looks like a refinement step so just comment it out and see, or else make it give up after 100 iterations or so because by then it would clearly be far off the board.

Here’s why:

Unity will lock up 100% of the time EVERY millisecond your scripting code is running.

Nothing will render, no input will be processed, no Debug.Log() will come out, no GameObjects or transforms will appear to update.

Absolutely NOTHING will happen… until your code either:

  • returns from whatever function it is running

  • yields from whatever coroutine it is running

As long as your code is looping, Unity isn’t going to do even a single frame of change. Nothing.

No exceptions.

“Yield early, yield often, yield like your game depends on it… it does!” - Kurt Dekker