using System; using System.Collections.Generic; using System.Diagnostics; using UnityEngine; using Debug = UnityEngine.Debug; namespace Unity.MLAgents.Integrations.Match3 { /// /// Representation of the AbstractBoard dimensions, and number of cell and special types. /// public struct BoardSize { /// /// Number of rows on the board /// public int Rows; /// /// Number of columns on the board /// public int Columns; /// /// Maximum number of different types of cells (colors, pieces, etc). /// public int NumCellTypes; /// /// Maximum number of special types. This can be zero, in which case /// all cells of the same type are assumed to be equivalent. /// public int NumSpecialTypes; /// /// Check that all fields of the left-hand BoardSize are less than or equal to the field of the right-hand BoardSize /// /// /// /// True if all fields are less than or equal. 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; } /// /// Check that all fields of the left-hand BoardSize are greater than or equal to the field of the right-hand BoardSize /// /// /// /// True if all fields are greater than or equal. 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; } /// /// Return a string representation of the BoardSize. /// /// public override string ToString() { return $"Rows: {Rows}, Columns: {Columns}, NumCellTypes: {NumCellTypes}, NumSpecialTypes: {NumSpecialTypes}"; } } /// /// An adapter between ML Agents and a Match-3 game. /// public abstract class AbstractBoard : MonoBehaviour { /// /// 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. /// /// public abstract BoardSize GetMaxBoardSize(); /// /// Return the current size of the board. The values must less than or equal to the values returned from /// . /// By default, this will return ; if your board doesn't change size, you don't need to /// override it. /// /// public virtual BoardSize GetCurrentBoardSize() { return GetMaxBoardSize(); } /// /// 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. /// /// /// /// public abstract int GetCellType(int row, int col); /// /// 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. /// /// /// /// public abstract int GetSpecialType(int row, int col); /// /// Check whether the particular Move is valid for the game. /// The actual results will depend on the rules of the game, but we provide /// that handles basic match3 rules with no special or immovable pieces. /// /// /// Moves that would go outside of are filtered out before they are /// passed to IsMoveValid(). /// /// The move to check. /// public abstract bool IsMoveValid(Move m); /// /// Instruct the game to make the given . 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. /// /// The move to carry out. /// public abstract bool MakeMove(Move m); /// /// Return the total number of moves possible for the board. /// /// public int NumMoves() { return Move.NumPotentialMoves(GetMaxBoardSize()); } /// /// 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. /// public Action OnNoValidMovesAction; /// /// Iterate through all moves on the board. /// /// public IEnumerable 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); } } /// /// Iterate through all valid moves on the board. /// /// public IEnumerable 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); } } /// /// 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 method. /// /// /// 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; } } /// /// 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". /// /// /// /// /// /// 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; } /// /// Make sure that the current BoardSize isn't larger than the original value of . /// If it is, log a warning. /// /// [Conditional("DEBUG")] 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}" ); } } } }