From b382e82059fb2866f623013f2e8536c70805e92b Mon Sep 17 00:00:00 2001 From: Sehyeon Date: Wed, 19 Mar 2025 13:36:04 +0900 Subject: [PATCH] =?UTF-8?q?DO-4=20[Refactor]=20=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/{Script/AI => KSH}/GameCopy_AI.unity | 0 .../{Script/AI => KSH}/GameCopy_AI.unity.meta | 0 Assets/Script/AI/AIEvaluator.cs | 394 ++++++++++++++++++ Assets/Script/AI/AIEvaluator.cs.meta | 3 + Assets/Script/AI/MiniMaxAIController.cs | 14 +- Assets/Script/Game/GameLogic.cs | 7 +- 6 files changed, 411 insertions(+), 7 deletions(-) rename Assets/{Script/AI => KSH}/GameCopy_AI.unity (100%) rename Assets/{Script/AI => KSH}/GameCopy_AI.unity.meta (100%) create mode 100644 Assets/Script/AI/AIEvaluator.cs create mode 100644 Assets/Script/AI/AIEvaluator.cs.meta diff --git a/Assets/Script/AI/GameCopy_AI.unity b/Assets/KSH/GameCopy_AI.unity similarity index 100% rename from Assets/Script/AI/GameCopy_AI.unity rename to Assets/KSH/GameCopy_AI.unity diff --git a/Assets/Script/AI/GameCopy_AI.unity.meta b/Assets/KSH/GameCopy_AI.unity.meta similarity index 100% rename from Assets/Script/AI/GameCopy_AI.unity.meta rename to Assets/KSH/GameCopy_AI.unity.meta diff --git a/Assets/Script/AI/AIEvaluator.cs b/Assets/Script/AI/AIEvaluator.cs new file mode 100644 index 0000000..c215406 --- /dev/null +++ b/Assets/Script/AI/AIEvaluator.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +public static class AIEvaluator +{ + // 패턴 가중치 상수 + public static class PatternScore + { + // AI 패턴 점수 + public const float FIVE_IN_A_ROW = 100000f; + public const float OPEN_FOUR = 15000f; + public const float HALF_OPEN_FOUR = 5000f; + public const float CLOSED_FOUR = 500f; + public const float OPEN_THREE = 3000f; + public const float HALF_OPEN_THREE = 500f; + public const float CLOSED_THREE = 50f; + public const float OPEN_TWO = 100f; + public const float HALF_OPEN_TWO = 30f; + public const float CLOSED_TWO = 10f; + public const float OPEN_ONE = 10f; + public const float CLOSED_ONE = 1f; + + // 복합 패턴 점수 + public const float DOUBLE_THREE = 8000f; + public const float DOUBLE_FOUR = 12000f; + public const float FOUR_THREE = 10000f; + + // 위치 가중치 기본값 + public const float CENTER_WEIGHT = 1.2f; + public const float EDGE_WEIGHT = 0.8f; + } + + // 방향 상수 -> public으로 빼기 + private static readonly int[][] Directions = new int[][] + { + new int[] {1, 0}, // 수직 + new int[] {0, 1}, // 수평 + new int[] {1, 1}, // 대각선 ↘ ↖ + new int[] {1, -1} // 대각선 ↙ ↗ + }; + + // 보드 전체 상태 평가 + public static float EvaluateBoard(Enums.PlayerType[,] board, Enums.PlayerType aiPlayer) + { + float score = 0; + int size = board.GetLength(0); + + // 복합 패턴 감지를 위한 위치 저장 리스트 + List<(int row, int col, int[] dir)> aiOpen3Positions = new List<(int, int, int[])>(); + List<(int row, int col, int[] dir)> playerOpen3Positions = new List<(int, int, int[])>(); + List<(int row, int col, int[] dir)> ai4Positions = new List<(int, int, int[])>(); + List<(int row, int col, int[] dir)> player4Positions = new List<(int, int, int[])>(); + + // 1. 기본 패턴 평가 + score += EvaluateBoardPatterns(board, aiPlayer, size, aiOpen3Positions, playerOpen3Positions, + ai4Positions, player4Positions); + + // 2. 복합 패턴 평가 + score += EvaluateComplexPatterns(aiOpen3Positions, playerOpen3Positions, ai4Positions, player4Positions, aiPlayer); + + return score; + } + + // 기본 패턴 (돌의 연속, 열린 끝 등) 평가 + private static float EvaluateBoardPatterns( + Enums.PlayerType[,] board, + Enums.PlayerType aiPlayer, + int size, + List<(int row, int col, int[] dir)> aiOpen3Positions, + List<(int row, int col, int[] dir)> playerOpen3Positions, + List<(int row, int col, int[] dir)> ai4Positions, + List<(int row, int col, int[] dir)> player4Positions) + { + float score = 0; + Enums.PlayerType opponentPlayer = (aiPlayer == Enums.PlayerType.PlayerA) ? + Enums.PlayerType.PlayerB : Enums.PlayerType.PlayerA; + + for (int row = 0; row < size; row++) + { + for (int col = 0; col < size; col++) + { + if (board[row, col] == Enums.PlayerType.None) continue; + + Enums.PlayerType currentPlayer = board[row, col]; + int playerScore = (currentPlayer == aiPlayer) ? 1 : -1; // AI는 양수, 플레이어는 음수 + + // 위치 가중치 계산 + float positionWeight = CalculatePositionWeight(row, col, size); + + foreach (var dir in Directions) + { + var (count, openEnds) = MiniMaxAIController.CountStones(board, row, col, dir, currentPlayer); + + // 점수 계산 + float patternScore = EvaluatePattern(count, openEnds); + + // 패턴 수집 (복합 패턴 감지용) + CollectPatterns(row, col, dir, count, openEnds, currentPlayer, aiPlayer, + aiOpen3Positions, playerOpen3Positions, ai4Positions, player4Positions); + + // 위치 가중치 적용 + patternScore *= positionWeight; + + // 최종 점수 적용 (플레이어는 음수) + score += playerScore * patternScore; + } + } + } + + return score; + } + + // 개별 패턴 평가 (돌 개수와 열린 끝 기준) + private static float EvaluatePattern(int count, int openEnds) + { + if (count >= 5) + { + return PatternScore.FIVE_IN_A_ROW; + } + else if (count == 4) + { + return (openEnds == 2) ? PatternScore.OPEN_FOUR : + (openEnds == 1) ? PatternScore.HALF_OPEN_FOUR : + PatternScore.CLOSED_FOUR; + } + else if (count == 3) + { + return (openEnds == 2) ? PatternScore.OPEN_THREE : + (openEnds == 1) ? PatternScore.HALF_OPEN_THREE : + PatternScore.CLOSED_THREE; + } + else if (count == 2) + { + return (openEnds == 2) ? PatternScore.OPEN_TWO : + (openEnds == 1) ? PatternScore.HALF_OPEN_TWO : + PatternScore.CLOSED_TWO; + } + else if (count == 1) + { + return (openEnds == 2) ? PatternScore.OPEN_ONE : PatternScore.CLOSED_ONE; + } + + return 0; + } + + // 복합 패턴 평가 (3-3, 4-4, 4-3 등) + private static float EvaluateComplexPatterns( + List<(int row, int col, int[] dir)> aiOpen3Positions, + List<(int row, int col, int[] dir)> playerOpen3Positions, + List<(int row, int col, int[] dir)> ai4Positions, + List<(int row, int col, int[] dir)> player4Positions, + Enums.PlayerType aiPlayer) + { + float score = 0; + + // 삼삼(3-3) 감지 + int aiThreeThree = DetectDoubleThree(aiOpen3Positions); + int playerThreeThree = DetectDoubleThree(playerOpen3Positions); + + // 사사(4-4) 감지 + int aiFourFour = DetectDoubleFour(ai4Positions); + int playerFourFour = DetectDoubleFour(player4Positions); + + // 사삼(4-3) 감지 + int aiFourThree = DetectFourThree(ai4Positions, aiOpen3Positions); + int playerFourThree = DetectFourThree(player4Positions, playerOpen3Positions); + + // 복합 패턴 점수 합산 + score += aiThreeThree * PatternScore.DOUBLE_THREE; + score -= playerThreeThree * PatternScore.DOUBLE_THREE; + + score += aiFourFour * PatternScore.DOUBLE_FOUR; + score -= playerFourFour * PatternScore.DOUBLE_FOUR; + + score += aiFourThree * PatternScore.FOUR_THREE; + score -= playerFourThree * PatternScore.FOUR_THREE; + + return score; + } + + // 위치 가중치 계산 함수 + private static float CalculatePositionWeight(int row, int col, int size) + { + float boardCenterPos = (size - 1) / 2.0f; + + // 현재 위치와 중앙과의 거리 계산 (0~1 사이 값) + float distance = Math.Max(Math.Abs(row - boardCenterPos), Math.Abs(col - boardCenterPos)) / boardCenterPos; + + // 중앙(거리 0)은 1.2배, 가장자리(거리 1)는 0.8배 + return PatternScore.CENTER_WEIGHT - ((PatternScore.CENTER_WEIGHT - PatternScore.EDGE_WEIGHT) * distance); + } + + // 패턴 수집 함수 (복합 패턴 감지용) + private static void CollectPatterns( + int row, int col, int[] dir, int count, int openEnds, + Enums.PlayerType currentPlayer, Enums.PlayerType aiPlayer, + List<(int row, int col, int[] dir)> aiOpen3Positions, + List<(int row, int col, int[] dir)> playerOpen3Positions, + List<(int row, int col, int[] dir)> ai4Positions, + List<(int row, int col, int[] dir)> player4Positions) + { + // 열린 3 패턴 수집 + if (count == 3 && openEnds == 2) + { + if (currentPlayer == aiPlayer) + aiOpen3Positions.Add((row, col, dir)); + else + playerOpen3Positions.Add((row, col, dir)); + } + + // 4 패턴 수집 + if (count == 4 && openEnds >= 1) + { + if (currentPlayer == aiPlayer) + ai4Positions.Add((row, col, dir)); + else + player4Positions.Add((row, col, dir)); + } + } + + // 삼삼(3-3) 감지 함수 + private static int DetectDoubleThree(List<(int row, int col, int[] dir)> openThreePositions) + { + int doubleThreeCount = 0; + var checkedPairs = new HashSet<(int, int)>(); + + for (int i = 0; i < openThreePositions.Count; i++) + { + var (row1, col1, dir1) = openThreePositions[i]; + + for (int j = i + 1; j < openThreePositions.Count; j++) + { + var (row2, col2, dir2) = openThreePositions[j]; + + // 같은 돌에서 다른 방향으로 두 개의 열린 3이 형성된 경우 + if (row1 == row2 && col1 == col2 && !AreParallelDirections(dir1, dir2)) + { + if (!checkedPairs.Contains((row1, col1))) + { + doubleThreeCount++; + checkedPairs.Add((row1, col1)); + } + } + } + } + + return doubleThreeCount; + } + + // 방향이 평행한지 확인하는 함수 + private static bool AreParallelDirections(int[] dir1, int[] dir2) + { + return (dir1[0] == dir2[0] && dir1[1] == dir2[1]) || + (dir1[0] == -dir2[0] && dir1[1] == -dir2[1]); + } + + // 사사(4-4) 감지 함수 + private static int DetectDoubleFour(List<(int row, int col, int[] dir)> fourPositions) + { + int doubleFourCount = 0; + var checkedPairs = new HashSet<(int, int)>(); + + for (int i = 0; i < fourPositions.Count; i++) + { + var (row1, col1, dir1) = fourPositions[i]; + + for (int j = i + 1; j < fourPositions.Count; j++) + { + var (row2, col2, dir2) = fourPositions[j]; + + if (row1 == row2 && col1 == col2 && !AreParallelDirections(dir1, dir2)) + { + if (!checkedPairs.Contains((row1, col1))) + { + doubleFourCount++; + checkedPairs.Add((row1, col1)); + } + } + } + } + + return doubleFourCount; + } + + // 사삼(4-3) 감지 함수 + private static int DetectFourThree( + List<(int row, int col, int[] dir)> fourPositions, + List<(int row, int col, int[] dir)> openThreePositions) + { + int fourThreeCount = 0; + var checkedPairs = new HashSet<(int, int)>(); + + foreach (var (row1, col1, _) in fourPositions) + { + foreach (var (row2, col2, _) in openThreePositions) + { + // 같은 돌에서 4와 열린 3이 동시에 형성된 경우 + if (row1 == row2 && col1 == col2) + { + if (!checkedPairs.Contains((row1, col1))) + { + fourThreeCount++; + checkedPairs.Add((row1, col1)); + } + } + } + } + + return fourThreeCount; + } + + // 이동 평가 함수 (EvaluateMove 대체) + public static float EvaluateMove(Enums.PlayerType[,] board, int row, int col, Enums.PlayerType AIPlayer) + { + float score = 0; + Enums.PlayerType opponentPlayer = (AIPlayer == Enums.PlayerType.PlayerA) ? + Enums.PlayerType.PlayerB : Enums.PlayerType.PlayerA; + + // AI 관점에서 평가 + board[row, col] = AIPlayer; + + foreach (var dir in Directions) + { + var (count, openEnds) = MiniMaxAIController.CountStones(board, row, col, dir, AIPlayer, false); + + if (count >= 4) + { + score += PatternScore.FIVE_IN_A_ROW / 10; + } + else if (count == 3) + { + score += (openEnds == 2) ? PatternScore.OPEN_THREE / 3 : + (openEnds == 1) ? PatternScore.HALF_OPEN_THREE / 5 : + PatternScore.CLOSED_THREE / 5; + } + else if (count == 2) + { + score += (openEnds == 2) ? PatternScore.OPEN_TWO / 2 : + (openEnds == 1) ? PatternScore.HALF_OPEN_TWO / 3 : + PatternScore.CLOSED_TWO / 5; + } + else if (count == 1) + { + score += (openEnds == 2) ? PatternScore.OPEN_ONE : + PatternScore.CLOSED_ONE; + } + } + + // 상대 관점에서 평가 (방어 가치) + board[row, col] = opponentPlayer; + + foreach (var dir in Directions) + { + var (count, openEnds) = MiniMaxAIController.CountStones(board, row, col, dir, opponentPlayer, false); + + // 상대 패턴 차단에 대한 가치 (약간 낮은 가중치) + if (count >= 4) + { + score += PatternScore.FIVE_IN_A_ROW / 12.5f; + } + else if (count == 3) + { + score += (openEnds == 2) ? PatternScore.OPEN_THREE / 3.75f : + (openEnds == 1) ? PatternScore.HALF_OPEN_THREE / 6.25f : + PatternScore.CLOSED_THREE / 6.25f; + } + else if (count == 2) + { + score += (openEnds == 2) ? PatternScore.OPEN_TWO / 2.5f : + (openEnds == 1) ? PatternScore.HALF_OPEN_TWO / 3.75f : + PatternScore.CLOSED_TWO / 5f; + } + else if (count == 1) + { + score += (openEnds == 2) ? PatternScore.OPEN_ONE / 1.25f : + PatternScore.CLOSED_ONE; + } + } + + // 원래 상태로 복원 + board[row, col] = Enums.PlayerType.None; + + // 중앙에 가까울수록 추가 점수 + int size = board.GetLength(0); + float centerDistance = Math.Max( + Math.Abs(row - (size - 1) / 2.0f), + Math.Abs(col - (size - 1) / 2.0f) + ); + float centerBonus = 1.0f - (centerDistance / ((size - 1) / 2.0f)) * 0.3f; // 30% 가중치 + + return score * centerBonus; + } +} diff --git a/Assets/Script/AI/AIEvaluator.cs.meta b/Assets/Script/AI/AIEvaluator.cs.meta new file mode 100644 index 0000000..a35398f --- /dev/null +++ b/Assets/Script/AI/AIEvaluator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34f6533696ad41518da4bcc203309338 +timeCreated: 1742357084 \ No newline at end of file diff --git a/Assets/Script/AI/MiniMaxAIController.cs b/Assets/Script/AI/MiniMaxAIController.cs index 5cf111e..87494d9 100644 --- a/Assets/Script/AI/MiniMaxAIController.cs +++ b/Assets/Script/AI/MiniMaxAIController.cs @@ -26,6 +26,11 @@ public static class MiniMaxAIController private static Dictionary<(int row, int col), Dictionary<(int dirX, int dirY), (int count, int openEnds)>> _spatialStoneCache = new Dictionary<(int row, int col), Dictionary<(int dirX, int dirY), (int count, int openEnds)>>(); + // AI Player Type 변경 (AI가 선수로 둘 수 있을지도 모르니..) + public static void SetAIPlayerType(Enums.PlayerType AIPlayerType) + { + _AIPlayerType = AIPlayerType; + } // 급수 설정 -> 실수 넣을 때 계산 public static void SetLevel(int level) @@ -101,7 +106,7 @@ public static class MiniMaxAIController { if (CheckGameWin(Enums.PlayerType.PlayerA, board, recentRow, recentCol)) return -100 + depth; if (CheckGameWin(Enums.PlayerType.PlayerB, board, recentRow, recentCol)) return 100 - depth; - if (depth == 0) return EvaluateBoard(board); + if (depth == 0) return AIEvaluator.EvaluateBoard(board, _AIPlayerType); float bestScore = isMaximizing ? float.MinValue : float.MaxValue; List<(int row, int col, float score)> validMoves = GetValidMoves(board); // 현재 놓을 수 있는 자리 리스트 @@ -145,7 +150,7 @@ public static class MiniMaxAIController if (board[row, col] == Enums.PlayerType.None && HasNearbyStones(board, row, col)) { // 보드 전체가 아닌 해당 돌에 대해서만 Score 계산 - float score = EvaluateMove(board, row, col); + float score = AIEvaluator.EvaluateMove(board, row, col, _AIPlayerType); validMoves.Add((row, col, score)); } } @@ -175,7 +180,7 @@ public static class MiniMaxAIController } // 특정 방향으로 같은 돌 개수와 열린 끝 개수를 계산하는 함수 - private static (int count, int openEnds) CountStones( + public static (int count, int openEnds) CountStones( Enums.PlayerType[,] board, int row, int col, int[] direction, Enums.PlayerType player, bool isSaveInCache = true) { int dirX = direction[0], dirY = direction[1]; @@ -310,7 +315,7 @@ public static class MiniMaxAIController return fiveInARowMoves; } - +/* #region Evaluate Score // 특정 위치의 Score를 평가하는 새로운 함수 @@ -590,4 +595,5 @@ public static class MiniMaxAIController } #endregion +*/ } diff --git a/Assets/Script/Game/GameLogic.cs b/Assets/Script/Game/GameLogic.cs index c46f97f..688b390 100644 --- a/Assets/Script/Game/GameLogic.cs +++ b/Assets/Script/Game/GameLogic.cs @@ -261,7 +261,7 @@ public class GameLogic : MonoBehaviour ReplayManager.Instance.RecordStonePlaced(Enums.StoneType.Black, row, col); //기보 데이터 저장 break; case Enums.PlayerType.PlayerB: - + /* // AI 테스트 시작 OmokAI.Instance.StartBestMoveSearch(_board, (bestMove) => { @@ -277,13 +277,14 @@ public class GameLogic : MonoBehaviour } }); // AI 테스트 끝 + */ - /*stoneController.SetStoneType(Enums.StoneType.White, row, col); + stoneController.SetStoneType(Enums.StoneType.White, row, col); stoneController.SetStoneState(Enums.StoneState.LastPositioned, row, col); _board[row, col] = Enums.PlayerType.PlayerB; LastNSelectedSetting(row, col); - ReplayManager.Instance.RecordStonePlaced(Enums.StoneType.White, row, col);*/ + ReplayManager.Instance.RecordStonePlaced(Enums.StoneType.White, row, col); break; } }