프로젝트 개요

십목은 오목을 기반으로 한 변형 보드게임이다. 유니티 게임 개발 역량을 키우기 위해 만든 연습용 프로젝트로, 19×19 그리드와 턴제 로직 위에 간단한 휴리스틱 알고리즘을 적용한 AI 상대를 붙였다.

항목내용
엔진Unity 2021.3.45f2
기여도100% (1인 개발)
플랫폼Android
리소스인터넷에서 구한 에셋으로 바둑판과 돌 이미지를 구성
결과완성

게임 제작의 기초인 2차원 배열 활용턴제 시스템, 그리고 AI와의 대결을 직접 구현해 보는 것이 목표였다.

19×19 바둑판 위에 흑돌과 백돌이 놓여 있는 십목 게임 플레이 화면
플레이 화면. 흑과 백의 턴 상태를 직관적으로 확인할 수 있게 구성했다.

보드를 무엇으로 들고 있는가

보드 상태는 int[,] 2차원 배열 하나로 관리한다. 각 칸이 빈칸인지, 흑돌인지, 백돌인지를 정수로 들고 돌을 놓을 때마다 갱신한다.

왜 배열 하나면 충분한가보드게임은 화면에 보이는 돌 오브젝트와 판정에 쓰이는 데이터를 분리해 두는 편이 다루기 쉽다. 승패 판정이나 AI 탐색은 배열만 읽으면 되고, 오브젝트는 배열이 바뀔 때 따라 그리기만 하면 된다. 판정 로직이 화면 상태에 끌려다니지 않는다.

승패 판정

돌을 둔 직후 그 위치를 기준으로 가로 · 세로 · 양방향 대각선 네 축을 검사해 연속된 돌의 개수를 센다. 당시 코드 중 가로 판정 부분이다.

private bool Is_Victory(int i, int j)
{
    int k;
    int count = 1;

    // 가로 판정
    for (k = -4; k <= 4; k++)
    {
        if (i + k >= 0 && i + k < 19)
        {
            if (posi.stone_postion[i, j] == posi.stone_postion[i + k, j])
            {
                count++;
                if (count > 5) return true;
            }
            else
            {
                count = 1;
            }
        }
    }
    // (세로, 대각선 로직 동일 구조로 생략)
    return false;
}

놓은 칸을 중심으로 좌우 4칸씩, 총 9칸을 한 번에 훑는다. 같은 색이면 count 를 올리고 다르면 1로 되돌리는 단순한 구조다.

지금 이 코드를 읽으면 보이는 것

AI가 둘 자리를 고르는 방법

탐색 트리를 쓰지 않는다. 보드의 빈칸마다 8방향으로 늘어선 돌의 개수를 세어 가중치를 매기고, 가장 높은 좌표에 둔다. 이때 자기 돌을 잇는 값과 상대 돌을 막는 값을 각각 구해 둘 중 무엇을 우선할지 판단한다.

// 위치별 우선순위 가중치 탐색 및 결정 로직의 일부
if (max_ai < 3 && max_player < 3)
{
    if (max_player > max_ai)
    {
        max_posi_drx[2, 0] = max_posi_drx[0, 0];
        max_posi_drx[2, 1] = max_posi_drx[0, 1];
    }
    else
    {
        max_posi_drx[2, 0] = max_posi_drx[1, 0];
        max_posi_drx[2, 1] = max_posi_drx[1, 1];
    }
}
// 위와 같은 조건 분기를 통해 최적의 위치를 max_posi_drx[2] 배열에 저장 후 돌을 둠
putting.Put(max_posi_drx[2, 0], max_posi_drx[2, 1], 2);
코드읽는 법
max_player플레이어 쪽에서 가장 위험한 자리의 가중치
max_aiAI 쪽에서 가장 좋은 자리의 가중치
max_posi_drx[0] / [1]각각 방어 후보 좌표 / 공격 후보 좌표
max_posi_drx[2]실제로 둘 좌표
< 3 분기양쪽 다 아직 급하지 않은 국면일 때의 처리

공격과 방어를 따로 계산해 두고 더 큰 쪽을 고르는 구조다. 양쪽 가중치가 3 미만이면 아직 승부처가 아니라고 보고 그 안에서 다시 비교한다.

이렇게 개선할 수 있다max_posi_drx[0], [1], [2] 처럼 인덱스로 의미를 구분하면 나중에 읽을 때 매번 되짚어야 한다. defenseBest, attackBest, chosen 같은 이름의 Vector2Int 로 두면 위 분기가 그대로 읽힌다. 가중치 기준값 3 도 상수로 빼두면 난이도를 조절할 지점이 명확해진다.

스크립트 구성

Unity 에디터에서 십목 프로젝트를 연 화면. Hierarchy에 Square 오브젝트가 나열되어 있고 Script 폴더에 스크립트 파일들이 보인다
에디터 화면. Assets/Script 아래에 역할별 스크립트가 나뉘어 있고, 보드 칸은 Square 오브젝트로 깔려 있다.

에디터 화면을 보면 AI, GameManager, Put_Stone, Stone_Field, Turn_System, Winning 등으로 스크립트가 나뉘어 있다. 보드의 각 칸은 Square 오브젝트로 깔아 두고, AI_ObjectField 슬롯에 Stone_Field 를 연결해 배열을 넘긴다.

정리

이 프로젝트에서 얻은 것은 2차원 배열 제어와 상태 관리다. 게임 로직 구현의 기본기에 해당하는 부분이고, 특히 컴퓨터가 규칙에 맞게 타당한 자리에 돌을 두도록 휴리스틱 알고리즘을 직접 설계하고 값을 조절해 본 것이 가장 큰 수확이었다.

동시에 남은 숙제도 분명하다. 구현 당시에는 기능이 동작하는 데 초점을 맞추다 보니 연산 로직이 한곳에 몰렸다. 다음에 비슷한 것을 만든다면 클래스의 책임을 나누고 유지보수하기 좋은 구조로 가야 한다.