diff --git a/Assets/Scripts/Common/GameFSM/StateRouter.cs b/Assets/Scripts/Common/GameFSM/StateRouter.cs index 899b8b3..569d89a 100644 --- a/Assets/Scripts/Common/GameFSM/StateRouter.cs +++ b/Assets/Scripts/Common/GameFSM/StateRouter.cs @@ -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); diff --git a/Assets/Scripts/Gameplay/Board.cs b/Assets/Scripts/Gameplay/Board.cs index 91df8aa..d98e953 100644 --- a/Assets/Scripts/Gameplay/Board.cs +++ b/Assets/Scripts/Gameplay/Board.cs @@ -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; diff --git a/Assets/Scripts/Gameplay/Bot.cs b/Assets/Scripts/Gameplay/Bot.cs index 76eef59..33e76f0 100644 --- a/Assets/Scripts/Gameplay/Bot.cs +++ b/Assets/Scripts/Gameplay/Bot.cs @@ -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 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 } \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/FieldChecker.cs b/Assets/Scripts/Gameplay/FieldChecker.cs index 05d9443..09dc8f9 100644 --- a/Assets/Scripts/Gameplay/FieldChecker.cs +++ b/Assets/Scripts/Gameplay/FieldChecker.cs @@ -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) + /// + /// Проверяет, привела ли последняя фишка (установленная командой lastPlayer в клетку (row, col)) к победе. + /// + /// Команда, которая только что сделала ход. + /// Строка (индекс по вертикали). + /// Столбец (индекс по горизонтали). + /// Результат проверки (окончание игры, ничья, множество победных клеток). + 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) + + /// + /// Проверяет линию, заданную направлением (rowStep, colStep), проходящую через клетку (row, col). + /// Если найдена непрерывная последовательность из фишек lastPlayer длиной >= TargetLineLength, + /// все клетки этой последовательности добавляются в winningCells. + /// + 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() + /// + /// Проверяет, заполнено ли всё поле (ничья). + /// + 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; } -} \ No newline at end of file + + + 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; + } +} diff --git a/Assets/Scripts/Gameplay/Player.cs b/Assets/Scripts/Gameplay/Player.cs index a289359..fc3482d 100644 --- a/Assets/Scripts/Gameplay/Player.cs +++ b/Assets/Scripts/Gameplay/Player.cs @@ -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 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); } diff --git a/Assets/Scripts/Gameplay/Solver.cs b/Assets/Scripts/Gameplay/Solver.cs index 020bebf..f22943d 100644 --- a/Assets/Scripts/Gameplay/Solver.cs +++ b/Assets/Scripts/Gameplay/Solver.cs @@ -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; - } - // -------- Стохастика на корне -------- /// /// При околонулевой температуре с великой вероятностью лучший по score diff --git a/Assets/Scripts/Gameplay/TurnManager.cs b/Assets/Scripts/Gameplay/TurnManager.cs index 05cd081..f443864 100644 --- a/Assets/Scripts/Gameplay/TurnManager.cs +++ b/Assets/Scripts/Gameplay/TurnManager.cs @@ -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}"); } } \ No newline at end of file diff --git a/Assets/Scripts/IGamePlayer.cs b/Assets/Scripts/IGamePlayer.cs index 6a7ff61..c2baa28 100644 --- a/Assets/Scripts/IGamePlayer.cs +++ b/Assets/Scripts/IGamePlayer.cs @@ -4,5 +4,5 @@ public interface IGamePlayer : IControlable { void PrepareChip(); void Sleep(); - event Action OnChipDropped; + event Action OnChipDropped; } \ No newline at end of file