using System;
using UnityEngine;
namespace Unity.MLAgents.Extensions.Match3
{
///
/// Directions for a Move.
///
public enum Direction
{
///
/// Move up (increasing row direction).
///
Up,
///
/// Move down (decreasing row direction).
///
Down, // -row direction
///
/// Move left (decreasing column direction).
///
Left, // -column direction
///
/// Move right (increasing column direction).
///
Right, // +column direction
}
///
/// Struct that encapsulates a swap of adjacent cells.
/// A Move can be constructed from either a starting row, column, and direction,
/// or from a "move index" between 0 and NumPotentialMoves()-1.
/// Moves are enumerated as the internal edges of the game grid.
/// Left/right moves come first. There are (maxCols - 1) * maxRows of these.
/// Up/down moves are next. There are (maxRows - 1) * maxCols of these.
///
public struct Move
{
///
/// Index of the move, from 0 to NumPotentialMoves-1.
///
public int MoveIndex;
///
/// Row of the cell that will be moved.
///
public int Row;
///
/// Column of the cell that will be moved.
///
public int Column;
///
/// Direction that the cell will be moved.
///
public Direction Direction;
///
/// Construct a Move from its move index and the board size.
/// This is useful for iterating through all the Moves on a board, or constructing
/// the Move corresponding to an Agent decision.
///
/// Must be between 0 and NumPotentialMoves(maxRows, maxCols).
///
///
///
public static Move FromMoveIndex(int moveIndex, BoardSize maxBoardSize)
{
var maxRows = maxBoardSize.Rows;
var maxCols = maxBoardSize.Columns;
if (moveIndex < 0 || moveIndex >= NumPotentialMoves(maxBoardSize))
{
throw new ArgumentOutOfRangeException("Invalid move index.");
}
Direction dir;
int row, col;
if (moveIndex < (maxCols - 1) * maxRows)
{
dir = Direction.Right;
col = moveIndex % (maxCols - 1);
row = moveIndex / (maxCols - 1);
}
else
{
dir = Direction.Up;
var offset = moveIndex - (maxCols - 1) * maxRows;
col = offset % maxCols;
row = offset / maxCols;
}
return new Move
{
MoveIndex = moveIndex,
Direction = dir,
Row = row,
Column = col
};
}
///
/// Increment the Move to the next MoveIndex, and update the Row, Column, and Direction accordingly.
///
///
public void Next(BoardSize maxBoardSize)
{
var maxRows = maxBoardSize.Rows;
var maxCols = maxBoardSize.Columns;
var switchoverIndex = (maxCols - 1) * maxRows;
MoveIndex++;
if (MoveIndex < switchoverIndex)
{
Column++;
if (Column == maxCols - 1)
{
Row++;
Column = 0;
}
}
else if (MoveIndex == switchoverIndex)
{
// switch from moving right to moving up
Row = 0;
Column = 0;
Direction = Direction.Up;
}
else
{
Column++;
if (Column == maxCols)
{
Row++;
Column = 0;
}
}
}
///
/// Construct a Move from the row, column, and direction.
///
///
///
///
///
///
public static Move FromPositionAndDirection(int row, int col, Direction dir, BoardSize maxBoardSize)
{
// Check for out-of-bounds
if (row < 0 || row >= maxBoardSize.Rows)
{
throw new IndexOutOfRangeException($"row was {row}, but must be between 0 and {maxBoardSize.Rows - 1}.");
}
if (col < 0 || col >= maxBoardSize.Columns)
{
throw new IndexOutOfRangeException($"col was {col}, but must be between 0 and {maxBoardSize.Columns - 1}.");
}
// Check moves that would go out of bounds e.g. col == 0 and dir == Left
if (
row == 0 && dir == Direction.Down ||
row == maxBoardSize.Rows - 1 && dir == Direction.Up ||
col == 0 && dir == Direction.Left ||
col == maxBoardSize.Columns - 1 && dir == Direction.Right
)
{
throw new IndexOutOfRangeException($"Cannot move cell at row={row} col={col} in Direction={dir}");
}
// Normalize - only consider Right and Up
if (dir == Direction.Left)
{
dir = Direction.Right;
col = col - 1;
}
else if (dir == Direction.Down)
{
dir = Direction.Up;
row = row - 1;
}
int moveIndex;
if (dir == Direction.Right)
{
moveIndex = col + row * (maxBoardSize.Columns - 1);
}
else
{
var offset = (maxBoardSize.Columns - 1) * maxBoardSize.Rows;
moveIndex = offset + col + row * maxBoardSize.Columns;
}
return new Move
{
Row = row,
Column = col,
Direction = dir,
MoveIndex = moveIndex,
};
}
///
/// Check if the move is valid for the given board size.
/// This will be passed the return value from AbstractBoard.GetCurrentBoardSize().
///
///
///
public bool InRangeForBoard(BoardSize boardSize)
{
var (otherRow, otherCol) = OtherCell();
// Get the maximum row and column this move would affect.
var maxMoveRow = Mathf.Max(Row, otherRow);
var maxMoveCol = Mathf.Max(Column, otherCol);
return maxMoveRow < boardSize.Rows && maxMoveCol < boardSize.Columns;
}
///
/// Get the other row and column that correspond to this move.
///
///
///
public (int Row, int Column) OtherCell()
{
switch (Direction)
{
case Direction.Up:
return (Row + 1, Column);
case Direction.Down:
return (Row - 1, Column);
case Direction.Left:
return (Row, Column - 1);
case Direction.Right:
return (Row, Column + 1);
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// Get the opposite direction of this move.
///
///
///
public Direction OtherDirection()
{
switch (Direction)
{
case Direction.Up:
return Direction.Down;
case Direction.Down:
return Direction.Up;
case Direction.Left:
return Direction.Right;
case Direction.Right:
return Direction.Left;
default:
throw new ArgumentOutOfRangeException();
}
}
///
/// Return the number of potential moves for a board of the given size.
/// This is equivalent to the number of internal edges in the board.
///
///
///
public static int NumPotentialMoves(BoardSize maxBoardSize)
{
return maxBoardSize.Rows * (maxBoardSize.Columns - 1) + (maxBoardSize.Rows - 1) * (maxBoardSize.Columns);
}
}
}