File size: 11,806 Bytes
05c9ac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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>
        [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}"
                );
            }
        }
    }
}