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