Оптимизация проверок поля

This commit is contained in:
Сергей Бондарь 2026-08-05 10:49:38 +03:00
parent b2ee79a408
commit ffa123e8c1
8 changed files with 181 additions and 268 deletions

View File

@ -6,7 +6,9 @@ public static class StateRouter
private static GameManager gm => GameManager.instance;
public static void RouteAfterTurn()
{
var checkResult = FieldChecker.Check(GameManager.instance.currentPlayer);
//var checkResult = FieldChecker.Check(GameManager.instance.currentPlayer);
CheckResult checkResult = null;
gm.lastResult = checkResult;//это неправильно. Нужно при проверке записывать в поле.
TurnEndingResult turnResult = EndOfTurnHandler.Evaluate(checkResult, gm.gameMode, gm.currentPlayer);

View File

@ -83,14 +83,13 @@ public class Board : MonoBehaviour
public Team[,] GridToTeam()
{
Team[,] res = new Team[Columns, Rows];
for (int col = 0; col < Columns; col++)
Team[,] res = new Team[Rows, Columns];
for (int row = 0; row < Rows; row++)
{
for (int row = 0; row < Rows; row++)
for (int col = 0; col < Columns; col++)
{
var chip = Grid[col, row];
if (chip == null) res[col, row] = Team.None;
else res[col, row] = chip.team;
res[row, col] = chip == null ? Team.None : chip.team;
}
}
return res;

View File

@ -32,13 +32,13 @@ public class Bot : BasePlayer, IGamePlayer
private float randomMoveTimer;
private float randomMoveInterval = 0.3f;
public event System.Action OnChipDropped;
private int droppedRow, droppedColumn;
public event System.Action<int, int> OnChipDropped;
private int SelectedColumn => availableColumns[selectedColumnIndex];
public bool ControlsEnabled { get; set; } = true;
#region Public Methods
public void PrepareChip()
{
SetState(PlayerState.Preparing);
@ -76,9 +76,6 @@ public class Bot : BasePlayer, IGamePlayer
}
}
#endregion
#region State Updates
private void UpdatePreparing()
{
@ -124,15 +121,10 @@ public class Bot : BasePlayer, IGamePlayer
if (currentChip.MoveToAccelerated(dropTargetPosition))
{
SetState(PlayerState.Sleep);
OnChipDropped?.Invoke();
Debug.Log("Chip dropped");
OnChipDropped?.Invoke(droppedColumn, droppedRow);
}
}
#endregion
#region Bot Logic
private void StartThinking()
{
SetState(PlayerState.Thinking);
@ -210,6 +202,7 @@ public class Bot : BasePlayer, IGamePlayer
if (selectedColumnIndex == targetIndex)
{
PerformDrop();
return;
}
@ -222,14 +215,14 @@ public class Bot : BasePlayer, IGamePlayer
private void PerformDrop()
{
dropTargetPosition = board.GetTargetPosition(SelectedColumn);
droppedColumn = SelectedColumn;
droppedRow = board.GetLowestEmptyRow(SelectedColumn);
board.DropChip(currentChip, SelectedColumn);
SetState(PlayerState.Dropping);
}
#endregion
#region Random Movement
private void TryRandomMovement()
{
if (availableColumns == null || availableColumns.Length <= 1) return;
@ -251,10 +244,6 @@ public class Bot : BasePlayer, IGamePlayer
}
}
#endregion
#region Helpers
private void CacheAvailableColumns()
{
availableColumns = board.GetNonFullColumns();
@ -281,10 +270,6 @@ public class Bot : BasePlayer, IGamePlayer
stickedTargetPosition = new Vector2(newX, stickedTargetPosition.y);
}
#endregion
#region Nested Types
private enum PlayerState
{
Sleep,
@ -293,6 +278,4 @@ public class Bot : BasePlayer, IGamePlayer
Moving,
Dropping
}
#endregion
}

View File

@ -2,10 +2,11 @@
public enum Team
{
None,
None,
Player1,
Player2
}
public static class FieldChecker
{
public static Chip[,] FieldArray => Board.Instance.Grid;
@ -14,215 +15,170 @@ public static class FieldChecker
public static int TargetLineLength = 4;
public static CheckResult lastResult;
public static CheckResult Check(Team lastPlayer)
/// <summary>
/// Проверяет, привела ли последняя фишка (установленная командой lastPlayer в клетку (row, col)) к победе.
/// </summary>
/// <param name="lastPlayer">Команда, которая только что сделала ход.</param>
/// <param name="row">Строка (индекс по вертикали).</param>
/// <param name="col">Столбец (индекс по горизонтали).</param>
/// <returns>Результат проверки (окончание игры, ничья, множество победных клеток).</returns>
public static CheckResult Check(Team lastPlayer, int row, int col)
{
HashSet<(int row, int col)> WinningIndexes = new();
var winningCells = new HashSet<(int row, int col)>();
var rowsResult = checkAllRows(lastPlayer);
var colsResult = checkAllColumns(lastPlayer);
var diagonalsResult = checkAllDiagonals(lastPlayer);
// Проверяем четыре направления, проходящие через (row, col)
CheckLine(row, col, 0, 1, lastPlayer, winningCells); // горизонталь
CheckLine(row, col, 1, 0, lastPlayer, winningCells); // вертикаль
CheckLine(row, col, 1, 1, lastPlayer, winningCells); // главная диагональ
CheckLine(row, col, 1, -1, lastPlayer, winningCells); // побочная диагональ
bool hasWin = rowsResult.hasWin ||
colsResult.hasWin ||
diagonalsResult.hasWin;
bool hasWin = winningCells.Count > 0;
if (hasWin)
{
WinningIndexes.UnionWith(rowsResult.winningIndexes);
WinningIndexes.UnionWith(colsResult.winningIndexes);
WinningIndexes.UnionWith(diagonalsResult.winningIndexes);
lastResult = new CheckResult(end: true,
isDraw: false,
WinningIndexes);
lastResult = new CheckResult(end: true, isDraw: false, winningCells);
return lastResult;
}
else if (checkDraw())
if (CheckDraw())
{
lastResult = new CheckResult(end: true,
isDraw: true,
WinningIndexes);
return lastResult;
}
else
{
lastResult = new CheckResult(end: false,
isDraw: false,
WinningIndexes);
lastResult = new CheckResult(end: true, isDraw: true, winningCells);
return lastResult;
}
lastResult = new CheckResult(end: false, isDraw: false, winningCells);
return lastResult;
}
private static (bool hasWin, HashSet<(int row, int col)> winningIndexes) checkAllRows(Team targetTeam)
/// <summary>
/// Проверяет линию, заданную направлением (rowStep, colStep), проходящую через клетку (row, col).
/// Если найдена непрерывная последовательность из фишек lastPlayer длиной >= TargetLineLength,
/// все клетки этой последовательности добавляются в winningCells.
/// </summary>
private static void CheckLine(int row, int col, int rowStep, int colStep, Team team, HashSet<(int, int)> winningCells)
{
var winningCells = new HashSet<(int row, int col)>();
for (int row = 0; row < Height; row++)
// Движение назад (против направления)
int r = row, c = col;
int backCount = 0;
while (true)
{
int start = -1;
for (int col = 0; col < Width; col++)
{
var chip = FieldArray[col, row];
bool isMatch = chip != null && chip.team == targetTeam;
if (isMatch && start == -1)
start = col;
if (!isMatch && start != -1)
{
int length = col - start;
if (length >= TargetLineLength)
{
for (int c = start; c < col; c++)
winningCells.Add((row, c));
}
start = -1;
}
}
// Проверка последовательности в конце строки
if (start != -1)
{
int length = Width - start;
if (length >= TargetLineLength)
{
for (int c = start; c < Width; c++)
winningCells.Add((row, c));
}
}
int prevR = r - rowStep;
int prevC = c - colStep;
if (prevR < 0 || prevR >= Height || prevC < 0 || prevC >= Width)
break;
var chip = FieldArray[prevC, prevR]; // массив индексируется [x=col, y=row]
if (chip == null || chip.team != team)
break;
backCount++;
r = prevR;
c = prevC;
}
int startR = r, startC = c;
return (winningCells.Count > 0, winningCells);
}
private static (bool hasWin, HashSet<(int row, int col)> winningIndexes) checkAllColumns(Team targetTeam)
{
var winningCells = new HashSet<(int row, int col)>();
for (int col = 0; col < Width; col++)
// Движение вперёд (по направлению)
r = row;
c = col;
int forwardCount = 0;
while (true)
{
int start = -1;
int nextR = r + rowStep;
int nextC = c + colStep;
if (nextR < 0 || nextR >= Height || nextC < 0 || nextC >= Width)
break;
var chip = FieldArray[nextC, nextR];
if (chip == null || chip.team != team)
break;
forwardCount++;
r = nextR;
c = nextC;
}
int endR = r, endC = c;
for (int row = 0; row < Height; row++)
int totalLength = backCount + 1 + forwardCount; // +1 за саму клетку
if (totalLength >= TargetLineLength)
{
// Добавляем все клетки от (startR, startC) до (endR, endC) с шагом (rowStep, colStep)
for (int i = 0; i < totalLength; i++)
{
var chip = FieldArray[col, row];
bool isMatch = chip != null && chip.team == targetTeam;
if (isMatch && start == -1)
start = row;
if (!isMatch && start != -1)
{
int length = row - start;
if (length >= TargetLineLength)
{
for (int r = start; r < row; r++)
winningCells.Add((r, col));
}
start = -1;
}
}
// Проверка последовательности в конце столбца
if (start != -1)
{
int length = Height - start;
if (length >= TargetLineLength)
{
for (int r = start; r < Height; r++)
winningCells.Add((r, col));
}
}
}
return (winningCells.Count > 0, winningCells);
}
private static (bool hasWin, HashSet<(int row, int col)> winningIndexes) checkAllDiagonals(Team targetCellState)
{
var winningCells = new HashSet<(int row, int col)>();
// Проверка главных диагоналей
for (int row = 0; row < Height; row++)
{
CheckDiagonalStartingAt(row, 0, 1, 1, targetCellState, winningCells);
}
for (int col = 1; col < Width; col++)
{
CheckDiagonalStartingAt(0, col, 1, 1, targetCellState, winningCells);
}
// Проверка побочных диагоналей
for (int row = 0; row < Height; row++)
{
CheckDiagonalStartingAt(row, Width - 1, 1, -1, targetCellState, winningCells);
}
for (int col = Width - 2; col >= 0; col--)
{
CheckDiagonalStartingAt(0, col, 1, -1, targetCellState, winningCells);
}
return (winningCells.Count > 0, winningCells);
}
private static void CheckDiagonalStartingAt(int startRow, int startCol, int rowStep, int colStep,
Team targetCellState, HashSet<(int, int)> winningCells)
{
int rows = Height;
int cols = Width;
int currentRow = startRow;
int currentCol = startCol;
int sequenceLength = 0;
int sequenceStartRow = startRow;
int sequenceStartCol = startCol;
while (currentRow >= 0 && currentRow < rows &&
currentCol >= 0 && currentCol < cols)
{
var chip = FieldArray[currentCol, currentRow];
bool isMatch = chip != null && chip.team == targetCellState;
if (isMatch)
{
sequenceLength++;
}
else
{
if (sequenceLength >= TargetLineLength)
{
for (int i = 0; i < sequenceLength; i++)
{
int r = sequenceStartRow + i * rowStep;
int c = sequenceStartCol + i * colStep;
winningCells.Add((r, c));
}
}
sequenceLength = 0;
sequenceStartRow = currentRow + rowStep;
sequenceStartCol = currentCol + colStep;
}
currentRow += rowStep;
currentCol += colStep;
}
// Проверка последовательности в конце диагонали
if (sequenceLength >= TargetLineLength)
{
for (int i = 0; i < sequenceLength; i++)
{
int r = sequenceStartRow + i * rowStep;
int c = sequenceStartCol + i * colStep;
winningCells.Add((r, c));
int curR = startR + i * rowStep;
int curC = startC + i * colStep;
winningCells.Add((curR, curC));
}
}
}
public static bool checkDraw()
/// <summary>
/// Проверяет, заполнено ли всё поле (ничья).
/// </summary>
public static bool CheckDraw()
{
for (int col = 0; col < Width; col++)
{
if (Board.Instance.GetLowestEmptyRow(col) != -1) return false;
if (Board.Instance.GetLowestEmptyRow(col) != -1)
return false;
}
return true;
}
}
public static bool IsWinningMove(Team[,] board, int row, int col, Team player)
{
int height = board.GetLength(0);
int width = board.GetLength(1);
// Проверяем четыре направления
return CheckLine(board, row, col, 0, 1, player, height, width) ||
CheckLine(board, row, col, 1, 0, player, height, width) ||
CheckLine(board, row, col, 1, 1, player, height, width) ||
CheckLine(board, row, col, 1, -1, player, height, width);
}
// Вспомогательный метод для проверки одной линии
private static bool CheckLine(Team[,] board, int row, int col, int dr, int dc, Team player, int height, int width)
{
int count = 1;
// вперёд
int r = row + dr, c = col + dc;
while (r >= 0 && r < height && c >= 0 && c < width && board[r, c] == player)
{
count++;
r += dr;
c += dc;
}
// назад
r = row - dr; c = col - dc;
while (r >= 0 && r < height && c >= 0 && c < width && board[r, c] == player)
{
count++;
r -= dr;
c -= dc;
}
return count >= 4;
}
// Метод для проверки, есть ли у оппонента немедленная победа в один ход
public static bool HasImmediateWin(Team[,] board, Team player)
{
int height = board.GetLength(0);
int width = board.GetLength(1);
for (int col = 0; col < width; col++)
{
int row = GetLowestEmptyRow(board, col);
if (row == -1) continue;
board[row, col] = player;
bool win = IsWinningMove(board, row, col, player);
board[row, col] = Team.None;
if (win) return true;
}
return false;
}
private static int GetLowestEmptyRow(Team[,] board, int col)
{
int height = board.GetLength(0);
for (int r = height - 1; r >= 0; r--)
if (board[r, col] == Team.None)
return r;
return -1;
}
}

View File

@ -24,7 +24,9 @@ public class Player : BasePlayer, IGamePlayer
private Vector2 stickedTargetPosition;
private Vector2 dropTargetPosition;
public event Action OnChipDropped;
private int droppedRow, droppedColumn;
public event Action<int,int> OnChipDropped;
private int SelectedColumn => availableColumns[selectedColumnIndex];
@ -121,8 +123,7 @@ public class Player : BasePlayer, IGamePlayer
if (currentChip.MoveToAccelerated(dropTargetPosition))
{
SetState(PlayerState.Sleep);
OnChipDropped?.Invoke();
Debug.Log("Chip dropped");
OnChipDropped?.Invoke(droppedColumn, droppedRow);
}
}
}
@ -151,6 +152,10 @@ public class Player : BasePlayer, IGamePlayer
if (!IsActionPressed()) return;
dropTargetPosition = board.GetTargetPosition(SelectedColumn);
droppedColumn = SelectedColumn;
droppedRow = board.GetLowestEmptyRow(SelectedColumn);
board.DropChip(currentChip, SelectedColumn);
SetState(PlayerState.Dropping);
}

View File

@ -121,7 +121,7 @@ public class Solver
board[row, col] = BotPiece;
// если сразу победа — отлично
if (WinningMoveAt(board, row, col, BotPiece))
if (FieldChecker.IsWinningMove(board, row, col, BotPiece))
{
board[row, col] = Team.None;
result.Add((col, WIN_SCORE * depth));
@ -146,14 +146,14 @@ public class Solver
// если этот ход уже выигрывает нам игру — не считаем его "ведущим к проигрышу",
// соперник просто не получит ход
if (WinningMoveAt(board, row, botCol, BotPiece))
if (FieldChecker.IsWinningMove(board, row, botCol, BotPiece))
{
board[row, botCol] = Team.None;
return false;
}
// может ли оппонент выиграть в 1 ход?
bool res = HasImmediateWin(board, HumanPiece);
bool res = FieldChecker.HasImmediateWin(board, HumanPiece);
// откат
board[row, botCol] = Team.None;
@ -190,7 +190,7 @@ public class Solver
Team[,] board, int depth, int alpha, int beta,
bool maximizingPlayer, int lastRow, int lastCol, Team lastPlayer)
{
if (lastRow >= 0 && WinningMoveAt(board, lastRow, lastCol, lastPlayer))
if (lastRow >= 0 && FieldChecker.IsWinningMove(board, lastRow, lastCol, lastPlayer))
{
int s = (lastPlayer == BotPiece ? +WIN_SCORE : -WIN_SCORE) * Math.Max(1, depth);
return (-1, s);
@ -265,7 +265,7 @@ public class Solver
score -= CountPotentialLinesPlayable(board, opp, 2) * TWO_IN_ROW;
// 4) Немедленная угроза оппонента → сильный штраф (чтобы бот приоритизировал блок)
if (HasImmediateWin(board, opp))
if (FieldChecker.HasImmediateWin(board, opp))
score -= BLOCK_OPPONENT_BONUS;
return score;
@ -343,44 +343,6 @@ public class Solver
return -1;
}
private static bool HasImmediateWin(Team[,] board, Team player)
{
var valid = GetValidLocations(board);
foreach (int c in valid)
{
int r = GetNextOpenRow(board, c);
if (r < 0) continue;
board[r, c] = player;
bool win = WinningMoveAt(board, r, c, player);
board[r, c] = Team.None;
if (win) return true;
}
return false;
}
private static bool WinningMoveAt(Team[,] b, int r, int c, Team p)
{
bool In(int rr, int cc) => rr >= 0 && rr < ROWS && cc >= 0 && cc < COLS;
int CountDir(int dr, int dc)
{
int rr = r + dr, cc = c + dc, cnt = 0;
while (In(rr, cc) && b[rr, cc] == p) { cnt++; rr += dr; cc += dc; }
return cnt;
}
// — (горизонталь)
if (1 + CountDir(0, -1) + CountDir(0, +1) >= 4) return true;
// | (вертикаль)
if (1 + CountDir(-1, 0) + CountDir(+1, 0) >= 4) return true;
// \ (диагональ)
if (1 + CountDir(-1, -1) + CountDir(+1, +1) >= 4) return true;
// / (диагональ)
if (1 + CountDir(-1, +1) + CountDir(+1, -1) >= 4) return true;
return false;
}
// -------- Стохастика на корне --------
/// <summary>
/// При околонулевой температуре с великой вероятностью лучший по score

View File

@ -41,12 +41,12 @@ public class TurnManager : MonoBehaviour
currentPlayer = player;
}
private void OnTurnCompleted()
private void OnTurnCompleted(int column, int row)
{
// Отписываемся от события текущего игрока
currentPlayer.OnChipDropped -= OnTurnCompleted;
if (CheckGameOver())
if (CheckGameOver(column, row))
{
OnGameOver();
return;
@ -75,11 +75,11 @@ public class TurnManager : MonoBehaviour
StartTurn(currentPlayer);
}
private bool CheckGameOver()
private bool CheckGameOver(int column, int row)
{
if(currentPlayer is BasePlayer player)
{
var checkResult = FieldChecker.Check(player.team);
var checkResult = FieldChecker.Check(player.team, row, column);
return checkResult.End;
}
else
@ -93,6 +93,12 @@ public class TurnManager : MonoBehaviour
isGameOver = true;
currentPlayer.Sleep();
ControlsPerformer.Instance.SetControlable(null);
Debug.Log("Game Over!");
BasePlayer lastPlayer = (BasePlayer)currentPlayer;
var result = FieldChecker.lastResult;
Debug.Log($"end: {result.End}; draw: {result.Draw}; lastPlayer: {lastPlayer.team}");
}
}

View File

@ -4,5 +4,5 @@ public interface IGamePlayer : IControlable
{
void PrepareChip();
void Sleep();
event Action OnChipDropped;
event Action<int, int> OnChipDropped;
}