298 lines
8.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
public event System.Action OnChipDropped;
private int SelectedColumn => availableColumns[selectedColumnIndex];
public bool ControlsEnabled { get; set; } = true;
#region Public Methods
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;
}
}
#endregion
#region State Updates
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();
Debug.Log("Chip dropped");
}
}
#endregion
#region Bot Logic
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);
board.DropChip(currentChip, SelectedColumn);
SetState(PlayerState.Dropping);
}
#endregion
#region Random Movement
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);
}
}
}
#endregion
#region Helpers
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);
}
#endregion
#region Nested Types
private enum PlayerState
{
Sleep,
Preparing,
Thinking,
Moving,
Dropping
}
#endregion
}