From 184fd190ff7fdbc74c4174e3f5664f073c5edcc1 Mon Sep 17 00:00:00 2001 From: ninetailsrabbit Date: Sat, 15 Jun 2024 11:56:17 +0100 Subject: [PATCH] calculate correctly the pending fall moves using virtual board --- .../tests/src/components/BoardTests.cs | 93 ++++++++++++++++++- Match3Maker/Match3Maker.csproj | 2 +- Match3Maker/src/components/Board.cs | 77 +++++++++------ 3 files changed, 139 insertions(+), 33 deletions(-) diff --git a/Match3Maker.Tests/tests/src/components/BoardTests.cs b/Match3Maker.Tests/tests/src/components/BoardTests.cs index 2ada40d..8710f69 100644 --- a/Match3Maker.Tests/tests/src/components/BoardTests.cs +++ b/Match3Maker.Tests/tests/src/components/BoardTests.cs @@ -826,7 +826,6 @@ public void Should_Be_Able_Create_Sequences_Of_Cells_With_Selected_Type() { Piece prism = new(_pieceFactory.CreateNormalPiece("prism")); Piece special = new(_pieceFactory.CreateSpecialPiece("special")); - List pieces = [square, circle, triangle, prism, special]; var board = new Board(8, 7, 10); @@ -837,6 +836,13 @@ public void Should_Be_Able_Create_Sequences_Of_Cells_With_Selected_Type() { Assert.True(board.CreateSequenceOfCellsWithShape("circle").Cells.All(cell => cell.Piece.Type.Shape.Equals("circle"))); } + [Fact] + public void Should_Throw_Exception_When_Try_To_Move_Pieces_And_Fill_When_Grid_Cells_Are_Not_Prepared() { + var board = new Board(8, 7, 10); + + Assert.Throws(() => board.MovePiecesAndFillEmptyCells()); + + } [Fact] public void Should_Be_Able_To_Calculate_The_Pending_Fall_Moves_When_Sequence_Is_Consumed() { @@ -846,7 +852,6 @@ public void Should_Be_Able_To_Calculate_The_Pending_Fall_Moves_When_Sequence_Is_ Piece prism = new(_pieceFactory.CreateNormalPiece("prism")); Piece special = new(_pieceFactory.CreateSpecialPiece("special")); - List pieces = [square, circle, triangle, prism, special]; var board = new Board(8, 7, 10); @@ -860,6 +865,90 @@ public void Should_Be_Able_To_Calculate_The_Pending_Fall_Moves_When_Sequence_Is_ Assert.Equal(board.GridWidth, board.EmptyCells().Count); Assert.Equal(board.GridWidth, board.PendingFallMoves().Count); } + + [Fact] + public void Should_Create_A_Virtual_Board_Only_With_Fill_Updates_When_First_Row_Consumed() { + Piece square = new(_pieceFactory.CreateNormalPiece("square")); + Piece circle = new(_pieceFactory.CreateNormalPiece("circle")); + Piece triangle = new(_pieceFactory.CreateNormalPiece("triangle")); + Piece prism = new(_pieceFactory.CreateNormalPiece("prism")); + Piece special = new(_pieceFactory.CreateSpecialPiece("special")); + + List pieces = [square, circle, triangle, prism, special]; + + var board = new Board(8, 7, 10); + + board.AddAvailablePieces(pieces).PrepareGridCells().FillInitialBoard(true); + + board.CreateSequenceFromRow(0).Consume(); + + var virtualBoard = board.MovePiecesAndFillEmptyCells(); + + //No movements in the first row, only fills + Assert.True(virtualBoard.Updates.All(update => update.CurrentUpdateType.Equals(BoardCellUpdate.UPDATE_TYPE.FILL) && update.CellPieceFill.Cell.Row.Equals(0))); + Assert.Equal(board.GridWidth, virtualBoard.Updates.Count); + + // Virtual board has no empty cells after the fill but the original board should keep unaltered. + Assert.Empty(virtualBoard.EmptyCells()); + Assert.Equal(board.GridWidth, board.EmptyCells().Count); + } + + [Fact] + public void Should_Create_A_Virtual_Board_Only_With_Fill_Updates_On_Entire_Column_Consumed() { + Piece square = new(_pieceFactory.CreateNormalPiece("square")); + Piece circle = new(_pieceFactory.CreateNormalPiece("circle")); + Piece triangle = new(_pieceFactory.CreateNormalPiece("triangle")); + Piece prism = new(_pieceFactory.CreateNormalPiece("prism")); + Piece special = new(_pieceFactory.CreateSpecialPiece("special")); + + List pieces = [square, circle, triangle, prism, special]; + + var board = new Board(8, 7, 10); + + board.AddAvailablePieces(pieces).PrepareGridCells().FillInitialBoard(true); + + board.CreateSequenceFromColumn(2).Consume(); + + var virtualBoard = board.MovePiecesAndFillEmptyCells(); + + //No movements in the first row, only fills + Assert.True(virtualBoard.Updates.All(update => update.CurrentUpdateType.Equals(BoardCellUpdate.UPDATE_TYPE.FILL) && update.CellPieceFill.Cell.Column.Equals(2))); + Assert.Equal(board.GridHeight, virtualBoard.Updates.Count); + + // Virtual board has no empty cells after the fill but the original board should keep unaltered. + Assert.Empty(virtualBoard.EmptyCells()); + Assert.Equal(board.GridHeight, board.EmptyCells().Count); + } + + [Fact] + public void Should_Create_A_Virtual_Board_With_Movement_And_Fill() { + Piece square = new(_pieceFactory.CreateNormalPiece("square")); + Piece circle = new(_pieceFactory.CreateNormalPiece("circle")); + Piece triangle = new(_pieceFactory.CreateNormalPiece("triangle")); + Piece prism = new(_pieceFactory.CreateNormalPiece("prism")); + Piece special = new(_pieceFactory.CreateSpecialPiece("special")); + + List pieces = [square, circle, triangle, prism, special]; + + var board = new Board(8, 7, 10); + board.AddAvailablePieces(pieces).PrepareGridCells().FillInitialBoard(true); + + var sequence = new Sequence([board.Cell(1, 2), board.Cell(2, 2), board.Cell(3, 2)], Sequence.SHAPES.HORIZONTAL); + + //No updates in an initial board + Assert.Empty(board.MovePiecesAndFillEmptyCells().Updates); + + sequence.Consume(); + + Assert.Equal(sequence.Size(), board.EmptyCells().Count); + + var virtualBoard = board.MovePiecesAndFillEmptyCells(); + + // 6 Movements that represent moving down the row 0 and 1 after consuming the number 2. + Assert.Equal(sequence.Size() * 2, virtualBoard.MovementUpdates().Count); + Assert.Equal(sequence.Size(), virtualBoard.FillUpdates().Count); + Assert.Empty(virtualBoard.EmptyCells()); + } } } \ No newline at end of file diff --git a/Match3Maker/Match3Maker.csproj b/Match3Maker/Match3Maker.csproj index 707bfd6..16a54dc 100644 --- a/Match3Maker/Match3Maker.csproj +++ b/Match3Maker/Match3Maker.csproj @@ -14,7 +14,7 @@ Ninetailsrabbit.Match3Maker - 1.1.4 + 1.1.5 This lightweight library provides the core logic and functionality you need to build engaging match-3 games. Focus on game design and mechanics while leaving the complex logic to this library © 2024 Ninetailsrabbit Ninetailsrabbit diff --git a/Match3Maker/src/components/Board.cs b/Match3Maker/src/components/Board.cs index 693fd36..19a59ff 100644 --- a/Match3Maker/src/components/Board.cs +++ b/Match3Maker/src/components/Board.cs @@ -4,14 +4,29 @@ namespace Match3Maker { - public sealed class VirtualBoard(List gridCells) { - public List GridCells = gridCells; + #region Virtual board + public sealed class VirtualBoard { + public List GridCells = []; public List Updates = []; + public VirtualBoard(List> gridCells) { + GridCells = gridCells.SelectMany(cells => cells).Select(cell => new GridCell(cell.Column, cell.Row, cell.Piece, cell.CanContainPiece)).ToList(); + + // Only assign neighbours to fall & side down pieces + GridCells.ForEach(cell => { + cell.NeighbourBottom = Cell(cell.Column, cell.Row + 1); + cell.DiagonalNeighbourBottomRight = Cell(cell.Column + 1, cell.Row + 1); + cell.DiagonalNeighbourBottomLeft = Cell(cell.Column - 1, cell.Row + 1); + }); + } + + public GridCell? Cell(int column, int row) => GridCells.FirstOrDefault(cell => cell.Column.Equals(column) && cell.Row.Equals(row)); + public List EmptyCells() => GridCells.Where(cell => cell.IsEmpty() && cell.CanContainPiece).ToList(); public void AddUpdate(BoardCellUpdate update) { Updates.Add(update); } - + public List MovementUpdates() => Updates.Where(update => update.CurrentUpdateType.Equals(UPDATE_TYPE.MOVEMENT)).ToList(); + public List FillUpdates() => Updates.Where(update => update.CurrentUpdateType.Equals(UPDATE_TYPE.FILL)).ToList(); } public sealed class BoardCellUpdate(UPDATE_TYPE currentUpdateType, CellPieceMovement? cellPieceMovement = null, CellPieceFill? cellPieceFill = null) { public enum UPDATE_TYPE { @@ -26,10 +41,10 @@ public enum UPDATE_TYPE { public bool IsFill() => CurrentUpdateType.Equals(UPDATE_TYPE.FILL); } - public record CellPieceMovement(GridCell PreviousCell, GridCell NewCell, Piece Piece); public record CellPieceFill(GridCell Cell, Piece Piece); + #endregion public class Board { public static readonly int MIN_GRID_WIDTH = 3; @@ -358,13 +373,13 @@ public List LeftCellsFrom(GridCell cell, int distance) { public List CellsThatCannotContainPiecesFromRow(int row) => CellsFromRow(row).Where(cell => !cell.CanContainPiece).ToList(); public List CellsThatCanContainPiecesFromColumn(int column) => CellsFromColumn(column).Where(cell => cell.CanContainPiece).ToList(); public List CellsThatCannotContainPiecesFromColumn(int column) => CellsFromColumn(column).Where(cell => !cell.CanContainPiece).ToList(); - public Sequence CreateSequenceFromRow(int row) => new(CellsThatCanContainPiecesFromRow(row)); - public Sequence CreateSequenceFromColumn(int column) => new(CellsThatCanContainPiecesFromColumn(column)); - public Sequence CreateSequenceFromRowOfPieceType(int row, Type type) => new(CellsFromRowOfPieceType(row, type)); - public Sequence CreateSequenceFromColumnOfPieceType(int column, Type type) => new(CellsFromColumnOfPieceType(column, type)); + public Sequence CreateSequenceFromRow(int row) => new(CellsThatCanContainPiecesFromRow(row), Sequence.SHAPES.HORIZONTAL); + public Sequence CreateSequenceFromColumn(int column) => new(CellsThatCanContainPiecesFromColumn(column), Sequence.SHAPES.VERTICAL); + public Sequence CreateSequenceFromRowOfPieceType(int row, Type type) => new(CellsFromRowOfPieceType(row, type), Sequence.SHAPES.HORIZONTAL); + public Sequence CreateSequenceFromColumnOfPieceType(int column, Type type) => new(CellsFromColumnOfPieceType(column, type), Sequence.SHAPES.VERTICAL); public Sequence CreateSequenceOfCellsWithPieceType(Type type) => new(CellsWithPieceType(type)); - public Sequence CreateSequenceFromRowOfShape(int row, string shape) => new(CellsFromRowOfShape(row, shape)); - public Sequence CreateSequenceFromColumnOfShape(int column, string shape) => new(CellsFromColumnOfShape(column, shape)); + public Sequence CreateSequenceFromRowOfShape(int row, string shape) => new(CellsFromRowOfShape(row, shape), Sequence.SHAPES.HORIZONTAL); + public Sequence CreateSequenceFromColumnOfShape(int column, string shape) => new(CellsFromColumnOfShape(column, shape), Sequence.SHAPES.VERTICAL); public Sequence CreateSequenceOfCellsWithShape(string shape) => new(CellsWithShape(shape)); public List CellsFromColumn(int column) { @@ -516,30 +531,32 @@ public void RemoveMatchesFromBoard() { } public VirtualBoard MovePiecesAndFillEmptyCells() { - var gridCellsCopy = GridCells.SelectMany(cells => cells).Select(cell => new GridCell(cell.Column, cell.Row, cell.Piece, cell.CanContainPiece)).ToList(); + if (GridCells.IsEmpty()) + throw new ArgumentException("The board need to have the grid cells prepared, this operation cannot be done"); - VirtualBoard virtualBoard = new(gridCellsCopy); + VirtualBoard virtualBoard = new(GridCells); - if (GridCells.Count > 0 && IsFree()) { + var pendingMoves = PendingFallMoves(virtualBoard.GridCells); - var pendingMoves = PendingFallMoves(virtualBoard.GridCells); + while (pendingMoves.Count > 0) { + foreach (var currentCell in pendingMoves) { + GridCell? bottomCell = currentCell.NeighbourBottom; - while (pendingMoves.Count > 0) { - virtualBoard.Updates.Clear(); + if (bottomCell is not null) { + bottomCell.AssignPiece(currentCell.Piece); + currentCell.RemovePiece(); - foreach (var currentCell in pendingMoves) { - GridCell? bottomCell = currentCell.NeighbourBottom; + virtualBoard.AddUpdate(new BoardCellUpdate(UPDATE_TYPE.MOVEMENT, new(currentCell, bottomCell, bottomCell.Piece))); + } + } - if (bottomCell is not null) { - bottomCell.AssignPiece(currentCell.Piece); - currentCell.RemovePiece(); + pendingMoves = PendingFallMoves(virtualBoard.GridCells); + } - virtualBoard.AddUpdate(new BoardCellUpdate(UPDATE_TYPE.MOVEMENT, new(currentCell, bottomCell, bottomCell.Piece))); - } - } + foreach (var emptyCell in virtualBoard.EmptyCells()) { + GenerateRandomPieceOnCell(emptyCell); - pendingMoves = PendingFallMoves(virtualBoard.GridCells); - } + virtualBoard.AddUpdate(new BoardCellUpdate(UPDATE_TYPE.FILL, null, new(emptyCell, emptyCell.Piece))); } return virtualBoard; @@ -548,10 +565,10 @@ public VirtualBoard MovePiecesAndFillEmptyCells() { public List PendingFallMoves(IEnumerable? cells = null) { cells ??= GridCells.SelectMany(cells => cells).Select(cell => cell).ToList(); - return cells.Where( - cell => cell.HasPiece() && - cell.Piece.Type.CanBeMoved() && - cell.NeighbourBottom is GridCell bottomCell && bottomCell.IsEmpty()) + return cells.Where(cell => cell.HasPiece() && cell.Piece.Type.CanBeMoved() && cell.NeighbourBottom is not null) + .Where(cell => (SelectedFillMode.Equals(FILL_MODES.FALL_DOWN) && cell.NeighbourBottom.IsEmpty()) || + (SelectedFillMode.Equals(FILL_MODES.SIDE_DOWN) && (cell.NeighbourBottom.IsEmpty() || cell.NeighbourBottom.HasPiece()) && (cell.DiagonalNeighbourBottomRight.IsEmpty() || cell.DiagonalNeighbourBottomLeft.IsEmpty())) + ) .ToList(); } #endregion