281 lines
8.4 KiB
C#
281 lines
8.4 KiB
C#
using UnityEngine;
|
||
using System.Threading;
|
||
|
||
public class Bot : BasePlayer, IGamePlayer
|
||
{
|
||
[Header("Movement Settings")]
|
||
[SerializeField] private float prepareSpeed;
|
||
[SerializeField] private float moveSpeed;
|
||
|
||
[Header("Bot Settings")]
|
||
[SerializeField] private DifficultyPreset difficultyPreset;
|
||
[SerializeField] private bool useDummyBots = false;
|
||
[SerializeField] private float minThinkingTime = 1f;
|
||
|
||
// State
|
||
private PlayerState state = PlayerState.Sleep;
|
||
private Chip currentChip;
|
||
private int[] availableColumns;
|
||
private int selectedColumnIndex;
|
||
|
||
// Movement targets
|
||
private Vector2 stickedTargetPosition;
|
||
private Vector2 dropTargetPosition;
|
||
|
||
// Thinking
|
||
private Thread calculationThread;
|
||
private int calculatedColumn = -1;
|
||
private float thinkingStartTime;
|
||
private bool isThinkingComplete = false;
|
||
|
||
// Random movement
|
||
private float randomMoveTimer;
|
||
private float randomMoveInterval = 0.3f;
|
||
|
||
private int droppedRow, droppedColumn;
|
||
|
||
public event System.Action<int, int> OnChipDropped;
|
||
|
||
private int SelectedColumn => availableColumns[selectedColumnIndex];
|
||
public bool ControlsEnabled { get; set; } = true;
|
||
|
||
public void PrepareChip()
|
||
{
|
||
SetState(PlayerState.Preparing);
|
||
CacheAvailableColumns();
|
||
CreateChip();
|
||
SetInitialChipPosition();
|
||
}
|
||
|
||
public void Sleep()
|
||
{
|
||
SetState(PlayerState.Sleep);
|
||
currentChip = null;
|
||
availableColumns = null;
|
||
StopThinking();
|
||
}
|
||
|
||
public void UpdateControls()
|
||
{
|
||
switch (state)
|
||
{
|
||
case PlayerState.Preparing:
|
||
UpdatePreparing();
|
||
break;
|
||
case PlayerState.Thinking:
|
||
UpdateThinking();
|
||
break;
|
||
case PlayerState.Moving:
|
||
UpdateMoving();
|
||
break;
|
||
case PlayerState.Dropping:
|
||
UpdateDropping();
|
||
break;
|
||
case PlayerState.Sleep:
|
||
break;
|
||
}
|
||
}
|
||
|
||
|
||
private void UpdatePreparing()
|
||
{
|
||
// Движение сверху вниз к stickedTargetPosition (без оффсета)
|
||
if (currentChip.MoveToLinear(stickedTargetPosition, prepareSpeed))
|
||
{
|
||
StartThinking();
|
||
}
|
||
}
|
||
|
||
private void UpdateThinking()
|
||
{
|
||
// Случайные движения (инициируют переход в Moving)
|
||
TryRandomMovement();
|
||
|
||
// Проверяем, завершены ли вычисления и прошло ли минимальное время
|
||
if (isThinkingComplete && Time.time - thinkingStartTime >= minThinkingTime)
|
||
{
|
||
MoveToCalculatedColumn();
|
||
}
|
||
}
|
||
|
||
private void UpdateMoving()
|
||
{
|
||
// Плавное горизонтальное движение к целевой позиции (Y сохраняется)
|
||
if (currentChip.MoveToLinear(stickedTargetPosition, moveSpeed))
|
||
{
|
||
// Если условия для дропа выполнены - бросаем
|
||
if (isThinkingComplete && Time.time - thinkingStartTime >= minThinkingTime)
|
||
{
|
||
PerformDrop();
|
||
}
|
||
else
|
||
{
|
||
// Иначе возвращаемся в режим раздумий
|
||
SetState(PlayerState.Thinking);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateDropping()
|
||
{
|
||
if (currentChip.MoveToAccelerated(dropTargetPosition))
|
||
{
|
||
SetState(PlayerState.Sleep);
|
||
OnChipDropped?.Invoke(droppedColumn, droppedRow);
|
||
}
|
||
}
|
||
|
||
private void StartThinking()
|
||
{
|
||
SetState(PlayerState.Thinking);
|
||
isThinkingComplete = false;
|
||
calculatedColumn = -1;
|
||
thinkingStartTime = Time.time;
|
||
randomMoveTimer = 0f;
|
||
|
||
StopThinking();
|
||
calculationThread = new Thread(CalculateBestMove);
|
||
calculationThread.Start();
|
||
}
|
||
|
||
private void StopThinking()
|
||
{
|
||
if (calculationThread != null && calculationThread.IsAlive)
|
||
{
|
||
calculationThread.Abort();
|
||
calculationThread = null;
|
||
}
|
||
}
|
||
|
||
private void CalculateBestMove()
|
||
{
|
||
try
|
||
{
|
||
DifficultyPreset currentPreset = difficultyPreset;
|
||
if (useDummyBots && DifficultyManager.Instance != null)
|
||
currentPreset = DifficultyManager.Instance.DummyPreset;
|
||
|
||
if (currentPreset != null && board != null)
|
||
{
|
||
int movesMade = board.MovesCount;
|
||
currentPreset = currentPreset.GetPreset(movesMade);
|
||
Solver.SetPreset(currentPreset);
|
||
}
|
||
|
||
GameManager gm = GameManager.instance;
|
||
if (gm != null)
|
||
{
|
||
bool onBossByScore = gm.vsBotScore == gm.botsAmount - 1;
|
||
bool isHardCommonDifficulty = SettingsManager.GetString(SettingKey.DIFFICULTY) == "Сложная";
|
||
bool isUpperCashDifficulty = SettingsManager.GetString(SettingKey.DIFFICULTY_MODE) == "Заработок";
|
||
bool needSuperPreset = onBossByScore && (isHardCommonDifficulty || isUpperCashDifficulty);
|
||
|
||
if (needSuperPreset)
|
||
calculatedColumn = SolverV2.FindBestMove(board.GridToTeam(), team);
|
||
else
|
||
calculatedColumn = Solver.FindBestMove(board.GridToTeam(), team);
|
||
}
|
||
else
|
||
{
|
||
calculatedColumn = Solver.FindBestMove(board.GridToTeam(), team);
|
||
}
|
||
|
||
isThinkingComplete = true;
|
||
}
|
||
catch (ThreadAbortException) { }
|
||
catch (System.Exception e)
|
||
{
|
||
Debug.LogError($"Bot calculation error: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private void MoveToCalculatedColumn()
|
||
{
|
||
if (calculatedColumn == -1) return;
|
||
|
||
int targetIndex = System.Array.IndexOf(availableColumns, calculatedColumn);
|
||
if (targetIndex == -1)
|
||
{
|
||
Debug.LogWarning($"Column {calculatedColumn} is not available!");
|
||
return;
|
||
}
|
||
|
||
if (selectedColumnIndex == targetIndex)
|
||
{
|
||
|
||
PerformDrop();
|
||
return;
|
||
}
|
||
|
||
selectedColumnIndex = targetIndex;
|
||
UpdateTargetPositionX(board.GetStickedPosition(SelectedColumn).x);
|
||
SetState(PlayerState.Moving);
|
||
}
|
||
|
||
private void PerformDrop()
|
||
{
|
||
dropTargetPosition = board.GetTargetPosition(SelectedColumn);
|
||
|
||
droppedColumn = SelectedColumn;
|
||
droppedRow = board.GetLowestEmptyRow(SelectedColumn);
|
||
|
||
board.DropChip(currentChip, SelectedColumn);
|
||
SetState(PlayerState.Dropping);
|
||
}
|
||
|
||
private void TryRandomMovement()
|
||
{
|
||
if (availableColumns == null || availableColumns.Length <= 1) return;
|
||
|
||
randomMoveTimer += Time.deltaTime;
|
||
if (randomMoveTimer >= randomMoveInterval)
|
||
{
|
||
randomMoveTimer = 0f;
|
||
int direction = Random.Range(0, 2) * 2 - 1;
|
||
int newIndex = Mathf.Clamp(selectedColumnIndex + direction, 0, availableColumns.Length - 1);
|
||
|
||
if (newIndex != selectedColumnIndex)
|
||
{
|
||
selectedColumnIndex = newIndex;
|
||
// Сохраняем текущий Y, меняем только X
|
||
UpdateTargetPositionX(board.GetStickedPosition(SelectedColumn).x);
|
||
SetState(PlayerState.Moving);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CacheAvailableColumns()
|
||
{
|
||
availableColumns = board.GetNonFullColumns();
|
||
selectedColumnIndex = availableColumns.Length / 2;
|
||
}
|
||
|
||
private void CreateChip()
|
||
{
|
||
currentChip = ChipPool.Instance.GetConfigured(chipSprite, team);
|
||
}
|
||
|
||
private void SetInitialChipPosition()
|
||
{
|
||
// Начальная позиция – сверху с оффсетом
|
||
stickedTargetPosition = board.GetMiddleStickedPosition();
|
||
currentChip.transform.position = stickedTargetPosition + Vector2.up * 5f;
|
||
}
|
||
|
||
private void SetState(PlayerState newState) => state = newState;
|
||
|
||
// Обновляет только X целевой позиции, Y остаётся неизменным
|
||
private void UpdateTargetPositionX(float newX)
|
||
{
|
||
stickedTargetPosition = new Vector2(newX, stickedTargetPosition.y);
|
||
}
|
||
|
||
private enum PlayerState
|
||
{
|
||
Sleep,
|
||
Preparing,
|
||
Thinking,
|
||
Moving,
|
||
Dropping
|
||
}
|
||
} |