386 lines
12 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;
using CharacterSystem;
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;
[SerializeField, Min(0f)] private float _candidatePauseDuration = 0.35f;
public DifficultyPreset dummyPreset;
// 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 float _requiredThinkingTime;
private bool isThinkingComplete = false;
private DifficultyPreset _activeDifficultyPreset;
private bool _useStrongSolver;
private int[] _thinkingCandidates;
private int _thinkingCandidateIndex;
private float _candidatePauseUntil;
private bool _isVisitingCandidates;
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 CancelTurn()
{
StopThinking();
if (currentChip != null)
{
if (state == PlayerState.Dropping)
currentChip.transform.position = dropTargetPosition;
else
ChipPool.Instance.Release(currentChip);
}
currentChip = null;
availableColumns = null;
SetState(PlayerState.Sleep);
}
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()
{
if (!isThinkingComplete)
return;
if (!_isVisitingCandidates)
{
StartCandidateVisit();
return;
}
if (Time.time < _candidatePauseUntil)
return;
if (_thinkingCandidateIndex < _thinkingCandidates.Length - 1)
{
_thinkingCandidateIndex++;
MoveToCandidate(_thinkingCandidates[_thinkingCandidateIndex]);
return;
}
if (Time.time - thinkingStartTime >= _requiredThinkingTime)
PerformDrop();
}
private void UpdateMoving()
{
if (currentChip.MoveToLinear(stickedTargetPosition, moveSpeed))
{
SetState(PlayerState.Thinking);
_candidatePauseUntil = Time.time + _candidatePauseDuration;
}
}
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;
_thinkingCandidates = null;
_thinkingCandidateIndex = 0;
_candidatePauseUntil = 0f;
_isVisitingCandidates = false;
CacheDifficulty();
StopThinking();
calculationThread = new Thread(CalculateBestMove);
calculationThread.Start();
}
private void StopThinking()
{
if (calculationThread != null && calculationThread.IsAlive)
{
calculationThread.Abort();
calculationThread = null;
}
}
private void CalculateBestMove()
{
try
{
Solver.SetPreset(_activeDifficultyPreset);
if (_useStrongSolver)
calculatedColumn = SolverV2.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 StartCandidateVisit()
{
_thinkingCandidates = BuildThinkingCandidates();
_thinkingCandidateIndex = 0;
_isVisitingCandidates = true;
MoveToCandidate(_thinkingCandidates[_thinkingCandidateIndex]);
}
private void MoveToCandidate(int column)
{
int targetIndex = System.Array.IndexOf(availableColumns, column);
if (targetIndex == -1)
{
Debug.LogWarning($"Column {column} is not available!");
return;
}
if (selectedColumnIndex == targetIndex)
{
_candidatePauseUntil = Time.time + _candidatePauseDuration;
return;
}
selectedColumnIndex = targetIndex;
SoundManager.instance?.PlayColumnSelectionSound();
UpdateTargetPositionX(board.GetStickedPosition(SelectedColumn).x);
SetState(PlayerState.Moving);
}
private int[] BuildThinkingCandidates()
{
int alternativeColumn = FindAlternativeCandidate(calculatedColumn);
return alternativeColumn >= 0
? new[] { alternativeColumn, calculatedColumn }
: new[] { calculatedColumn };
}
private int FindAlternativeCandidate(int finalColumn)
{
Team[,] grid = board.GridToTeam();
Team opponent = team == Team.Player1 ? Team.Player2 : Team.Player1;
int bestColumn = -1;
float bestScore = float.NegativeInfinity;
float boardCenter = (board.Columns - 1) * 0.5f;
foreach (int column in availableColumns)
{
if (column == finalColumn)
continue;
int row = board.GetLowestEmptyRow(column);
int attackScore = EvaluatePlacement(grid, row, column, team);
int blockScore = EvaluatePlacement(grid, row, column, opponent);
float centerPenalty = Mathf.Abs(column - boardCenter);
float score = attackScore * 2f + blockScore * 1.5f - centerPenalty;
if (score > bestScore)
{
bestScore = score;
bestColumn = column;
}
}
return bestColumn;
}
private int EvaluatePlacement(Team[,] grid, int row, int column, Team evaluatedTeam)
{
grid[row, column] = evaluatedTeam;
int longestLine = Mathf.Max(
GetLineLength(grid, row, column, evaluatedTeam, 1, 0),
GetLineLength(grid, row, column, evaluatedTeam, 0, 1),
GetLineLength(grid, row, column, evaluatedTeam, 1, 1),
GetLineLength(grid, row, column, evaluatedTeam, 1, -1));
grid[row, column] = Team.None;
return longestLine >= 4 ? 100 + longestLine : longestLine * longestLine;
}
private int GetLineLength(Team[,] grid, int row, int column, Team evaluatedTeam, int rowStep, int columnStep)
{
return 1
+ CountDirection(grid, row, column, evaluatedTeam, rowStep, columnStep)
+ CountDirection(grid, row, column, evaluatedTeam, -rowStep, -columnStep);
}
private int CountDirection(Team[,] grid, int row, int column, Team evaluatedTeam, int rowStep, int columnStep)
{
int count = 0;
int currentRow = row + rowStep;
int currentColumn = column + columnStep;
while (currentRow >= 0 && currentRow < grid.GetLength(0) &&
currentColumn >= 0 && currentColumn < grid.GetLength(1) &&
grid[currentRow, currentColumn] == evaluatedTeam)
{
count++;
currentRow += rowStep;
currentColumn += columnStep;
}
return count;
}
private void PerformDrop()
{
dropTargetPosition = board.GetTargetPosition(SelectedColumn);
droppedColumn = SelectedColumn;
droppedRow = board.GetLowestEmptyRow(SelectedColumn);
board.DropChip(currentChip, SelectedColumn);
SetState(PlayerState.Dropping);
}
private void CacheAvailableColumns()
{
availableColumns = board.GetNonFullColumns();
selectedColumnIndex = availableColumns.Length / 2;
}
private void CreateChip()
{
if (CharactersController.HasActiveCharacter && CharactersController.CurrentCharacter.visualModule.chipImage != null)
chipSprite = CharactersController.CurrentCharacter.visualModule.chipImage;
currentChip = ChipPool.Instance.GetConfigured(chipSprite, team);
}
private void CacheDifficulty()
{
DifficultyPreset currentPreset = difficultyPreset;
_requiredThinkingTime = minThinkingTime;
if (useDummyBots)
{
currentPreset = dummyPreset;
}
else if (GameManager.Instance != null &&
GameManager.Instance.gameMode == GameMode.pve &&
CharactersController.HasActiveCharacter)
{
DifficultyModule difficultyModule = CharactersController.CurrentCharacter.difficultyModule;
currentPreset = difficultyModule.GetCurrentPreset();
_requiredThinkingTime = difficultyModule.ThinkTime;
}
_activeDifficultyPreset = currentPreset != null && board != null
? currentPreset.GetPreset(board.MovesCount)
: currentPreset;
bool isLastCharacter = CharactersController.HasActiveCharacter &&
CharactersController.CharacterIndex == CharactersController.CharactersAmount - 1;
bool isHardDifficulty = SettingsManager.GetString(SettingKey.DIFFICULTY) == "Сложная";
bool isEarningMode = SettingsManager.GetString(SettingKey.DIFFICULTY_MODE) == "Заработок";
_useStrongSolver = isLastCharacter && (isHardDifficulty || isEarningMode);
}
private void SetInitialChipPosition()
{
// Начальная позиция – сверху с оффсетом
stickedTargetPosition = board.GetStickedPosition(SelectedColumn);
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
}
}