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