diff --git a/Assets/Texel/Gameplay/GameBoard.cs b/Assets/Texel/Gameplay/GameBoard.cs index c52a7b2..6f2baef 100644 --- a/Assets/Texel/Gameplay/GameBoard.cs +++ b/Assets/Texel/Gameplay/GameBoard.cs @@ -15,24 +15,10 @@ using BoardData = System.Collections.Generic.List; -using Cluster = System.Collections.Generic.List<(int x, int y)>; - #region Board Logic public class GameBoard : EntityBase, IAutoSerialize, IAutoDeserialize { public BoardState board = new BoardState(); - GameBoardDrawer _drawer; - public GameBoardDrawer drawer { - get { - if (_drawer) return _drawer; - return _drawer = GetComponent(); - } - } - - public void BreakFX(int x, int y) { - drawer[x, y]?.OnBreakFX(); - } - public override void Deserialize(Hashtable h) { base.Serialize(h); if (h.TryGetValue('b', out var hashedBoard)) @@ -68,9 +54,6 @@ public class GameBoard : EntityBase, IAutoSerialize, IAutoDeserialize { board = BoardState.Copy(board); Debug.Log(board.ToString()); } - - [ContextMenu("Recursive Sim")] - void RecursiveSim() => board = board.SimulateRecursive(this, out _); } #endregion @@ -102,7 +85,7 @@ public enum TileDetail : byte { // Gets cast to byte a lot Air = 0x0, // None and Air as aliases Normal = 0x1, // Flag that tile exists Pending = 0x2, // State for active in combo but not cleared - Exploded = 0x04, // State for a block that is being exploded + //Damaged = 0x04, // Used for Tanuki and Ice -- Actually unneccessary by replacement setup Dropped = 0x08, // Flag for tiles that were just dropped, for smears Fairy = 0x10, // do not clear air under this Poofed = 0x20, // Drawn as a cloud, freshly removed @@ -154,40 +137,16 @@ public class TileInfo { data = ti.data; // More delicious garbage } - public static bool SameAs(TileInfo a, TileInfo b) { - if (a.color != b.color) return false; - - // Internal flags we don't consider for if a tile is the same - var irrelevant = TileDetail.Dropped | TileDetail.Poofed | TileDetail.Pending | TileDetail.Exploded; - if ((a.detail & ~irrelevant) != (b.detail & ~irrelevant)) - return false; - - return true; - } - - public TileInfo SetFalling(bool fall) { - detail &= ~TileDetail.Dropped; - if (fall) detail |= TileDetail.Dropped; - return this; - } - public TileInfo AsTanuki() { - detail |= TileDetail.Tanuki; + detail &= TileDetail.Tanuki; return this; } public TileInfo AsIce() { - detail |= TileDetail.Ice; + detail &= TileDetail.Ice; return this; } - public TileInfo SetPending() { - detail |= TileDetail.Pending; - return this; - } - - public bool isPending => (detail & TileDetail.Pending) != 0; - public TileInfo BreakTile(params TileColor[] options) { var d = detail; @@ -211,22 +170,17 @@ public class TileInfo { public static implicit operator int(TileInfo ti) => ti.data; public static explicit operator TileInfo(int i) => new TileInfo(i); - public bool CombosWith(TileInfo other) { - if (other == null) return false; - return CombosWith(other.color); - } + public bool CombosWith(TileInfo other) => CombosWith(color); public bool CombosWith(TileColor c) { if (c == 0) return false; // nothing combos with air if (color == 0) return false; // Also don't combo with us if we're air - if (c == color && (byte)c < 200) return true; - if ((byte)c < 200) // If it's a simple color block return c == color; // just match if it matches our own color switch (c) { // Special handling for wildcard case TileColor.Wildcard: - //if ((byte)color < 200) return true; // If we are a simple block + if ((byte)color < 200) return true; // If we are a simple block return false; // room for more weird custom magic logic? @@ -238,347 +192,11 @@ public class TileInfo { } #endregion -#region Board Logic - -public static class BoardLogic { - // return height of tallest column - public static int TallestStack(this BoardState bs) - => bs.state.Max(t => t.Count); - - - // Get a tile from a stack padded with nulls - public static TileInfo NullPadded(this TileStack ts, int row) { - if (row < 0) return null; - if (row >= ts.Count) return null; - - return ts[row]; - } - // Get the tile from the stack padded with air - public static TileInfo Padded(this TileStack ts, int row) - => ts.NullPadded(row) ?? TileInfo.Air; - - public static void ChangeTile(this TileStack ts, int row, TileInfo ti) { - if (row < 0) return; - if (row >= ts.Count) return; - ts[row] = ti; - } - - public static bool StackHasMatches(this TileStack ts) { - // Check up the length of the column for a match 3 - for (int y = 0; y < ts.Count; ++y) { - var at = ts[y]; - var match = at.CombosWith(ts.Padded(y + 1)) && at.CombosWith(ts.Padded(y + 2)); - if (match) return true; - } - return false; - } - - public static IEnumerable Neighbors(this BoardState bs, int x, int y) { - var self = bs.tile(x, y); - var neighborsWithNull = - new[] { - bs.tile(x + 1, y), - bs.tile(x - 1, y), - bs.tile(x, y + 1), - bs.tile(x, y - 1) }; - - return neighborsWithNull.Where(t => t != null).Where(t => t != self); - } - - public static IEnumerable ComboAdjacents(this BoardState bs, int x, int y) { - var neighbors = bs.Neighbors(x, y); - var self = bs.tile(x, y); - - return neighbors.Where(ti => ti.CombosWith(self)); - } - - public static IEnumerable<(int x, int y)> MatchingAdjacentCoordinates(this BoardState bs, (int x, int y) p) { - var matches = new List<(int x, int y)>(); - var self = bs.TileAtPoint(p); - - // HACK return empty set if self is null - if (self == null) return new (int x, int y)[] { }; - - var (x, y) = p; - - if (self.CombosWith(bs.tile(p.x + 1, p.y))) - matches.Add((x+1, y)); - if (self.CombosWith(bs.tile(p.x - 1, p.y))) - matches.Add((x-1, y)); - if (self.CombosWith(bs.tile(p.x, p.y + 1))) - matches.Add((x, y+1)); - if (self.CombosWith(bs.tile(p.x, p.y - 1))) - matches.Add((x, y-1)); - - return matches; - } - - - public static Cluster Clusterize(this BoardState bs, int x, int y) { - var at = bs.tile(x, y); - if (at == null) return null; - - List<(int x, int y)> OpenSet, ClosedSet; - OpenSet = new List<(int x, int y)>(); - ClosedSet = new List<(int x, int y)>(); - - ClosedSet.Add((x, y)); - OpenSet.AddRange(bs.MatchingAdjacentCoordinates((x, y))); - - while (OpenSet.Count > 0) { - var element = OpenSet[0]; - OpenSet.RemoveAt(0); - - ClosedSet.Add(element); - - var matches = bs.MatchingAdjacentCoordinates(element); - foreach (var match in matches) { - if (ClosedSet.Contains(match)) - continue; - OpenSet.Add(match); - } - } - - return ClosedSet; - } - - public static int[] MatchingNeighbors(this TileStack ts, int y) { - var self = ts[y]; - if (self == null) goto Empty; - if (self.isAir) goto Empty; - - var above = ts.NullPadded(y + 1); - var below = ts.NullPadded(y - 1); - - bool aboveMatch = false; - bool belowMatch = false; - - if (above != null && TileInfo.SameAs(self, above)) - aboveMatch = true; - - if (below != null && TileInfo.SameAs(self, below)) - belowMatch = true; - - if (aboveMatch && belowMatch) - return new[] {y+1, y-1}; - if (aboveMatch) - return new[] { y + 1 }; - if (belowMatch) - return new[] { y - 1 }; - - Empty: - return new int[] { }; - } - - public static Cluster ClusterizeVertical(this BoardState bs, int x, int y) { - var stack = bs[x]; - - var self = stack[y]; - if (self == null) return null; - if (self.isAir) return null; - - var OpenSet = new List(); - var ClosedSet = new List(new[] { y }); - - OpenSet.AddRange(stack.MatchingNeighbors(y)); - - while (OpenSet.Count > 0) { - var element = OpenSet[0]; - OpenSet.RemoveAt(0); - - ClosedSet.Add(element); - var matches = stack.MatchingNeighbors(element); - - foreach(var match in matches) { - if (ClosedSet.Contains(match)) - continue; - OpenSet.Add(match); - } - } - - // Expand back out to (x, y) from the list of y's - return ClosedSet.Select(e => (x, e)).ToList(); - } - - public static BoardState Collapse(this BoardState bs) { - // TODO: Proper support for fairy blocks - - for (int x = 0; x < bs.state.Count; ++x) { - var col = bs[x]; - - // First, pad to length with air - while (col.Count < BoardState.BoardHeight) - col.Add(TileInfo.Air); - - var oldCol = col.Copy(); - - col.RemoveAll(t => t.isAir); - - // Repad with air - while (col.Count < BoardState.BoardHeight) - col.Add(TileInfo.Air); - - // TODO: Better falling logic - /* - // Set the falling flag for drawing - for(int i = 0; i < col.Count; ++i) { - col[i].SetFalling(TileInfo.SameAs(col[i], oldCol[i])); - }*/ - } - return bs; - } - - public static BoardState Place(this BoardState bs,Move m) { - var state = bs.SelfCopy(); - - var (first, second, third) = m.triplet; - - state.SetAtPoint(m.location, first); - state.SetAtPoint(m.locationB, second); - state.SetAtPoint(m.locationC, third); - - return state; - } - - // Set all matches tiledetails to pending - public static BoardState Activate(this BoardState bs, out int activations) { - activations = 0; - - var expandedClusters = new List(); - - // First, determine if any column contains a match - for (int x = 0; x < bs.state.Count; ++x) { - var col = bs[x]; - - // Exit if there is no match in the column - if (!col.StackHasMatches()) - continue; - - // We know there is a match in the column, get the coordinates of all valid clusters - var validBlobs = new List(); - - for (int y = 0; y < col.Count; ++y) { - var stackClusters = bs.ClusterizeVertical(x, y); - if (stackClusters != null && stackClusters.Count >= 3) { - validBlobs.Add(stackClusters); - } else - continue; - } - - // Expand matches horizontally to matches - foreach(var cluster in validBlobs) { - foreach(var point in cluster) { - expandedClusters.Add(bs.Clusterize(point.x,point.y)); - } - } - } - - foreach(var cluster in expandedClusters) { - foreach(var point in cluster) { - var at = bs.TileAtPoint(point); - if (at == null || at.isPending) - continue; - - at.SetPending(); - activations += 1; // Increment the activation counter - } - } - - return bs; - } - - public static BoardState BreakPending(this BoardState bs, GameBoard gb, bool sendCallbacks = false) { - var broken = new Cluster(); - for(int x = 0; x < bs.state.Count; ++x) { - for (int y = 0; y < bs.state.Count; ++y) { - var at = bs.tile(x, y); - - if (at.isAir) continue; // Skip air - - if (at.isPending) { - broken.Add((x, y)); - - if (sendCallbacks) - gb.BreakFX(x, y); - at = at.BreakTile(gb.Options); - } - } - } - return bs; - } - - // Simulate the eventual outcome of this board - // Note this is NOT deterministic because of how random tiles break - public static BoardState SimulateRecursive(this BoardState bs, GameBoard gb, out int activations) { - activations = 0; - - // Iterations for the current step - int stepActivations = 0; - do { - bs = bs.Collapse().Activate(out stepActivations).BreakPending(gb); - activations += stepActivations; - // Repeat until no new tiles activate - } while (stepActivations > 0); - - return bs; - } - - // Create a copy of a column - static TileStack Copy(this TileStack ts) { - var intform = ts.Select(tile => (int)tile); - return intform.Select(tile => (TileInfo)tile).ToList(); - } - - public static (int x, int y) ToPair(this MoveDir md) { - switch(md) { - case MoveDir.Left: - return (-1, 0); - case MoveDir.Right: - return (1, 0); - case MoveDir.Up: - return (0, 1); - case MoveDir.Down: - return (0, -1); - default: - return (0, 0); - } - } -} - -#endregion - -#region moves - -public enum MoveDir { None, Left, Right, Up, Down } - -public class Move { - public (TileInfo first, TileInfo second, TileInfo third) triplet; - public (int x, int y) location; - - public MoveDir first, second; - - public (int x, int y) locationB { - get { - var (x, y) = first.ToPair(); - return (location.x + x, location.y + y); - } - } - - public (int x, int y) locationC { - get { - var (x, y) = second.ToPair(); - return (locationB.x + x, locationB.y + y); - } - } -} - -#endregion - #region BoardStates [System.Serializable] public class BoardState { - public static readonly int BoardWidth = 6, BoardHeight = 12; + public static readonly int BoardWidth = 4, BoardHeight = 9; // Top THREE rows of board are the placement zone // Internal state of the board, as a list @@ -596,30 +214,11 @@ public class BoardState { public bool TryCol(int col, out TileStack ts) { ts = null; if (col < 0) return false; - if (col >= state.Count) return false; + if (col > state.Count) return false; ts = this[col]; return true; } - - public TileInfo TileAtPoint((int x, int y) p) { - if (TryCol(p.x, out var col)) - return col.NullPadded(p.y); - return null; - } - - public void SetAtPoint((int x, int y) p, TileInfo tile) { - if (TryCol(p.x, out var col)) { - col.ChangeTile(p.x, tile); - } - } - - // Return the tile at a position with null if it's not valid - public TileInfo tile(int x, int y) { - TryTileAt(x, y, out var ti); - return ti; - } - - public bool TryTileAt(int x, int y, out TileInfo ti) { + public bool TileAt(int x, int y, out TileInfo ti) { ti = null; if (y < 0) return false; // fail out if asking for negative y coords diff --git a/Assets/Texel/Gameplay/GameBoardDrawer.cs b/Assets/Texel/Gameplay/GameBoardDrawer.cs index 47015e3..2f76430 100644 --- a/Assets/Texel/Gameplay/GameBoardDrawer.cs +++ b/Assets/Texel/Gameplay/GameBoardDrawer.cs @@ -32,17 +32,6 @@ public class GameBoardDrawer : MonoBehaviour { List> TileDrawers; - // Get the Offset for an X/Y coord - public Vector3 Position(int x, int y) { - return new Vector3(x, y) * TileSize; - } - - public TileDrawer this[int x, int y] { - get { - return TileDrawers[x][y]; - } - } - public float TileSize = 0.5f; void ValidateDrawers() { @@ -80,7 +69,7 @@ public class GameBoardDrawer : MonoBehaviour { var tileT = tile.transform; tileT.localPosition = new Vector3(x, y) * TileSize; - if (boardState.TryTileAt(x, y,out TileInfo ti)) { + if (boardState.TileAt(x, y,out TileInfo ti)) { tile.toDraw = ti; } else { tile.toDraw = TileInfo.Air; diff --git a/Assets/Texel/Gameplay/TileObjects/TileDrawer.cs b/Assets/Texel/Gameplay/TileObjects/TileDrawer.cs index 8bea302..a0aa4ce 100644 --- a/Assets/Texel/Gameplay/TileObjects/TileDrawer.cs +++ b/Assets/Texel/Gameplay/TileObjects/TileDrawer.cs @@ -13,12 +13,6 @@ public class TileDrawer : MonoBehaviour { public TileDetail detail = TileDetail.Normal; public TileColor color; - // TODO Use this FX hook - // This is called BEFORE the tile detail is updated - public void OnBreakFX() { - - } - void OnValidate() { if (toDraw == null) { toDraw = new TileInfo(color,detail); @@ -145,10 +139,6 @@ public class TileDrawer : MonoBehaviour { if (!tsi) return; if (toDraw == null) return; - // Higher tiles draw in front - var t = transform; - t.LeanSetPosZ(t.position.y); - // Copy the data from the TileInfo reference to local enums color = toDraw.color; detail = toDraw.detail; @@ -172,14 +162,10 @@ public class TileDrawer : MonoBehaviour { sprite = deets.TopOfStack; } + // non-snowman-y states if (detail.HasFlag(TileDetail.Poofed)) sprite = deets.Puff; - - - // FIXME This is wrong - if (detail.HasFlag(TileDetail.Pending)) - sprite = deets.Puff; } else { // no matching TileObject data switch (color) {