979 lines
26 KiB
C#
979 lines
26 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
using System.Linq;
|
|
using Hashtable = ExitGames.Client.Photon.Hashtable;
|
|
|
|
using BoardState = System.Collections.Generic.List<System.Collections.Generic.List<TileInfo>>;
|
|
using ExitGames.Client.Photon;
|
|
|
|
using EntityNetwork;
|
|
|
|
public class GameBoard : EntityBase, IAutoSerialize, IAutoDeserialize {
|
|
public const int COLUMN = 6, ROW = 12;
|
|
|
|
public GameBoardRender render;
|
|
|
|
[System.NonSerialized]
|
|
public BoardState board;
|
|
private BoardState lastSentBoard;
|
|
|
|
public bool active;
|
|
|
|
public enum DelayState { None, Collapse, Combo, Loss }
|
|
public DelayState delayState = DelayState.None;
|
|
|
|
// TIL I need to always have an update timer or the dispatcher doesn't get created
|
|
[NetVar('n', true, true, 100)]
|
|
public int NextDrop;
|
|
[NetVar('c', true, true)]
|
|
public int CurrentDrop;
|
|
|
|
public (TileInfo left, TileInfo right) currentPair {
|
|
get => TileInfo.FromPairInt(CurrentDrop);
|
|
set => CurrentDrop = TileInfo.ToPairInt(value);
|
|
}
|
|
public (TileInfo left, TileInfo right) nextPair {
|
|
get => TileInfo.FromPairInt(NextDrop);
|
|
set => NextDrop = TileInfo.ToPairInt(value);
|
|
}
|
|
|
|
// Simple reference rotation
|
|
public void SwapTiles() {
|
|
var pair = currentPair;
|
|
currentPair = nextPair;
|
|
nextPair = pair;
|
|
}
|
|
|
|
[Header("Settings")]
|
|
// Tile timing
|
|
private int TilesUntilActivator = 3;
|
|
int nextActivator = -1;
|
|
private int ActivatorsUntilSpecial = 10;
|
|
int nextSpecial = -1;
|
|
|
|
public float nextRootX = -1, nextRootY = 12;
|
|
|
|
TileColor lastActivatorColor;
|
|
public void ReplaceNextTile() {
|
|
(TileInfo left, TileInfo right) pair = (TileInfo.CreateRandomBlockTile(), TileInfo.CreateRandomBlockTile());
|
|
--nextActivator;
|
|
if (nextActivator < 1) {
|
|
nextActivator = TilesUntilActivator;
|
|
var activator = TileInfo.CreateRandomActivatorTile();
|
|
while (activator.color == lastActivatorColor)
|
|
activator = TileInfo.CreateRandomActivatorTile();
|
|
|
|
lastActivatorColor = activator.color;
|
|
|
|
if (++nextSpecial >= ActivatorsUntilSpecial) {
|
|
nextSpecial = 0;
|
|
activator.kind = TileKind.Special;
|
|
}
|
|
|
|
if (Random.value > 0.5f) {
|
|
pair.left = activator;
|
|
} else {
|
|
pair.right = activator;
|
|
}
|
|
}
|
|
|
|
nextPair = pair;
|
|
}
|
|
|
|
public float airCollapseTime = 1f;
|
|
public float activationSpreadTime = 0.5f;
|
|
public int dropHeight = 12; // Y value to drop from
|
|
|
|
[Header("Scoring")]
|
|
[NetVar('C', true, true)]
|
|
public int Combo; // Increments on successively chained combos
|
|
[NetVar('S', true, true)]
|
|
public int score = 0;
|
|
|
|
|
|
#region GameLogic
|
|
// Time without moving that the game will kill you if you have lethal trash
|
|
public float AutoDeathTime = 10f;
|
|
|
|
public enum AnimationName { Idle, Happy, Sad }
|
|
public AnimationName desiredAnimation;
|
|
|
|
public float timeInState = 0;
|
|
public DelayState lastState = DelayState.None;
|
|
void GameLogic() {
|
|
desiredAnimation = AnimationName.Idle; // Start off assuming idle
|
|
if (HighestStack(board) > 9)
|
|
desiredAnimation = AnimationName.Sad;
|
|
|
|
if (delayState != lastState) {
|
|
timeInState = 0;
|
|
}
|
|
lastState = delayState;
|
|
|
|
timeInState += Time.deltaTime;
|
|
|
|
// Handle player input always, but block dropping when in a state
|
|
if (!AIEnabled)
|
|
PlayerInput();
|
|
|
|
switch (delayState) {
|
|
case DelayState.Collapse:
|
|
if (timeInState > airCollapseTime) {
|
|
board = Collapse(board); // Remove air
|
|
board = SpecialActivation(board, out bool didSpecial);
|
|
board = ActivateOnce(board, out bool didActivate);
|
|
if (didActivate || didSpecial) {
|
|
delayState = DelayState.Combo;
|
|
++Combo;
|
|
AudioProvider.Unk();
|
|
} else {
|
|
Combo = 0;
|
|
|
|
ApplyTrash();
|
|
|
|
delayState = DelayState.None;
|
|
}
|
|
}
|
|
return;
|
|
case DelayState.Combo:
|
|
desiredAnimation = AnimationName.Happy; // Look happy when we're combo the shit out of them
|
|
if (timeInState > activationSpreadTime) {
|
|
board = ActivateOnce(board, out bool didActivate);
|
|
if (didActivate) {
|
|
timeInState = 0;
|
|
return;
|
|
} else {
|
|
int scoreValue = CountActivations(board) * Combo;
|
|
|
|
// Score increments too slow, make it get bigger!
|
|
score += scoreValue * scoreValue;
|
|
SendTrash(scoreValue - 3);
|
|
|
|
// TODO - Trigger animation for attacking
|
|
|
|
GhostActivations(); // Cool FX!
|
|
board = ClearActivation(board);
|
|
//board = Collapse(board); // Turns out THIS is what breaks the cool collapse effects
|
|
// Overriding this like a dirty fellow to let me add negative buffer time
|
|
timeInState = -0.5f * airCollapseTime; // Take 1.5x longer to collapse from combo chain
|
|
lastState = DelayState.Collapse;
|
|
delayState = DelayState.Collapse;
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
case DelayState.None:
|
|
if (HighestStack(board) >= 13) {
|
|
RaiseEvent('l', true);
|
|
// TODO - Do a bunch of networking silliness to end the game here
|
|
}
|
|
|
|
var dangerLevel = HighestStackWithTrash(board, incomingTrash);
|
|
if (dangerLevel > 9) {
|
|
AudioProvider.RequestFastMusic();
|
|
desiredAnimation = AnimationName.Sad;
|
|
}
|
|
|
|
// If the player isn't taking actions, trash won't appear, so make instakills kill through inaction
|
|
if (timeInState > AutoDeathTime) {
|
|
if (dangerLevel > ROW) {
|
|
ApplyTrash();
|
|
} else {
|
|
timeInState = 0; // Reset the time in state before we check autodeath again
|
|
}
|
|
}
|
|
return;
|
|
case DelayState.Loss:
|
|
desiredAnimation = AnimationName.Sad;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Draw the big collapse of pieces after a few seconds
|
|
IEnumerator HandleLoss(float timer) {
|
|
if (board == null) yield break;
|
|
|
|
// Wait three seconds before doing the crumble so they can watch in dismay
|
|
// at the piles of trash that killed them
|
|
active = false;
|
|
|
|
yield return new WaitForSeconds(timer);
|
|
// First, crumble the board to be really cool
|
|
for(int x = 0; x < board.Count; ++x) {
|
|
var col = board[x];
|
|
for(int y = 0; y < col.Count; ++y) {
|
|
render.Crumble(col[y], (x, y));
|
|
}
|
|
}
|
|
|
|
// Now, re-initialize the board, so those falling pieces are the last of our board
|
|
board = BoardStateExtension.Initialize();
|
|
render.RebuildStack(board);
|
|
render.Render(board);
|
|
}
|
|
#endregion
|
|
|
|
#region Input
|
|
/// <summary>
|
|
/// Awaiting control stick neutral for control inputs to return
|
|
/// </summary>
|
|
public bool awaitingNeutral;
|
|
public static float inputThreshold = 0.3f;
|
|
|
|
public void PlayerInput() {
|
|
float rightIntent = Input.GetAxisRaw("Horizontal");
|
|
float upIntent = Input.GetAxisRaw("Rotate");
|
|
|
|
var max = Mathf.Max(Mathf.Abs(rightIntent), Mathf.Abs(upIntent));
|
|
if (max > 0.3f) {
|
|
if (!awaitingNeutral) {
|
|
if (rightIntent > 0.2f) {
|
|
dropColumn += 1;
|
|
} else if (rightIntent < -0.2f) {
|
|
dropColumn -= 1;
|
|
}
|
|
|
|
if (upIntent > 0.2f) {
|
|
playerRotation += 1;
|
|
} else if (upIntent < -0.2f) {
|
|
playerRotation -= 1;
|
|
}
|
|
|
|
// Shitty wrapping
|
|
if (playerRotation > 3)
|
|
playerRotation = 0;
|
|
if (playerRotation < 0)
|
|
playerRotation = 3;
|
|
|
|
dropColumn = ClampRotatedPosition(playerRotation);
|
|
dropHeight = playerRotation != 3 ? 12 : 13; // Shift the drop height based on rotation
|
|
}
|
|
awaitingNeutral = true;
|
|
} else {
|
|
awaitingNeutral = false;
|
|
}
|
|
|
|
if (Input.GetButtonDown("Drop")) {
|
|
if (delayState == DelayState.None) {
|
|
board = Collapse(board);
|
|
board = DropNow(board);
|
|
}
|
|
}
|
|
|
|
if (Input.GetButtonDown("Swap")) {
|
|
SwapTiles();
|
|
}
|
|
}
|
|
|
|
|
|
public BoardState DropNow(BoardState bs) {
|
|
AudioProvider.Unk();
|
|
|
|
bs = Place(bs, currentPair, playerRotation, dropColumn);
|
|
//this[dropColumn, dropHeight] = TileInfo.CreateRandomBlockTile();
|
|
bs = ReduceCountdowns(bs);
|
|
bs = Collapse(bs);
|
|
delayState = DelayState.Collapse;
|
|
|
|
SwapTiles();
|
|
ReplaceNextTile();
|
|
|
|
return bs;
|
|
}
|
|
#endregion
|
|
|
|
#region placement
|
|
[NetVar('p',true,true)]
|
|
public int dropColumn;
|
|
[NetVar('r', true, true)]
|
|
public int playerRotation = 0;
|
|
// Rotations (Coordinate is always on left)
|
|
// 0 - L/R
|
|
|
|
// 1 - L
|
|
// R
|
|
|
|
// 2 - R/L
|
|
|
|
// 3 - R
|
|
// L
|
|
|
|
private ((int x, int y) left, (int x, int y) right) GetPlacePosition(BoardState bs, int placeRotation, int dropColumn){
|
|
(int x, int y) left = (dropColumn, dropHeight), right = (dropColumn, dropHeight);
|
|
switch(placeRotation){
|
|
case 0:
|
|
right.x += 1;
|
|
break;
|
|
case 1:
|
|
right.y += 1;
|
|
break;
|
|
case 2:
|
|
right.x -= 1;
|
|
break;
|
|
case 3:
|
|
right.y -= 1;
|
|
break;
|
|
}
|
|
return (left, right);
|
|
}
|
|
|
|
public BoardState Place(BoardState bs,(TileInfo left, TileInfo right) pair, int placeRotation, int dropColumn) {
|
|
//Debug.LogFormat("Placing tile frame {0}",Time.frameCount);
|
|
(int x, int y) dropLeft = (dropColumn, dropHeight), dropRight = (dropColumn, dropHeight);
|
|
switch(placeRotation) {
|
|
case 0:
|
|
dropRight.x += 1;
|
|
break;
|
|
case 1:
|
|
dropRight.y += 1;
|
|
break;
|
|
case 2:
|
|
dropRight.x -= 1;
|
|
break;
|
|
case 3:
|
|
dropRight.y -= 1;
|
|
break;
|
|
}
|
|
// Fix an issue with placing tiles in the wrong order not working right
|
|
if (dropRight.y < dropLeft.y) {
|
|
// Texel - Switch to modifying the passed board state, instead of the authority board state
|
|
bs.SetTile(pair.right, dropRight.x, dropRight.y);
|
|
bs.SetTile(pair.left, dropLeft.x, dropLeft.y);
|
|
//this[dropRight.x, dropRight.y] = pair.right;
|
|
//this[dropLeft.x, dropLeft.y] = pair.left;
|
|
} else {
|
|
bs.SetTile(pair.left, dropLeft.x, dropLeft.y);
|
|
bs.SetTile(pair.right, dropRight.x, dropRight.y);
|
|
|
|
//this[dropLeft.x, dropLeft.y] = pair.left;
|
|
//this[dropRight.x, dropRight.y] = pair.right;
|
|
}
|
|
|
|
|
|
return bs;
|
|
}
|
|
|
|
public int ClampRotatedPosition(int sourceRotation) {
|
|
switch(sourceRotation) {
|
|
case 0:
|
|
return Mathf.Clamp(dropColumn, 0, COLUMN-2);
|
|
case 1:
|
|
case 3:
|
|
return Mathf.Clamp(dropColumn, 0, COLUMN-1);
|
|
case 2:
|
|
return Mathf.Clamp(dropColumn, 1, COLUMN-1);
|
|
default:
|
|
throw new System.IndexOutOfRangeException("Rotation is out of bounds you dolt");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region network
|
|
// Serialize/Deserialize network board state
|
|
public override void Deserialize(Hashtable h) {
|
|
base.Deserialize(h);
|
|
//Debug.Log("Deserializing");
|
|
|
|
if (h.TryGetValue('b', out var val)) {
|
|
board.FromHashtable((Hashtable)val);
|
|
//Debug.Log(val.ToString());
|
|
}
|
|
}
|
|
public override void Serialize(Hashtable h) {
|
|
base.Serialize(h);
|
|
//Debug.Log("Serializing");
|
|
|
|
h.Add('b', board.ToHashtable());
|
|
lastSentBoard = board.Copy();
|
|
stateDirty = false;
|
|
}
|
|
|
|
// Would be a lot better if we could get a good hash off of the board, but the data is too regular we'd collide constantly
|
|
void checkDirty() {
|
|
if (lastSentBoard != null && !board.Matches(lastSentBoard)) {
|
|
stateDirty = true;
|
|
return;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region trash
|
|
BoardState ReduceCountdowns(BoardState bs) {
|
|
for(int i = 0; i < bs.Count; ++i) {
|
|
var col = bs[i];
|
|
for(int y = 0; y < col.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.kind == TileKind.Trash){
|
|
tile.counter--;
|
|
if (tile.counter == 0){ // Revert tiles to blocks
|
|
tile.kind = TileKind.Block;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return bs;
|
|
}
|
|
|
|
// Funny how THIS one is super easy, but once trash is involved it's hard
|
|
int HighestStack(BoardState bs) => bs.Max(t => t.Count);
|
|
|
|
/// <summary>
|
|
/// Calculate the highest stack with trash.
|
|
/// </summary>
|
|
int HighestStackWithTrash(BoardState bs, int amount) {
|
|
var stackHeights = new List<int>();
|
|
for(int c = 0; c < bs.Count; ++c)
|
|
stackHeights.Add(bs[c].Count);
|
|
|
|
var cursorHead = trashCursorHead;
|
|
while (amount > 0) {
|
|
--amount;
|
|
stackHeights[cursorHead] += 1;
|
|
|
|
if (cursorHead >= COLUMN) {
|
|
cursorHead = 0;
|
|
}
|
|
}
|
|
|
|
return stackHeights.Max();
|
|
}
|
|
|
|
public int incomingTrash = 0;
|
|
|
|
void SendTrash(int amount) {
|
|
if (amount < 1) return;
|
|
// Uncached find? Blasphamy! Also whatever fukkit
|
|
var otherBoards = FindObjectsOfType<GameBoard>().Where(t => t != this);
|
|
foreach (var other in otherBoards) {
|
|
other.RaiseEvent('t', true, amount);
|
|
}
|
|
}
|
|
|
|
[NetEvent('t')]
|
|
void AddTrashNetwork(int i) {
|
|
incomingTrash += i;
|
|
}
|
|
|
|
// Column to drop trash
|
|
int trashCursorHead = 0;
|
|
void ApplyTrash() {
|
|
if (incomingTrash > 0) {
|
|
AudioProvider.Bunk();
|
|
delayState = DelayState.Collapse;
|
|
timeInState = 0;
|
|
}
|
|
while (incomingTrash > 0) {
|
|
--incomingTrash;
|
|
|
|
var trash = TileInfo.CreateRandomTrashTile();
|
|
board[trashCursorHead].Add(trash);
|
|
|
|
trashCursorHead += 1;
|
|
if (trashCursorHead >= COLUMN)
|
|
trashCursorHead = 0;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region activation
|
|
BoardState ClearActivation(BoardState bs) {
|
|
foreach(var col in bs) {
|
|
for(int y = 0; y < col.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.kind.Equals(TileKind.Activiting)){
|
|
TileInfo.SetAirTile(tile);
|
|
}
|
|
}
|
|
}
|
|
return bs;
|
|
}
|
|
|
|
// Activate all special tiles
|
|
BoardState SpecialActivation(BoardState bs, out bool didActivate) {
|
|
|
|
didActivate = false;
|
|
//return bs; // Test short-circuit
|
|
|
|
for (int x = 0; x < bs.Count; ++x) {
|
|
var col = bs[x];
|
|
for (int y = 0; y < col.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.kind == TileKind.Special) {
|
|
if (y > 0) {
|
|
// Get the color of the tile we are on top of
|
|
var under = col[y - 1];
|
|
tile.kind = TileKind.Activiting;
|
|
tile.color = under.color;
|
|
bs = ActivateAllColor(bs, under.color);
|
|
didActivate = true;
|
|
} else {
|
|
// Special tile is on the bottom, swap it to a random activating tile
|
|
tile = TileInfo.CreateRandomActivatorTile();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bs;
|
|
}
|
|
|
|
BoardState ActivateAllColor(BoardState bs,TileColor tc) {
|
|
for (int x = 0; x < bs.Count; ++x) {
|
|
var col = bs[x];
|
|
for (int y = 0; y < col.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.color == tc) {
|
|
tile.kind = TileKind.Activiting;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bs;
|
|
}
|
|
|
|
int CountActivations(BoardState bs) {
|
|
int total = 0;
|
|
foreach(var col in bs) {
|
|
total += col.Count(t => t.kind.Equals(TileKind.Activiting));
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
void GhostActivations() {
|
|
for(int x = 0; x < board.Count; ++x) {
|
|
var col = board[x];
|
|
for (int y = 0; y < col.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.kind.Equals(TileKind.Activiting)) {
|
|
render.Ghost(tile, (x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BoardState ActivateColorOnce(BoardState bs,TileColor tc,out bool success) {
|
|
success = false;
|
|
for(int x = 0; x < bs.Count; ++x) {
|
|
var col = bs[x];
|
|
for (int y = 0; y < col.Count; ++y) {
|
|
//var tile = this[x, y];
|
|
var tile = bs.tile(x, y);
|
|
if (tile.kind.Equals(TileKind.Activator) || tile.kind.Equals(TileKind.Activiting)) {
|
|
// Check adjacency for tiles to activate, filtering for unactivated blocks or other activators of our color
|
|
var neighbors = Neighbors(x, y,bs).Where(t => t.kind.Equals(TileKind.Block) || t.kind.Equals(TileKind.Activator)).Where(t => t.color.Equals(tile.color));
|
|
foreach(var neighbor in neighbors) {
|
|
success = true;
|
|
tile.kind = TileKind.Activiting;
|
|
neighbor.kind = TileKind.Activiting;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bs;
|
|
}
|
|
|
|
BoardState ActivateColorOnceSplit(BoardState bs, TileColor tc, out bool success) {
|
|
success = false;
|
|
var activable = new List<(int x, int y)>();
|
|
for (int x = 0; x < bs.Count; ++x) {
|
|
var col = bs[x];
|
|
for(int y = 0; y < col.Count; ++y) {
|
|
var tile = bs.tile(x, y);//this[x, y];
|
|
if (tile.color != tc) continue;
|
|
if (tile.kind.Equals(TileKind.Activator) || tile.kind.Equals(TileKind.Activiting))
|
|
activable.Add((x, y));
|
|
}
|
|
}
|
|
|
|
foreach(var potential in activable) {
|
|
var tile = bs.tile(potential.x, potential.y);//this[potential.x, potential.y];
|
|
if (tile.kind.Equals(TileKind.Activator) || tile.kind.Equals(TileKind.Activiting)) {
|
|
// Check adjacency for tiles to activate, filtering for unactivated blocks or other activators of our color
|
|
var neighbors = Neighbors(potential.x, potential.y,bs).Where(t => t.kind.Equals(TileKind.Block) || t.kind.Equals(TileKind.Activator)).Where(t => t.color.Equals(tile.color));
|
|
foreach (var neighbor in neighbors) {
|
|
success = true;
|
|
tile.kind = TileKind.Activiting;
|
|
neighbor.kind = TileKind.Activiting;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bs;
|
|
}
|
|
|
|
BoardState ActivateColor(BoardState bs, TileColor tc) {
|
|
bool didActivate;
|
|
do {
|
|
bs = ActivateColorOnce(bs, tc, out didActivate);
|
|
} while (didActivate);
|
|
|
|
return bs;
|
|
}
|
|
|
|
BoardState ActivateOnce(BoardState bs, out bool success) {
|
|
var colors = (TileColor[])System.Enum.GetValues(typeof(TileColor));
|
|
success = false;
|
|
foreach(var tc in colors) {
|
|
//bs = ActivateColorOnce(bs,tc, out bool colorSuccess);
|
|
bs = ActivateColorOnceSplit(bs, tc, out bool colorSuccess);
|
|
if (colorSuccess) success = true;
|
|
}
|
|
return bs;
|
|
}
|
|
|
|
BoardState Activate(BoardState bs) {
|
|
var colors = (TileColor[])System.Enum.GetValues(typeof(TileColor));
|
|
foreach(var tc in colors) {
|
|
bs = ActivateColor(bs, tc);
|
|
}
|
|
return bs;
|
|
}
|
|
#endregion
|
|
|
|
static BoardState Collapse(BoardState bs) {
|
|
for(int i = 0; i < bs.Count; ++i) {
|
|
bs[i] = Collapse(bs[i]);
|
|
}
|
|
return bs;
|
|
}
|
|
|
|
static List<TileInfo> Collapse(List<TileInfo> ti) {
|
|
return ti.Where(t => t.kind != TileKind.Air).ToList();
|
|
}
|
|
|
|
bool stateDirty = false;
|
|
|
|
[NetEvent('l')]
|
|
void SendLossState(){
|
|
delayState = DelayState.Loss;
|
|
StartCoroutine(HandleLoss(3f));
|
|
}
|
|
|
|
[Header("Network")]
|
|
public float networkTick = 0.1f;
|
|
private float nextNetworkTick;
|
|
|
|
public void Update() {
|
|
if (!active) {
|
|
render.RenderName();
|
|
return;
|
|
}
|
|
|
|
if (isMine) {
|
|
//checkDirty();
|
|
|
|
render.RebuildStack(board);
|
|
GameLogic();
|
|
|
|
render.SetAnimation();
|
|
|
|
if (Time.time >= nextNetworkTick && NetworkManager.inRoom){
|
|
UpdateNow();
|
|
nextNetworkTick = Time.time + networkTick;
|
|
}
|
|
|
|
}
|
|
|
|
render.RebuildStack(board);
|
|
render.Render(board);
|
|
// HACK So uh, there's an issue with air tiles taking two renders to be removed right, so uh, two renders it is. - Texel
|
|
// Seriously I can't believe this works this is dirty
|
|
|
|
// It does make it render twice as fast, I have idea
|
|
// i fix by doing two rebuild stacks >:)
|
|
// render.Render(board);
|
|
|
|
render.SetComboLevel(Combo);
|
|
render.SetScoreValue(score);
|
|
|
|
var pair = currentPair;
|
|
var pairp = GetPlacePosition(board, playerRotation, dropColumn);
|
|
render.RenderPlacement(pair.left, pair.right, pairp.left, pairp.right);
|
|
|
|
render.RenderName();
|
|
}
|
|
|
|
public override void Awake() {
|
|
base.Awake();
|
|
|
|
Register();
|
|
}
|
|
|
|
public void Setup(){
|
|
board = BoardStateExtension.Initialize();
|
|
|
|
StopAI();
|
|
// This is to reset the AI
|
|
|
|
// Build the list of possible placements the AI may use
|
|
PossiblePlacements = GetAllPossibilities();
|
|
|
|
lastActivatorColor = TileInfo.CreateRandomActivatorTile().color;
|
|
|
|
// Texel - Ooops we forgot to clear these
|
|
trashCursorHead = 0;
|
|
incomingTrash = 0;
|
|
score = 0;
|
|
|
|
render.NickNameText.text = ""; // Clear the nickname text to prevent multi->arcade corruption
|
|
|
|
nextActivator = TilesUntilActivator;
|
|
nextSpecial = ActivatorsUntilSpecial;
|
|
delayState = DelayState.None;
|
|
timeInState = 0;
|
|
|
|
ReplaceNextTile();
|
|
SwapTiles();
|
|
ReplaceNextTile();
|
|
|
|
if (isMine){
|
|
foreach(var c in board){
|
|
c.Add(TileInfo.CreateRandomBlockTile());
|
|
c.Add(TileInfo.CreateRandomBlockTile());
|
|
}
|
|
}
|
|
|
|
active = true;
|
|
}
|
|
|
|
public void Stop(){
|
|
StopAI();
|
|
active = false;
|
|
}
|
|
|
|
public void Clear(){
|
|
delayState = DelayState.None;
|
|
StartCoroutine(HandleLoss(0f));
|
|
}
|
|
|
|
public IEnumerable<TileInfo> Neighbors(int x, int y, BoardState bs) {
|
|
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);
|
|
|
|
/*var self = this[x, y];
|
|
var neighborsWithNull = new[] { this[x + 1, y], this[x - 1, y], this[x, y + 1], this[x, y - 1] };
|
|
return neighborsWithNull.Where(t => t != null).Where(t=>t!=self);*/
|
|
}
|
|
|
|
public TileInfo this[int x, int y] {
|
|
get {
|
|
// Pass null on sides/top, or anywhere where x, y doesn't exist.
|
|
if (x < 0 || x >= board.Count) return null;
|
|
if (y < 0) return null;
|
|
|
|
var col = board[x];
|
|
|
|
if (y < col.Count) return col[y];
|
|
return null;
|
|
}
|
|
set {
|
|
var col = board[x];
|
|
|
|
// Set value or append to end
|
|
if (y < col.Count) col[y] = value;
|
|
else col.Add(value); // Added else. Otherwises, any set would have grown board -- Ebony
|
|
}
|
|
}
|
|
|
|
#region AI
|
|
List<(int r, int c)> PossiblePlacements;
|
|
|
|
/// <summary>
|
|
/// Given input board state, return the end result for number of tiles that would be sent in an attack
|
|
/// This is destructive to the input board state
|
|
/// </summary>
|
|
int ProjectAttack(BoardState bs) {
|
|
int total = 0;
|
|
int comboLength = 0;
|
|
|
|
bool repeat = false;
|
|
do {
|
|
repeat = false;
|
|
bs = Collapse(bs);
|
|
bs = Activate(bs);
|
|
bs = SpecialActivation(bs, out bool didActivate);
|
|
|
|
var newActivations = CountActivations(bs);
|
|
if (newActivations > 0) {
|
|
comboLength += 1;
|
|
repeat = true;
|
|
|
|
if (newActivations > 3) total += (newActivations - 3) * comboLength;
|
|
bs = ClearActivation(bs);
|
|
}
|
|
} while (repeat);
|
|
|
|
return total;
|
|
}
|
|
|
|
// Generates actual subset of possible moves
|
|
List<(int simRotation, int simCol)> GetAllPossibilities() {
|
|
var Considerables = new List<(int r, int c)>();
|
|
for(int col = 0; col < COLUMN; ++col) {
|
|
Considerables.AddRange(new[] {
|
|
(0,col), (1,col), (2,col), (3,col)
|
|
});
|
|
}
|
|
|
|
for(int i = 0; i < Considerables.Count; ++i) {
|
|
Considerables[i] = ClampPossibility(Considerables[i]);
|
|
}
|
|
return Considerables.Distinct().ToList();
|
|
}
|
|
|
|
(int r, int c) ClampPossibility((int r, int c) possibility) {
|
|
int oldDrop = dropColumn;
|
|
dropColumn = possibility.c;
|
|
possibility.c = ClampRotatedPosition(possibility.r);
|
|
dropColumn = oldDrop;
|
|
|
|
return possibility;
|
|
}
|
|
|
|
float averageConnectedSize(BoardState bs) {
|
|
int maxRed =0, maxGreen=0, maxBlue=0, maxYellow=0;
|
|
for (int i = 0; i < bs.Count; ++i) {
|
|
(int count, TileColor color) cluster = ExposedClusterSize(bs, i);
|
|
switch(cluster.color) {
|
|
case TileColor.Blue:
|
|
maxBlue = Mathf.Max(maxBlue, cluster.count);
|
|
break;
|
|
case TileColor.Green:
|
|
maxGreen = Mathf.Max(maxGreen, cluster.count);
|
|
break;
|
|
case TileColor.Red:
|
|
maxRed = Mathf.Max(maxRed, cluster.count);
|
|
break;
|
|
case TileColor.Yellow:
|
|
maxYellow = Mathf.Max(maxYellow, cluster.count);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (maxRed + maxGreen + maxBlue + maxYellow) * 0.25f;
|
|
}
|
|
|
|
// Texel - I gave up writing this and made it use a board copy destructively
|
|
(int count, TileColor color) ExposedClusterSize(BoardState bs, int col) {
|
|
var bc = bs.Copy();
|
|
//ReduceToBlocksOnly(bc);
|
|
|
|
int startX = col;
|
|
int startY = bc[col].Count - 1;
|
|
if (startY < 0) return (0, TileColor.Blue); // Oops can't eval an empty col
|
|
|
|
var tile = bc.tile(startX, startY);
|
|
|
|
tile.kind = TileKind.Activator;
|
|
//ProjectAttack(bc);
|
|
bc = Activate(bc);
|
|
|
|
return (CountActivations(bc),tile.color);
|
|
}
|
|
|
|
void ReduceToBlocksOnly(BoardState bs) {
|
|
for(int x = 0; x < bs.Count; ++x) {
|
|
var col = bs[x];
|
|
for(int y = 0; y < bs.Count; ++y) {
|
|
var tile = col[y];
|
|
if (tile.kind.Equals(TileKind.Air)) continue;
|
|
tile.kind = TileKind.Block;
|
|
tile.counter = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an internal copy of the board, simulate the given rotation/col against it.
|
|
/// </summary>
|
|
(int score,float averageConnectedSize, int maxHeight) EvalPossibility(BoardState bs, (int r, int c) possibility) {
|
|
var bc = bs.Copy();
|
|
try {
|
|
bc = Place(bc, currentPair, possibility.r, possibility.c);
|
|
|
|
// TODO - Highest connected size
|
|
float connectness = averageConnectedSize(bc);
|
|
int trashValue = ProjectAttack(bc); // Forward simulate the board to it's next stable position
|
|
int tallest = HighestStack(bc);
|
|
|
|
return (trashValue, connectness, tallest);
|
|
} catch (System.Exception e) {
|
|
Debug.LogFormat("Possibility ({0}, {1}) has encountered an exception", possibility.r, possibility.c);
|
|
throw (e);
|
|
}
|
|
}
|
|
// Methodology -> Place pieces to fire the longest combo, then place pieces to minimize our tallest stack.
|
|
(int r, int c) GetBestMove(BoardState bs) {
|
|
return PossiblePlacements
|
|
.Select(possiblity => (possiblity, EvalPossibility(bs, possiblity)))
|
|
.OrderByDescending(t => t.Item2.score) // First priority, attacking
|
|
.ThenByDescending(t => t.Item2.maxHeight < 10 ? 1 : 0) // Second priority, not offing ourselves
|
|
.ThenByDescending(t => t.Item2.averageConnectedSize) // Third priority, maximize cluster sizes
|
|
.ThenBy(t => t.Item2.maxHeight) // Fourth priority, keeping our height down
|
|
.Select(t => t.possiblity).First();
|
|
}
|
|
|
|
[ContextMenu("Startup AI")]
|
|
public void StartAIDefault() {
|
|
StartAI();
|
|
}
|
|
|
|
Coroutine aiThinkCoroutine;
|
|
|
|
public void StartAI(float thinkTime = 0.5f) {
|
|
AIEnabled = true;
|
|
AIMoveTime = thinkTime;
|
|
aiThinkCoroutine = StartCoroutine(AIThink());
|
|
}
|
|
|
|
public void StopAI(){
|
|
AIEnabled = false;
|
|
if (aiThinkCoroutine != null) StopCoroutine(aiThinkCoroutine);
|
|
aiThinkCoroutine = null;
|
|
}
|
|
|
|
public bool AIEnabled = false;
|
|
public float AIMoveTime = 0.5f;
|
|
IEnumerator AIThink() {
|
|
yield return null; // Wait a frame before trying any shit
|
|
|
|
int totalMoves = 0;
|
|
while (delayState != DelayState.Loss) {
|
|
if (!AIEnabled)
|
|
yield return new WaitUntil(() => AIEnabled);
|
|
var (r, c) = GetBestMove(board);
|
|
//Debug.LogFormat("AI: Column {0}, Rotation {1}", c, r);
|
|
totalMoves += 1;
|
|
|
|
// First match rotation
|
|
while (playerRotation != r) {
|
|
yield return new WaitForSeconds(AIMoveTime);
|
|
if (playerRotation < r) ++playerRotation;
|
|
if (playerRotation > r) --playerRotation;
|
|
|
|
dropColumn = ClampRotatedPosition(playerRotation);
|
|
}
|
|
|
|
// Then move to the right column
|
|
while (dropColumn != c) {
|
|
yield return new WaitForSeconds(AIMoveTime);
|
|
if (dropColumn > c) --dropColumn;
|
|
if (dropColumn < c) ++dropColumn;
|
|
}
|
|
|
|
// Wait until we're allowed to drop a piece
|
|
yield return new WaitUntil(() => delayState.Equals(DelayState.None));
|
|
board = DropNow(board);
|
|
// Now wait for it all to settle before repeating
|
|
yield return new WaitUntil(() => delayState.Equals(DelayState.None));
|
|
}
|
|
|
|
Debug.LogFormat("AI lost after {0} moves",totalMoves);
|
|
}
|
|
#endregion
|
|
}
|