TouhouLS/Assets/RealCode/GameBoard.cs

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
}