using System;
using Unity.MLAgents.Integrations.Match3;
using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.MLAgentsExamples
{
public class Match3Board : AbstractBoard
{
public int MinRows;
[FormerlySerializedAs("Rows")]
public int MaxRows;
public int MinColumns;
[FormerlySerializedAs("Columns")]
public int MaxColumns;
public int NumCellTypes;
public int NumSpecialTypes;
public const int k_EmptyCell = -1;
[Tooltip("Points earned for clearing a basic cell (cube)")]
public int BasicCellPoints = 1;
[Tooltip("Points earned for clearing a special cell (sphere)")]
public int SpecialCell1Points = 2;
[Tooltip("Points earned for clearing an extra special cell (plus)")]
public int SpecialCell2Points = 3;
///
/// Seed to initialize the object.
///
public int RandomSeed;
(int CellType, int SpecialType)[,] m_Cells;
bool[,] m_Matched;
private BoardSize m_CurrentBoardSize;
System.Random m_Random;
void Awake()
{
m_Cells = new (int, int)[MaxColumns, MaxRows];
m_Matched = new bool[MaxColumns, MaxRows];
// Start using the max rows and columns, but we'll update the current size at the start of each episode.
m_CurrentBoardSize = new BoardSize
{
Rows = MaxRows,
Columns = MaxColumns,
NumCellTypes = NumCellTypes,
NumSpecialTypes = NumSpecialTypes
};
}
void Start()
{
m_Random = new System.Random(RandomSeed == -1 ? gameObject.GetInstanceID() : RandomSeed);
InitRandom();
}
public override BoardSize GetMaxBoardSize()
{
return new BoardSize
{
Rows = MaxRows,
Columns = MaxColumns,
NumCellTypes = NumCellTypes,
NumSpecialTypes = NumSpecialTypes
};
}
public override BoardSize GetCurrentBoardSize()
{
return m_CurrentBoardSize;
}
///
/// Change the board size to a random size between the min and max rows and columns. This is
/// cached so that the size is consistent until it is updated.
/// This is just for an example; you can change your board size however you want.
///
public void UpdateCurrentBoardSize()
{
var newRows = m_Random.Next(MinRows, MaxRows + 1);
var newCols = m_Random.Next(MinColumns, MaxColumns + 1);
m_CurrentBoardSize.Rows = newRows;
m_CurrentBoardSize.Columns = newCols;
}
public override bool MakeMove(Move move)
{
if (!IsMoveValid(move))
{
return false;
}
var originalValue = m_Cells[move.Column, move.Row];
var (otherRow, otherCol) = move.OtherCell();
var destinationValue = m_Cells[otherCol, otherRow];
m_Cells[move.Column, move.Row] = destinationValue;
m_Cells[otherCol, otherRow] = originalValue;
return true;
}
public override int GetCellType(int row, int col)
{
if (row >= m_CurrentBoardSize.Rows || col >= m_CurrentBoardSize.Columns)
{
throw new IndexOutOfRangeException();
}
return m_Cells[col, row].CellType;
}
public override int GetSpecialType(int row, int col)
{
if (row >= m_CurrentBoardSize.Rows || col >= m_CurrentBoardSize.Columns)
{
throw new IndexOutOfRangeException();
}
return m_Cells[col, row].SpecialType;
}
public override bool IsMoveValid(Move m)
{
if (m_Cells == null)
{
return false;
}
return SimpleIsMoveValid(m);
}
public bool MarkMatchedCells(int[,] cells = null)
{
ClearMarked();
bool madeMatch = false;
for (var i = 0; i < m_CurrentBoardSize.Rows; i++)
{
for (var j = 0; j < m_CurrentBoardSize.Columns; j++)
{
// Check vertically
var matchedRows = 0;
for (var iOffset = i; iOffset < m_CurrentBoardSize.Rows; iOffset++)
{
if (m_Cells[j, i].CellType != m_Cells[j, iOffset].CellType)
{
break;
}
matchedRows++;
}
if (matchedRows >= 3)
{
madeMatch = true;
for (var k = 0; k < matchedRows; k++)
{
m_Matched[j, i + k] = true;
}
}
// Check vertically
var matchedCols = 0;
for (var jOffset = j; jOffset < m_CurrentBoardSize.Columns; jOffset++)
{
if (m_Cells[j, i].CellType != m_Cells[jOffset, i].CellType)
{
break;
}
matchedCols++;
}
if (matchedCols >= 3)
{
madeMatch = true;
for (var k = 0; k < matchedCols; k++)
{
m_Matched[j + k, i] = true;
}
}
}
}
return madeMatch;
}
///
/// Sets cells that are matched to the empty cell, and returns the score earned.
///
///
public int ClearMatchedCells()
{
var pointsByType = new[] { BasicCellPoints, SpecialCell1Points, SpecialCell2Points };
int pointsEarned = 0;
for (var i = 0; i < m_CurrentBoardSize.Rows; i++)
{
for (var j = 0; j < m_CurrentBoardSize.Columns; j++)
{
if (m_Matched[j, i])
{
var speciaType = GetSpecialType(i, j);
pointsEarned += pointsByType[speciaType];
m_Cells[j, i] = (k_EmptyCell, 0);
}
}
}
ClearMarked(); // TODO clear here or at start of matching?
return pointsEarned;
}
public bool DropCells()
{
var madeChanges = false;
// Gravity is applied in the negative row direction
for (var j = 0; j < m_CurrentBoardSize.Columns; j++)
{
var writeIndex = 0;
for (var readIndex = 0; readIndex < m_CurrentBoardSize.Rows; readIndex++)
{
m_Cells[j, writeIndex] = m_Cells[j, readIndex];
if (m_Cells[j, readIndex].CellType != k_EmptyCell)
{
writeIndex++;
}
}
// Fill in empties at the end
for (; writeIndex < m_CurrentBoardSize.Rows; writeIndex++)
{
madeChanges = true;
m_Cells[j, writeIndex] = (k_EmptyCell, 0);
}
}
return madeChanges;
}
public bool FillFromAbove()
{
bool madeChanges = false;
for (var i = 0; i < m_CurrentBoardSize.Rows; i++)
{
for (var j = 0; j < m_CurrentBoardSize.Columns; j++)
{
if (m_Cells[j, i].CellType == k_EmptyCell)
{
madeChanges = true;
m_Cells[j, i] = (GetRandomCellType(), GetRandomSpecialType());
}
}
}
return madeChanges;
}
public (int, int)[,] Cells
{
get { return m_Cells; }
}
public bool[,] Matched
{
get { return m_Matched; }
}
// Initialize the board to random values.
public void InitRandom()
{
for (var i = 0; i < MaxRows; i++)
{
for (var j = 0; j < MaxColumns; j++)
{
m_Cells[j, i] = (GetRandomCellType(), GetRandomSpecialType());
}
}
}
public void InitSettled()
{
InitRandom();
while (true)
{
var anyMatched = MarkMatchedCells();
if (!anyMatched)
{
return;
}
ClearMatchedCells();
DropCells();
FillFromAbove();
}
}
void ClearMarked()
{
for (var i = 0; i < MaxRows; i++)
{
for (var j = 0; j < MaxColumns; j++)
{
m_Matched[j, i] = false;
}
}
}
int GetRandomCellType()
{
return m_Random.Next(0, NumCellTypes);
}
int GetRandomSpecialType()
{
// 1 in N chance to get a type-2 special
// 2 in N chance to get a type-1 special
// otherwise 0 (boring)
var N = 10;
var val = m_Random.Next(0, N);
if (val == 0)
{
return 2;
}
if (val <= 2)
{
return 1;
}
return 0;
}
}
}