From 6cc1092872de5b6ef3ec99d99da06fcf04691bd7 Mon Sep 17 00:00:00 2001 From: Texel Date: Tue, 24 Jan 2023 09:07:23 -0500 Subject: [PATCH] Initial board state and tile info data setup, unit tests for board setup / serializer Framework for the full tile type color matching, but it's all data structures and validating the structure --- Assets/Gameplay.meta | 8 + Assets/Gameplay/GameBoard.cs | 273 ++++++++++++++++++ Assets/Gameplay/GameBoard.cs.meta | 11 + Assets/Runtime/Extensions/RandomFromArray.cs | 10 + .../Extensions/RandomFromArray.cs.meta | 11 + 5 files changed, 313 insertions(+) create mode 100644 Assets/Gameplay.meta create mode 100644 Assets/Gameplay/GameBoard.cs create mode 100644 Assets/Gameplay/GameBoard.cs.meta create mode 100644 Assets/Runtime/Extensions/RandomFromArray.cs create mode 100644 Assets/Runtime/Extensions/RandomFromArray.cs.meta diff --git a/Assets/Gameplay.meta b/Assets/Gameplay.meta new file mode 100644 index 0000000..7b30c65 --- /dev/null +++ b/Assets/Gameplay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f2336f3c0f2bcc54188adc8cbc44cce6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Gameplay/GameBoard.cs b/Assets/Gameplay/GameBoard.cs new file mode 100644 index 0000000..17f16cc --- /dev/null +++ b/Assets/Gameplay/GameBoard.cs @@ -0,0 +1,273 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +using System.Linq; +using Hashtable = ExitGames.Client.Photon.Hashtable; // Compatibility + +using ExitGames.Client.Photon; + +using EntityNetwork; + + +// Shorthand aliases for nested list nonsense +using BoardData = System.Collections.Generic.List>; +// Tile Stack is the vertical list of tiles +using TileStack = System.Collections.Generic.List; + +#region Board Logic +public class GameBoard : EntityBase, IAutoSerialize, IAutoDeserialize { + BoardState board = new BoardState(); + + public override void Deserialize(Hashtable h) { + base.Serialize(h); + if (h.TryGetValue('b', out var hashedBoard)) + board = new BoardState((Hashtable)hashedBoard); + } + + public override void Serialize(Hashtable h) { + base.Serialize(h); + h.Add('b', board.ToHashtable()); + } + + public TileColor[] Options; + + // Short form for random tiles + TileInfo Random => TileInfo.Random(Options); + + [ContextMenu("Test board")] + public void TestBoard() { + for(int i = 0; i < BoardState.BoardWidth; ++i) { + board[i] = new TileStack(new[] { Random, Random, Random }); + } + } + + [ContextMenu("Test Board Serialization")] + public void TestSerialize() { + board = BoardState.Copy(board); + Debug.Log(board.ToHashtable()); + } +} + +#endregion + + +#region Board state related +public enum TileColor : byte { + Empty = 0, + Red = 1, + Blue = 2, + Green = 3, + Pink = 4, + Purple = 5, + // Room for more colors? + + Rock = 200, // Cannot be matched, destroyed fully on adjacent match + //Fairy = 201, // Fairy is handled as detail + Bomb = 202, // 3x3 radius destruction + Dynamite = 203, // 5x5 cross destruction + Seal = 204, // destroys all matching color on landing + //Ice = 205, // Ice is handled as a detail + Mystery = 205, // Becomes random basic on destroy + Wildcard = 206, // Pairs with any + Spark = 207 // Removes entire column on create +} + +public enum TileDetail : byte { + None = 0x0, + Air = 0x0, // None and Air as aliases + Normal = 0x1, // Flag that tile exists + Pending = 0x2, // State for active in combo but not cleared + //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 + /* 0x20 unused */ + Tanuki = 0x40, // Changes to a simple colored tile when it would expire + Ice = 0x80, // Same as Tanuki but doesn't change color +} + +// Tile info is made with packed bytes into an int +// Could this be thinner? Yes, but Jam Game -- Texel +[System.Serializable] +public class TileInfo { + public int data; + + public TileDetail detail { + // Pack detail into least significant bits (0xFF) + set { + data = (data & ~0xFF) | ((byte)value & 0xFF); + } + get => (TileDetail)(data & 0xFF); + } + public TileColor color { + // color gets second least significant bits (0xFF00) + set { + data = (data & ~0xFF00) | (((byte)value & 0xFF) << 8); + } + get => (TileColor)((data & 0xFF00) >> 8); + } + + // Constructor using raw data + public TileInfo(int _data) { + data = _data; + } + + public static TileInfo Random(params TileColor[] options) { + return new TileInfo(options.GetRandom()); + } + + public TileInfo(TileColor c, byte detail = 1) { + // Make a new Tile from the color, assuming detail with a default + var ti = new TileInfo((byte)c & ((detail & (byte)0xFF) << 8)); + } + + public TileInfo AsTanuki() { + detail &= TileDetail.Tanuki; + return this; + } + + public TileInfo AsIce() { + detail &= TileDetail.Ice; + return this; + } + + public TileInfo BreakTile(params TileColor[] options) { + var d = detail; + + switch (detail) { + case TileDetail.Tanuki: + return Random(options); // Replace with new random tile + case TileDetail.Ice: // Remove just the ice flag, keep color + detail = detail & ~TileDetail.Ice; + return this; + case TileDetail.Normal: + goto default; + default: + return Air(); + } + } + + // Make and return a new air tile + public static TileInfo Air() => new TileInfo(0); // Just air + public bool isAir => data == 0; + + public static implicit operator int(TileInfo ti) => ti.data; + public static explicit operator TileInfo(int i) => new TileInfo(i); + + 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 ((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 + return false; + + // room for more weird custom magic logic? + + default: + return false; + } + } +} +#endregion + +#region BoardStates + +[System.Serializable] +public class BoardState { + 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 + public BoardData state; + + // Grab a particular stack, allows [][] notation (no bounds checking) + public TileStack this[int col] { + get => state[col]; + set { + state[col] = value; + } + } + + // Failable indexing setup + public bool TryCol(int col, out TileStack ts) { + ts = null; + if (col < 0) return false; + if (col > BoardWidth) return false; + ts = this[col]; + return true; + } + 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 + + // Grab the column and fail out if we can't get it + if (!TryCol(x, out TileStack col)) + return false; + + // Fail out if the column isn't tall enough + if (col.Count < y) + return false; + + ti = col[y]; + return true; + } + + public void init() { + state = new List>(); + for (int i = 0; i < BoardWidth; ++i) + state.Add(new TileStack()); + } + + public BoardState() { + init(); + } + + // Create a copy of a column + TileStack Copy(TileStack ts) { + var intform = ts.Select(tile => (int)tile); + return intform.Select(tile => (TileInfo)tile).ToList(); + } + + // Create a copy of a board + public static BoardState Copy(BoardState bs) => bs.SelfCopy(); + public BoardState SelfCopy() { + var bs = new BoardState(); + + for(int i = 0; i < state.Count; ++i) + bs.state[i] = Copy(state[i]); + + return bs; + } + + // Constructor that uses the networked hashtable + public BoardState(Hashtable ht) { + init(); // Setup the empty column configuration + for (int i = 0; i < BoardWidth; ++i) { + // Grab the int[]'d data from the hashtable + var intArray = (int[])ht[i]; + // Convert it to a List (TileStack) and load it into the column slot + state[i] = intArray.Select(tile => (TileInfo)tile).ToList(); + } + } + + // Hashtable conversions + public static BoardState FromHashtable(Hashtable ht) => new BoardState(ht); + public Hashtable ToHashtable() { + var ht = new Hashtable(); + for (int i = 0; i < state.Count; ++i) { + var column = state[i]; + // Convert to an int array and push into the hashtable, keyed to our column number + ht.Add(i, column.Select(t => (int)t).ToArray()); + } + return ht; + } +} + +#endregion diff --git a/Assets/Gameplay/GameBoard.cs.meta b/Assets/Gameplay/GameBoard.cs.meta new file mode 100644 index 0000000..d31819c --- /dev/null +++ b/Assets/Gameplay/GameBoard.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d402e786b88425242825f7fe22d84afb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Runtime/Extensions/RandomFromArray.cs b/Assets/Runtime/Extensions/RandomFromArray.cs new file mode 100644 index 0000000..45891e3 --- /dev/null +++ b/Assets/Runtime/Extensions/RandomFromArray.cs @@ -0,0 +1,10 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +// Helper method for randomly rolling from a generic array +public static class RandomFromArray { + public static T GetRandom(this T[] array) { + return array[Random.Range(0, array.Length)]; + } +} diff --git a/Assets/Runtime/Extensions/RandomFromArray.cs.meta b/Assets/Runtime/Extensions/RandomFromArray.cs.meta new file mode 100644 index 0000000..99b64d3 --- /dev/null +++ b/Assets/Runtime/Extensions/RandomFromArray.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e6a7d58d735c2354ca8b3d974cd906ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: