您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
312 行
12 KiB
312 行
12 KiB
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 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 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>
|
|
[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}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|