프로젝트 개요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 |
| 리소스 | 인터넷에서 구한 에셋으로 바둑판과 돌 이미지를 구성 |
| 결과 | 완성 |
| Item | Details |
|---|---|
| Engine | Unity 2021.3.45f2 |
| Role | 100% (Solo development) |
| Platform | Android |
| Assets | Board and stone sprites sourced from free online assets |
| Status | Completed |
게임 제작의 기초인 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.

보드를 무엇으로 들고 있는가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.
승패 판정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
count가 1에서 시작한다. 일치할 때마다 오르므로count > 5는 일치 6번이 아니라 연속된 돌 5개를 뜻한다.count = 0으로 시작했다면 조건이count >= 5가 되어 읽기 쉬웠을 것이다.k = 0일 때 자기 자신과 비교한다. 항상 참이라 낭비되는 비교인데, 위의 시작값 1과 맞물려 결과적으로는 맞아떨어진다. 의도한 것인지 우연인지는 코드만 봐서는 알 수 없다.- 범위를 벗어난 칸에서는
count를 리셋하지 않는다.if안에 들어가지 못하고 그냥 넘어가므로, 보드 경계 근처에서 끊긴 구간이 이어져 세어질 여지가 있다. 경계는else로 명시해 리셋하는 편이 안전하다. stone_postion은position의 오타다. 동작에는 영향이 없지만 그대로 남아 있다.
countstarts at 1. Because it increments on every match,count > 5actually means 5 consecutive stones (initial 1 + 5 matches = 6). Starting atcount = 0and checkingcount >= 5would have been much clearer.- When
k = 0, it compares the cell against itself. That comparison is always true, which pairs withcount = 1to make the math work out to 6. Whether that was deliberate or a lucky off-by-one cancellation is hard to tell now. - Out-of-bounds indices do not reset
count. Because out-of-boundsi + kvalues simply skip theifbody, a broken run near the edge isn't explicitly reset, though in a-4..4window aroundithe out-of-bounds cells only occur at one end of the sweep. stone_postionis a typo forposition. It stayed that way across the codebase.
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_ai | AI 쪽에서 가장 좋은 자리의 가중치 |
max_posi_drx[0] / [1] | 각각 방어 후보 좌표 / 공격 후보 좌표 |
max_posi_drx[2] | 실제로 둘 좌표 |
< 3 분기 | 양쪽 다 아직 급하지 않은 국면일 때의 처리 |
| Identifier | Meaning |
|---|---|
max_player | Highest threat weight found for the player |
max_ai | Highest 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 branch | Fallback 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 도 상수로 빼두면 난이도를 조절할 지점이 명확해진다.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

에디터 화면을 보면 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.