대학 1학년 때, 아는 것도 없이 과동기 3명이서 순수 패기로만 참여했었던 대회이다. 그래서 예선 광탈을 했던 기억, 가독성 나쁘게 코드를 써서 동기에게 한소리 들은 기억, 잘하는 동기를 보고 감탄했던 기억, Bitbucket 잘 못써서 힘들었던 기억 등... 본선은 못갔지만 배운 건 꽤 있었던 것 같다.
당시 제작한 Weigting Module. 내가 쓰면, 동기가 고치고. 이 과정을 되게 많이 반복했었다. 당시 그만큼 내 코드엔 문제가 많았다.
//외부배열 board[][] 존재
int Boardweighting[BOARD_SIZE][BOARD_SIZE]={0};
int Ourteamrow=0,Ourteamcol=0;
int Oppositerow=0,Oppositecol=0;
int row=0,col=0;
//--------------- 돌 주변 좌표에 가중치 1 세팅 -----------------------------
void BWClear(){ //가중치 초기화 함수
int i=0,j=0;
for(i=0;i<BOARD_SIZE;i++){
for(j=0;j<BOARD_SIZE;j++){
Boardweighting[i][j]=0;
}
}
)
int isOur(int x, int y)
{
return x >= 0 && y >= 0 && x < width && y < height && board[x][y] == 1;
}
int isOp(int x, int y)
{
return x >= 0 && y >= 0 && x < width && y < height && board[x][y] == 2;
}
for(row=0;row<BOARD_SIZE;row++){
for(col=0;col<BOARD_SIZE;col++){
// LEFT →
if( col < BOARD_SIZE-1 && board[row][col+1]==1 && Boardweighting[row][col]<1 )
Boardweighting[row][col]=1;
else // RIGHT ←
if( col > 0 && board[row][col-1]==1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // DOWN ↓
if( row < BOARD_SIZE-1 && board[row+1][col]==1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // UP ↑
if( row > 0 && board[row-1][col]==1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // ↘
if( (row < BOARD_SIZE-1 && col < BOARD_SIZE-1) && board[row+1][col+1] == 1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // ↖
if( (row > 0 && col > 0) && board[row-1][col-1] == 1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // ↗
if( (row > 0 && col < BOARD_SIZE-1) && board[row-1][col+1] == 1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
else // ↙
if( (row < BOARD_SIZE-1 && col > 0) && board[row+1][col-1] == 1 && Boardweighting[row][col]<1)
Boardweighting[row][col]=1;
//-------------- 돌의 개수에 의한 일반 가중치 -------------------
for(row=0;row<BOARD_SIZE;row++){
for(col=0;col<BOARD_SIZE;col++){
// RIGHT →
if( col < 15
&& Boardweighting[row][col] ==1 //현재 가중치가 설정된 위치
&& isOur(row,col+1) == 1 ){ //right에 돌이 있는 경우
count=0;
tempPlayer=board[row][col+1] ;
for(i=col+1; i<15;i++){
if( board[row][i] == tempPlayer )
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// LEFT ←
if( col > 0
&& Boardweighting[row][col] == 1 //현재 가중치가 설정된 위치
&& isOur(row,col-1) == 1){ //left에 돌이 있는 경우
count=0;
tempPlayer=board[row][col-1] ;
for(i=col-1; i>=0;i--){
if( board[row][i] == tempPlayer )
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// DOWN ↓
if( row < 15
&& Boardweighting[row][col] ==1 //현재 가중치가 설정된 위치
&& isOur(row+1,col) == 1){ //down에 돌이 있는 경우
count=0;
tempPlayer=board[row+1][col] ;
for(i=row+1; i<15 ;i++){
if( board[i][col] == tempPlayer )
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// UP ↑
if( row > 0
&& board[row][col] == 1 //현재 가중치가 설정된 위치
&& isOur(row-1,col) == 1){ //up에 돌이 있는 경우
count=0;
tempPlayer=board[row-1][col] ;
for(i=row-1; i>=0 ;i--){
if( board[i][col] == tempPlayer )
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// ↘ right down
if( (row < 15 && col < 15)
&& board[row][col] == 1 // 현재 가중치가 설정된 위치
&& isOur(row+1,col+1) == 1) // right-down에 돌이 있는 경우
{
count=0;
tempPlayer=board[row+1][col+1] ;
for(i=1;i<5;i++){
if((row+i<15 && col+i<15) && (board[row+i][col+i]== tempPlayer))
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// ↖ left up
if( (row > 0 && col > 0)
&& board[row][col] == 1 // 현재 가중치가 설정된 위치
&& isOur(row-1,col-1) == 1){ // left-up에 돌이 있는 경우
count=0;
tempPlayer=Board[row-1][col-1] ;
for(i=1;i<5;i++){
if((row-i>=0 && col-i>=0) && (board[row-i][col-i]== tempPlayer))
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// ↗ right up
if( (row > 0 && col < 15)
&& board[row][col] == 1 // 현재 가중치가 설정된 위치
&& isOur(row-1,col+1) == 1) // right-up에 돌이 있는 경우
{
count=0;
tempPlayer=board[row-1][col+1] ;
for(i=1;i<10;i++){
if((row-i>=0 && col+i<15) && (board[row-i][col+i]== tempPlayer))
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
// ↙ left down
if( (row < 15 && col > 0)
&& board[row][col] == 1 // 현재 가중치가 설정된 위치
&& isOur(row+1,col-1) == 1) // left-down에 돌이 있을 경우
{
count=0;
tempPlayer=board[row+1][col-1] ;
for(i=1;i<5;i++){
if((row+i<15 && col-i>0) && (board[row+i][col-i]== tempPlayer))
count++;
else
break;
}
if(Boardweighting[row][col] < count){ // 해당 위치 가중치가 count 보다 작다면
Boardweighting[row][col]=count;
}
}
}
}
void GetAttackPos(int board[][BOARD_SIZE], int* row, int* col){ //parameter에 각각 Boardweighting, Ourteamrow, Ourteamcol
int Max=0;
for(int i=0;i< 15; i++){
for(int j=0;j< 15; j++){
if(board[i][j] > Max && board[i][j] < PLAYER){
Max = board[i][j];
*row =i;
*col =j; //Ourteamrow, Ourteamcol의 주소를 참조
}
}
}
return 0;
}
void GetDefencePos(int board[][BOARD_SIZE], int* row, int* col){ //parameter에 각각 Boardweighting, Oppositerow, Oppositecol
int Max=0;
for(int i=0;i< 15; i++){
for(int j=0;j< 15; j++){
if(board[i][j] > Max && board[i][j] < PLAYER){
Max = board[i][j];
*row =i;
*col =j; //Oppositerow, Oppositecol의 주소를 참조
}
}
}
return 0;
}
void CompareWeight(){
if (Boardweighting[Ourteamrow][Ourteamcol]>Boardweighting[Oppositerow][Oppositecol]){ //우리팀 기준 가중치가 더 클때 그곳에 둔다
x[i]=Ourteamcol;
y[i]=Ourteamrow;
}
if (Boardweighting[Ourteamrow][Ourteamcol]<=Boardweighting[Oppositerow][Oppositecol]){ //상대팀 기준 가중치가 더 클때 혹은 같을 때 그곳에 둔다
x[i]=Oppsitecol;
y[i]=Oppositerow;
}
}
당시 상대가 놓은 돌의 위치별로 가중치를 둬서 가장 높은 가치함수를 가진 곳에 수를 두는 알고리즘을 만드려고 했었다.
이 점이 문제였던게, 바둑이 아닌 육목이더라도, 상대의 돌의 수에 대한 가지의 수가 생각보다 많기 때문에 이걸 모두 가지의 수를 나누어서 문제를 해결한다는 것은 의미가 없다. 뭐 그 이전에 이미 가중치를 두는 기준조차도 틀렸지만...
지금 다시 시도한다면 아마 이렇게 할 것 같다.
1. 강화학습을 적용한다.
우선 육목판의 배열에 대해서 모두 하나의 State로 지정해준다. 그리고 각 상태에 대응하는 Action의 배열을 만들어준다.
그리고 State에 대한 아주 기본적인 최고 순위 Action(상대가 막힌 곳이 없이 3줄이 나온 경우)등을 제일 먼저 체크한 후, 각 육목판 State에서 할 수 있는 Action을 랜덤하게 행하고, 승리시 그에 대한 가중치를 상승시킨다.
다만, 이 육목판에서 나올 수 있는 경우의 수가 최대$3^{19×19}$인데다가, 7초 시간제한이 있고, 사용 가능한 언어도 C/C++로 제한되어있으니, 이 모든 과정을 예선 전에 미리 해놓고 weigting module에서의 모든 액션 가중치를 만들어놔야 할 것이다.
또한 룰 중에 blocking에 대한 룰이 있으므로, 최대 승률을 가진 action이 실행 불가할 때 그 다음 최대 승률의 action을 행할 수 있도록 설정해놔야 할 것이다.
이 방법이 최선은 아니겠지만, 더 나은 방법이 생각이 나지 않는다.
2. 내가 모르는 것과 공부해야하는 것을 명확히 한다. 그리고 공부해야하는 것을 찾는 방법을 알아낸다.
사실 이게 제일 이 프로젝트를 하고 제일 크게 깨달은 것이었다. 저 당시 강화학습이나 API 사용하는 요령을 모르는 것보다는, 무엇을 알아야하는 지를 고민하지 않은 것이 제일 큰 문제였다. '그냥 생각하면 답 나오겠지...'하고 무작정 코딩해서 나온게 저런 쓰레기같은 코드다.
아마 다시 저 때로 돌아간다면 적어도 바둑이던 체스던, 게임에 대한 알고리즘을 찾아보고 벤치마킹하거나 강화학습에 대해서 찾아보고 구현하려고 시도해보지 않았을까 싶다.
3. 프로젝트 중 공부한 것에 연장해서 공부했을 것 같다.
이 프로젝트가 끝나고 나는 한동안 프로그래밍에 손을 대지 않았다. 5달 정도?
육목 프로젝트 한 번으로, 난 나의 문제점을 굉장히 많이 파악할 수 있었다. 코드 가독성, 알고리즘과 자료구조에 대한 이해 부족, 기계학습에 대한 기본적인 접근법 등... 사실 이 시기만큼 공부하기에 적합했던 때가 없었던 것 같다.
하지만 난 당시 내 실력을 부끄럽게 여기면서도, 발전하고자 노력하진 않았던 것 같다. 학과 공부네 동아리네 뭐네 바쁘긴 했지만, 학과 공부를 제외하면 다른 것들이 프로그래밍보다 중요했나 싶기도 하고... 지금 돌아보면 많이 후회된다.
당분간은 이렇게 전에 했었던 프로젝트를 돌아보면서 당시 부족했던 건 무엇이고, 지금도 그렇지 않은지, 앞으로 어떤 것을 배우는 게 좋을지 생각하는 글을 쓸까 한다.
2편에서 계속...