포스트

[ Unity ] 난이도 측정을 위한 자동 플레이 구현

분석


Match-3 게임에서의 난이도는 레벨 성공 확률이라 할 수 있다. 레벨 제한 조건이 스왑 횟수인 경우, 목표 달성에 사용한 스왑 횟수가 제한 조건에 가까울수록 낮은 성공 확률을 보인다. 따라서 성공 확률이 낮을수록 더 어려운 난이도의 레벨로 판단할 수 있다.

그러나 게임 플레이 시, 같은 레벨을 반복하더라도 각각 목표달성에 사용한 스왑 횟수의 차이가 발생한다. 이러한 현상은 일반 블록으로 인해 발생하며, 일반 블록은 배치 위치와 종류가 정해진 특수 블록과 달리 배치 위치에 랜덤한 종류의 일반 블록이 생성되므로 같은 레벨을 반복하더라도 다른 일반 블록 배치를 보이며 목표 달성에 사용한 스왑 횟수의 분산이 크다.

따라서 정확한 난이도를 측정을 위해 일정 이상 반복이 필요하므로, 이러한 작업을 자동으로 진행하기 위해 컴퓨터를 통한 자동 플레이를 구현했다.

기존 플레이 방식은 현재 상태를 판단하는 함수를 통해 사용자의 입력에 따른 맵의 변화를 통해 업데이트를 진행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//현재 상태를 판단하는 함수
private void Update()
{
    if (!canPlay) return;
    if (WinContr.Result == GameResult.Win) return;
    if (WinContr.Result == GameResult.Loose) return;

    WinContr.UpdateTimer(Time.time);

    // check board state
    switch (MbState)
    {
        case MatchBoardState.ShowEstimate:
            ShowEstimateState();
            break;

        case MatchBoardState.Fill:
            FillState();
            break;

        case MatchBoardState.Collect:
            CollectState();
            break;
    }
}

그러나 이러한 방법은 사용자의 입력이 필요하고, 변화에 따른 애니메이션이 적용되었기 때문에 사용자의 입력이 필요 없는 자동 플레이와 현재 상태를 판단하는 함수를 새로 생성하였다.

자동 플레이는 우선순위 전략을 적용하였다. 스왑을 통한 매치를 발생시킬 수 있는 경우의 수를 토대로 코드를 짰을 때, 아래와 같은 단계를 거친다.

1
2
3
4
5
6
7
8
9
10
if(목표로 정한 일반 블록 파괴할 수 있는 경우의 수 존재)
	해당 블록 swap

else if(맵에 파괴 가능한 특수 블록이 존재 시, 해당 특수 블록의 파괴 조건을 만족시킬 수 있는 경우의 수 존재)
	해당 블록 swap

else
랜덤한 경우의 수 swap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
public void calculateFitnessThroughPlay(DNA<char> p, SpawnController sC, Transform trans, bool isObstacleTarget, CSVFileWriter cs)
{
    List<int> swapCntContainer = new List<int>();

    for (int repeatIdx = 0; repeatIdx < m3h.limits.repeat; repeatIdx++)
    {
        destoryAllCellInGrid();
        setGridFromGene(p, sC);
        foreach (var item in m3h.curTargets) item.Value.InitCurCount();
        m3h.grid.RemoveMatches1(sC);

        m3h.CreateFillPath(m3h.grid);
        p.swapCnt = 0;
        m3h.plays = new PlayHelper();


        while (m3h.plays.isClear == false && m3h.plays.isError == false && m3h.plays.playCnt < m3h.limits.find)
        {
            switch (m3h.plays.curState)
            {
                case 0: m3h.FillState();
                    break;
                case 1: m3h.ShowEstimateState(p,trans);
                    break;
                case 2: m3h.CollectState(p);
                    break;
            }
            m3h.plays.playCnt++;
        }

        if (m3h.plays.isError == true || m3h.plays.isClear == false)
        {
            swapCntContainer.Add(999999);
        }

        else
        {
            swapCntContainer.Add(p.swapCnt);
        }

    }
	p.calculateFeasibleFitness(m3h.wantDifficulty, swapCntContainer 평균);
    ga.generation++;
}


void getMatch3Level(Transform trans, SpawnController sC)
{
    for (int i = 0; i < m3h.limits.geneticGeneration; i++)
    {
        for (int j = 0; j < ga.population.Count; j++)
        {
            if (ga.population[j].fitness == 0)
            {

                initialize();
                setCells(ga.population[j], sC);
                estimateIsFeasible(ga.population[j]);
                calculateFitness(ga.population[j], sC, trans);


				if (ga.population[j].isFeasible)
                {
					calculateFitnessThroughPlay(ga.population[j], sC, trans, isObstacleTarget, cs);
                }
				
				else
				{
					ga.population[j].calculateInfeasibleFitness();
					ga.infeasiblePopulation.Add(ga.population[j]);
					ga.infeasibleFitnessSum += ga.population[j].fitness;
				}
				

                if (ga.bestFitness < ga.population[j].fitness && ga.population[j].isFeasible)
                {
                    ga.bestFitness = ga.population[j].fitness;
                }

                if (ga.bestFitness >= ga.targetFitness)
                {
                    ga.population[0] = ga.population[j];
                    break;
                }
            }

            else
            {
                if (ga.population[j].isFeasible)
                {
                    ga.feasiblePopulation.Add(ga.population[j]);
                    ga.feasibleFitnessSum += ga.population[j].fitness;
                }

                else
                {
                    ga.infeasiblePopulation.Add(ga.population[j]);
                    ga.infeasibleFitnessSum += ga.population[j].fitness;
                }
            }
        }

        if (ga.bestFitness >= ga.targetFitness) break;
        else ga.newGeneration();
    }
}


이러한 과정을 거쳐 성공 조건을 충족한 맵의 swap 횟수를 평균하여 맵의 난이도를 측정, 적합도 계산을 진행한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.