Добавить таймеры и шкалы прогресса, расширить призовую систему и контроль остатков, восстановить настройки и обновить таблицу призов главного меню.
386 lines
12 KiB
C#
386 lines
12 KiB
C#
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;
|
||
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.GetMiddleStickedPosition();
|
||
stickedTargetPosition.x = board.GetStickedPosition(SelectedColumn).x;
|
||
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
|
||
}
|
||
}
|