DO-4 [Refactor] 평가 함수 분리

This commit is contained in:
Sehyeon 2025-03-19 13:36:04 +09:00
parent 51f9299c61
commit b382e82059
6 changed files with 411 additions and 7 deletions

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34f6533696ad41518da4bcc203309338
timeCreated: 1742357084

View File

@ -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
*/
}

View File

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