using System; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using UnityEngine; | |
using Debug = UnityEngine.Debug; | |
namespace Unity.MLAgents.Integrations.Match3 | |
{ | |
/// <summary> | |
/// Representation of the AbstractBoard dimensions, and number of cell and special types. | |
/// </summary> | |
public struct BoardSize | |
{ | |
/// <summary> | |
/// Number of rows on the board | |
/// </summary> | |
public int Rows; | |
/// <summary> | |
/// Number of columns on the board | |
/// </summary> | |
public int Columns; | |
/// <summary> | |
/// Maximum number of different types of cells (colors, pieces, etc). | |
/// </summary> | |
public int NumCellTypes; | |
/// <summary> | |
/// Maximum number of special types. This can be zero, in which case | |
/// all cells of the same type are assumed to be equivalent. | |
/// </summary> | |
public int NumSpecialTypes; | |
/// <summary> | |
/// Check that all fields of the left-hand BoardSize are less than or equal to the field of the right-hand BoardSize | |
/// </summary> | |
/// <param name="lhs"></param> | |
/// <param name="rhs"></param> | |
/// <returns>True if all fields are less than or equal.</returns> | |
public static bool operator <=(BoardSize lhs, BoardSize rhs) | |
{ | |
return lhs.Rows <= rhs.Rows && lhs.Columns <= rhs.Columns && lhs.NumCellTypes <= rhs.NumCellTypes && | |
lhs.NumSpecialTypes <= rhs.NumSpecialTypes; | |
} | |
/// <summary> | |
/// Check that all fields of the left-hand BoardSize are greater than or equal to the field of the right-hand BoardSize | |
/// </summary> | |
/// <param name="lhs"></param> | |
/// <param name="rhs"></param> | |
/// <returns>True if all fields are greater than or equal.</returns> | |
public static bool operator >=(BoardSize lhs, BoardSize rhs) | |
{ | |
return lhs.Rows >= rhs.Rows && lhs.Columns >= rhs.Columns && lhs.NumCellTypes >= rhs.NumCellTypes && | |
lhs.NumSpecialTypes >= rhs.NumSpecialTypes; | |
} | |
/// <summary> | |
/// Return a string representation of the BoardSize. | |
/// </summary> | |
/// <returns></returns> | |
public override string ToString() | |
{ | |
return | |
$"Rows: {Rows}, Columns: {Columns}, NumCellTypes: {NumCellTypes}, NumSpecialTypes: {NumSpecialTypes}"; | |
} | |
} | |
/// <summary> | |
/// An adapter between ML Agents and a Match-3 game. | |
/// </summary> | |
public abstract class AbstractBoard : MonoBehaviour | |
{ | |
/// <summary> | |
/// Return the maximum size of the board. This is used to determine the size of observations and actions, | |
/// so the returned values must not change. | |
/// </summary> | |
/// <returns></returns> | |
public abstract BoardSize GetMaxBoardSize(); | |
/// <summary> | |
/// Return the current size of the board. The values must less than or equal to the values returned from | |
/// <see cref="GetMaxBoardSize"/>. | |
/// By default, this will return <see cref="GetMaxBoardSize"/>; if your board doesn't change size, you don't need to | |
/// override it. | |
/// </summary> | |
/// <returns></returns> | |
public virtual BoardSize GetCurrentBoardSize() | |
{ | |
return GetMaxBoardSize(); | |
} | |
/// <summary> | |
/// Returns the "color" of the piece at the given row and column. | |
/// This should be between 0 and BoardSize.NumCellTypes-1 (inclusive). | |
/// The actual order of the values doesn't matter. | |
/// </summary> | |
/// <param name="row"></param> | |
/// <param name="col"></param> | |
/// <returns></returns> | |
public abstract int GetCellType(int row, int col); | |
/// <summary> | |
/// Returns the special type of the piece at the given row and column. | |
/// This should be between 0 and BoardSize.NumSpecialTypes (inclusive). | |
/// The actual order of the values doesn't matter. | |
/// </summary> | |
/// <param name="row"></param> | |
/// <param name="col"></param> | |
/// <returns></returns> | |
public abstract int GetSpecialType(int row, int col); | |
/// <summary> | |
/// Check whether the particular Move is valid for the game. | |
/// The actual results will depend on the rules of the game, but we provide <see cref="SimpleIsMoveValid(Move)"/> | |
/// that handles basic match3 rules with no special or immovable pieces. | |
/// </summary> | |
/// <remarks> | |
/// Moves that would go outside of <see cref="GetCurrentBoardSize"/> are filtered out before they are | |
/// passed to IsMoveValid(). | |
/// </remarks> | |
/// <param name="m">The move to check.</param> | |
/// <returns></returns> | |
public abstract bool IsMoveValid(Move m); | |
/// <summary> | |
/// Instruct the game to make the given <see cref="Move"/>. Returns true if the move was made. | |
/// Note that during training, a move that was marked as invalid may occasionally still be | |
/// requested. If this happens, it is safe to do nothing and request another move. | |
/// </summary> | |
/// <param name="m">The move to carry out.</param> | |
/// <returns></returns> | |
public abstract bool MakeMove(Move m); | |
/// <summary> | |
/// Return the total number of moves possible for the board. | |
/// </summary> | |
/// <returns></returns> | |
public int NumMoves() | |
{ | |
return Move.NumPotentialMoves(GetMaxBoardSize()); | |
} | |
/// <summary> | |
/// An optional callback for when the all moves are invalid. Ideally, the game state should | |
/// be changed before this happens, but this is a way to get notified if not. | |
/// </summary> | |
public Action OnNoValidMovesAction; | |
/// <summary> | |
/// Iterate through all moves on the board. | |
/// </summary> | |
/// <returns></returns> | |
public IEnumerable<Move> AllMoves() | |
{ | |
var maxBoardSize = GetMaxBoardSize(); | |
var currentBoardSize = GetCurrentBoardSize(); | |
var currentMove = Move.FromMoveIndex(0, maxBoardSize); | |
for (var i = 0; i < NumMoves(); i++) | |
{ | |
if (currentMove.InRangeForBoard(currentBoardSize)) | |
{ | |
yield return currentMove; | |
} | |
currentMove.Next(maxBoardSize); | |
} | |
} | |
/// <summary> | |
/// Iterate through all valid moves on the board. | |
/// </summary> | |
/// <returns></returns> | |
public IEnumerable<Move> ValidMoves() | |
{ | |
var maxBoardSize = GetMaxBoardSize(); | |
var currentBoardSize = GetCurrentBoardSize(); | |
var currentMove = Move.FromMoveIndex(0, maxBoardSize); | |
for (var i = 0; i < NumMoves(); i++) | |
{ | |
if (currentMove.InRangeForBoard(currentBoardSize) && IsMoveValid(currentMove)) | |
{ | |
yield return currentMove; | |
} | |
currentMove.Next(maxBoardSize); | |
} | |
} | |
/// <summary> | |
/// Returns true if swapping the cells specified by the move would result in | |
/// 3 or more cells of the same type in a row. This assumes that all pieces are allowed | |
/// to be moved; to add extra logic, incorporate it into your <see cref="IsMoveValid"/> method. | |
/// </summary> | |
/// <param name="move"></param> | |
/// <returns></returns> | |
public bool SimpleIsMoveValid(Move move) | |
{ | |
using (TimerStack.Instance.Scoped("SimpleIsMoveValid")) | |
{ | |
var moveVal = GetCellType(move.Row, move.Column); | |
var (otherRow, otherCol) = move.OtherCell(); | |
var oppositeVal = GetCellType(otherRow, otherCol); | |
// Simple check - if the values are the same, don't match | |
// This might not be valid for all games | |
{ | |
if (moveVal == oppositeVal) | |
{ | |
return false; | |
} | |
} | |
bool moveMatches = CheckHalfMove(otherRow, otherCol, moveVal, move.Direction); | |
if (moveMatches) | |
{ | |
// early out | |
return true; | |
} | |
bool otherMatches = CheckHalfMove(move.Row, move.Column, oppositeVal, move.OtherDirection()); | |
return otherMatches; | |
} | |
} | |
/// <summary> | |
/// Check if one of the cells that is swapped during a move matches 3 or more. | |
/// Since these checks are similar for each cell, we consider the move as two "half moves". | |
/// </summary> | |
/// <param name="newRow"></param> | |
/// <param name="newCol"></param> | |
/// <param name="newValue"></param> | |
/// <param name="incomingDirection"></param> | |
/// <returns></returns> | |
bool CheckHalfMove(int newRow, int newCol, int newValue, Direction incomingDirection) | |
{ | |
var currentBoardSize = GetCurrentBoardSize(); | |
int matchedLeft = 0, matchedRight = 0, matchedUp = 0, matchedDown = 0; | |
if (incomingDirection != Direction.Right) | |
{ | |
for (var c = newCol - 1; c >= 0; c--) | |
{ | |
if (GetCellType(newRow, c) == newValue) | |
matchedLeft++; | |
else | |
break; | |
} | |
} | |
if (incomingDirection != Direction.Left) | |
{ | |
for (var c = newCol + 1; c < currentBoardSize.Columns; c++) | |
{ | |
if (GetCellType(newRow, c) == newValue) | |
matchedRight++; | |
else | |
break; | |
} | |
} | |
if (incomingDirection != Direction.Down) | |
{ | |
for (var r = newRow + 1; r < currentBoardSize.Rows; r++) | |
{ | |
if (GetCellType(r, newCol) == newValue) | |
matchedUp++; | |
else | |
break; | |
} | |
} | |
if (incomingDirection != Direction.Up) | |
{ | |
for (var r = newRow - 1; r >= 0; r--) | |
{ | |
if (GetCellType(r, newCol) == newValue) | |
matchedDown++; | |
else | |
break; | |
} | |
} | |
if ((matchedUp + matchedDown >= 2) || (matchedLeft + matchedRight >= 2)) | |
{ | |
return true; | |
} | |
return false; | |
} | |
/// <summary> | |
/// Make sure that the current BoardSize isn't larger than the original value of <see cref="GetMaxBoardSize"/>. | |
/// If it is, log a warning. | |
/// </summary> | |
/// <param name="originalMaxBoardSize"></param> | |
[ | ]|
internal void CheckBoardSizes(BoardSize originalMaxBoardSize) | |
{ | |
var currentBoardSize = GetCurrentBoardSize(); | |
if (!(currentBoardSize <= originalMaxBoardSize)) | |
{ | |
Debug.LogWarning( | |
"Current BoardSize is larger than maximum board size was on initialization. This may cause unexpected results.\n" + | |
$"Original GetMaxBoardSize() result: {originalMaxBoardSize}\n" + | |
$"GetCurrentBoardSize() result: {currentBoardSize}" | |
); | |
} | |
} | |
} | |
} | |