프로젝트 개요Project Overview

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

Sipmok is a board game variant based on Gomoku (Five in a Row). I built it as a practice project to sharpen my Unity fundamentals, combining a 19×19 grid and turn-based state management with a simple heuristic AI opponent.

좌우로 밀어서 표 전체 내용을 확인하세요
항목내용
엔진Unity 2021.3.45f2
기여도100% (1인 개발)
플랫폼Android
리소스인터넷에서 구한 에셋으로 바둑판과 돌 이미지를 구성
결과완성
Swipe horizontally to view the full table
ItemDetails
EngineUnity 2021.3.45f2
Role100% (Solo development)
PlatformAndroid
AssetsBoard and stone sprites sourced from free online assets
StatusCompleted

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

The goal was to implement the basics of board game programming from scratch: working with 2D arrays, turn-based state flow, and an AI opponent.

19×19 바둑판 위에 흑돌과 백돌이 놓여 있는 십목 게임 플레이 화면
플레이 화면. 흑과 백의 턴 상태를 직관적으로 확인할 수 있게 구성했다.In-game view, showing the 19×19 board and current turn status for Black and White.

보드를 무엇으로 들고 있는가How the Board State Is Stored

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

The entire board lives in a single int[,] 2D array. Each cell holds an integer representing empty, black, or white, updated whenever a stone is placed.

왜 배열 하나면 충분한가보드게임은 화면에 보이는 돌 오브젝트와 판정에 쓰이는 데이터를 분리해 두는 편이 다루기 쉽다. 승패 판정이나 AI 탐색은 배열만 읽으면 되고, 오브젝트는 배열이 바뀔 때 따라 그리기만 하면 된다. 판정 로직이 화면 상태에 끌려다니지 않는다.
Why a Single Array Is EnoughIn a board game, separating the visual stone GameObjects from the data used for rule evaluation keeps everything much simpler. Win checks and AI search only need to read the integer array, and the scene objects only need to render when the array changes. Game logic never has to query scene transforms.

승패 판정Checking for a Win

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

Right after a stone is placed, the code scans four axes—horizontal, vertical, and both diagonals—around that coordinate to count consecutive stones. Here is the horizontal check from the original script:

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;
}
private bool Is_Victory(int i, int j)
{
    int k;
    int count = 1;

    // Horizontal check
    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;
            }
        }
    }
    // (Vertical and diagonal checks follow the exact same structure)
    return false;
}

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

Centered on the newly placed stone, it sweeps 4 cells left to 4 cells right (9 cells total). Each matching stone increments count, and any mismatch resets it to 1.

지금 이 코드를 읽으면 보이는 것Looking Back at This Code Now

AI가 둘 자리를 고르는 방법How the AI Picks Its Move

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

The AI does not use a minimax search tree. Instead, for every empty cell on the board, it counts consecutive stones across all 8 directions to assign a heuristic weight, then picks the highest-scoring coordinate. It evaluates offensive value (extending its own lines) and defensive value (blocking the player's lines) separately and decides which one takes priority.

// 위치별 우선순위 가중치 탐색 및 결정 로직의 일부
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);
// Excerpt from positional weight evaluation and decision logic
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];
    }
}
// Store the chosen coordinate in max_posi_drx[2] and place the stone
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 분기양쪽 다 아직 급하지 않은 국면일 때의 처리
Swipe horizontally to view the full table
IdentifierMeaning
max_playerHighest threat weight found for the player
max_aiHighest opportunity weight found for the AI
max_posi_drx[0] / [1]Best defensive candidate coordinate / Best offensive candidate coordinate
max_posi_drx[2]Final coordinate where the AI will place its stone
< 3 branchFallback comparison when neither side has an urgent threat yet

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

In short, it scores offense and defense separately and picks the higher priority. When both weights are under 3, neither side has an urgent threat, so it simply compares the two directly.

이렇게 개선할 수 있다max_posi_drx[0], [1], [2] 처럼 인덱스로 의미를 구분하면 나중에 읽을 때 매번 되짚어야 한다. defenseBest, attackBest, chosen 같은 이름의 Vector2Int 로 두면 위 분기가 그대로 읽힌다. 가중치 기준값 3 도 상수로 빼두면 난이도를 조절할 지점이 명확해진다.
How This Could Be CleanerUsing array indices like max_posi_drx[0], [1], and [2] to distinguish roles forces you to decode what each index means every time you read it. Naming three Vector2Int variables defenseBest, attackBest, and chosen would make the branch self-explanatory. Extracting the threshold 3 into a named constant would also make difficulty tuning much easier.

스크립트 구성Script Structure

Unity 에디터에서 십목 프로젝트를 연 화면. Hierarchy에 Square 오브젝트가 나열되어 있고 Script 폴더에 스크립트 파일들이 보인다
에디터 화면. Assets/Script 아래에 역할별 스크립트가 나뉘어 있고, 보드 칸은 Square 오브젝트로 깔려 있다.Unity Editor view. Scripts under Assets/Script are split by role, and board cells are laid out as Square objects.

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

In the Editor, scripts are divided into AI, GameManager, Put_Stone, Stone_Field, Turn_System, and Winning. Board intersections are placed as Square objects in the scene, and Stone_Field is wired into the Field slot on AI_Object so the AI can read the board array.

정리Wrap-Up

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

The main takeaway from this project was controlling a 2D grid array and managing turn state. Designing a heuristic weight function from scratch—and tuning its numbers until the computer placed stones in sensible spots—was the most rewarding part.

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

At the same time, the areas for improvement are obvious. Because I was focused on getting the rules working, too much evaluation logic ended up packed into a single script. If I build another board game, separating board state, rule checks, and AI evaluation into cleaner classes comes first.