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
This commit is contained in:
Texel 2023-01-24 09:07:23 -05:00 committed by Touhexel
parent 9b69003715
commit 6cc1092872
5 changed files with 313 additions and 0 deletions

8
Assets/Gameplay.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f2336f3c0f2bcc54188adc8cbc44cce6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<System.Collections.Generic.List<TileInfo>>;
// Tile Stack is the vertical list of tiles
using TileStack = System.Collections.Generic.List<TileInfo>;
#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<List<TileInfo>>();
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<TileInfo> (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

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d402e786b88425242825f7fe22d84afb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<T>(this T[] array) {
return array[Random.Range(0, array.Length)];
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6a7d58d735c2354ca8b3d974cd906ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: