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 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 } }