reference > 게임트리를 사용한 제로섬 게임의 인공지능
위 내용을 바탕으로 minMax 알고리즘을 이용한 TicTacToe 게임을 구현해보았다.
레퍼런스에서의 소스코드에 있는 평가함수와는 다른 방식으로 해결해 보았다.
Tic Tac toe
게임 방식
• 게임에 참여하는 플레이어는 2명이다.
• 양 플레이어는 서로 번갈아가면서 가로 * 세로 3칸씩으로 구성된 표에 서로 자리를 점한다.
• 자리가 빈 곳에만 점할 수 있다.
승리 조건
• 플레이어가 점한 3자리가 1자로 되었을 경우 그 플레이어가 승리한다.
• 테이블이 꽉 찰 때까지 결판이 나지 않으면 경기는 무승부로 한다.
문제 풀이
초기상태 및 목표상태, 연산자 정의
• 초기상태 : 9칸의 빈 테이블
• 목표상태 : 승리상태를 얻어낼 수 있는 테이블
• 연산자
- 플레이어가 자리를 정할 수 있는 setMark(position, p)를 정의한다.
테이블의 position에 p를 입력함으로서 자리를 정한다.
- 테이블변수를 t, 플레이어를 p라 하고 스키마 win(t, p)를 정의한다.
win(t, p)는 테이블을 평가하여 플레이어 p가 승리하였는지 판단한다.
- 플레이어의 차례를 정하는 sequence(t) 스키마를 설정한다.
sequence(t)는 현재 플레이어의 판단한다.
- 테이블에서의 게임이 진행 중인지를 판단하는 finish(t) 스키마를 정의한다.
구 현
구현은 Java를 기반으로 구현하였다. 게임트리를 구현하기위해 결정트리 자료구조를 만든다. 이 자료형은 기존의 트리와 흡사하지만 각 노드마다 노드의 성분을 평가한 평가값이 들어있다. 결정하기 위한 알고리즘으로 알파베타 가지치기를 이용하였다. 알파베타 가지치기는 minMax 알고리즘의 일종으로, minMax알고리즘보다 더욱 더 경제적으로 설계되어있다.
• Table 클래스 : 3목게임을 관리하고 여러 가지 기능을 가진 클래스이다. 플레이어
가 가장 최근에 둔 자리를 저장해 놓는다거나, 기본적으로 진행되는 게임의 룰을
구현해놓았다.
public class Table{
private int [] t;
// 플레이어에 대한 값을 상수로서 정의한다.
public static final int player1 = 1;
public static final int player2 = -1;
// 최근에 놓은 값
private int latestPosition;
// 차례를 알 수 있는 논리값.
private boolean sequence;
// 생성자
public Table(){
t = new int[9];
latestPosition = -1;
sequence = true;
}
public Table(Table t){
this.t = t.t.clone();
latestPosition = t.getLatestPosition();
sequence = t.getSequence();
}
// 최근에 둔 수의 좌표를 가져온다
public int getLatestPosition() {
return latestPosition;
}
// 자리를 마크하는 setMark() 메소드
public Table setMark(int x, int y){
int coord = x + (3 * y);
if(x < 3 && y < 3)
if(t[coord] == 0){
latestPosition = coord;
t[coord] = sequence?player1:player2;
// 시퀀스값에 XOR 연산을 하여 수를 둘때마다 값이 변할수 있게 하였다.
sequence ^= true;
}
// 자리를 점한 테이블을 리턴한다.
return this;
}
// 오버로딩하여 2차원을 1차원으로 컨버팅 할 필요없게하였다.
public Table setMark(int coord){
if(coord < 9 && t[coord] == 0){
latestPosition = coord;
t[coord] = t[coord] = sequence?player1:player2;
sequence ^= true;
}
return this;
}
// 각종 멤버변수 접근 메소드, 테이블 상태를 확인하는 메소드
public boolean getSequence(){
return sequence;
}
public int whoseTurn(){
return sequence?player1:player2;
}
public boolean isFull(){
for(int i : t) if(i == 0) return false; return true;
}
// 승자를 확인하는 메소드
public boolean isWinner(int player){
if(t[0] == player && t[1] == player && t[2] == player ||
t[3] == player && t[4] == player && t[5] == player ||
t[6] == player && t[7] == player && t[8] == player ||
t[0] == player && t[3] == player && t[6] == player ||
t[1] == player && t[4] == player && t[7] == player ||
t[2] == player && t[5] == player && t[8] == player ||
t[0] == player && t[4] == player && t[8] == player ||
t[2] == player && t[4] == player && t[6] == player)
return true;
else
return false;
}
public int getTableCount(){
int counter = 0;
for(int i=0; i<9; i++)
if(t[i] != 0)
counter++;
return counter;
}
public boolean isFinished(){
return (isFull() || isWinner(player1) || isWinner(player2));
}
public int [] getTableArray(){
return t;
}
// 모니터링을 편리하게 하기 위해 toString()을 오버로딩하였다.
public String toString(){
String temp = super.toString();
temp += "[tableCount=" + getTableCount() + "]";
temp += "[sequence=" + (sequence?"O":"X") +"]";
temp += "[isFull=" + isFull() +"]";
temp += "[isFinished=" + isFinished() +"]";
for(int i = 0; i < 9; i++){
if(i%3 == 0)
temp += "\n";
temp += ((t[i]==player1)?"O":(t[i]==player2)?"X":"-") + " ";
}
return temp;
}}
• TableState 클래스 : State를 상속하여 테이블의 상태를 평가하고 평가값을 저장해 놓는다. 이 TableState를 기반으로 인공지능이 다음수를 예상하고 결정할 수 있게 구현된다.
※ 테이블의 상태 평가 함수의 구현
테이블의 각 말들이 가진 위치와 잠재적인 다음 수에 대한 이득을 카운트하여
양 플레이어가 가진 가중치의 비를 만들어 -1 ~ 1 사이의 평가값을 가지게 설정
하였다. 각 평가마다 설정된 가중치는 휴리스틱하게 설정되었다.
수식 > p1 = 플레이어1의 가중치, p2 = 플레이어2의 가중치
eval(t) = (p2 + p1) == 0 ? 1 : ((p1 / p2 + p1) * 2) - 1
위 함수를 보면 분자에 p1만이 올라와있는데 이는 minMax알고리즘을 이용할 때, p1측이 Max값이 되도록 유도를 하기 위함.
분모가 0인지를 판단하여 0일 경우 무한대가되므로 1로 설정한다.
• 평가 0
자신의 차례일 때 가중치를 주어 서로 똑같은 상황일 때 자신에게 더
이득이 되는 평가값을 갖게 한다.
• 평가 1
테이블에 있는 플레이어의 마크가 속한 줄(가로, 세로, 대각선)에 다른
플레이어가 있는지, 자리가 비어있는지를 판단하여 가중치를 설정한다.
- 테이블에 플레이어가 속한 줄들에 다른 플레이어와 같이 있는 경우
가중치를 주지 않는다.
• 평가 2
테이블에 플레이어가 속한 줄에 자신만이 속해있고 자리가 2개 이상이면서,
현재 차례가 해당 플레이어일 경우 결정타를 줄 수 있도록 높은 가중치를
설정한다.
• 평가 3
승자는 플레이어에 따라 Max값 또는 min값을 가지게 한다.
• 초기에 가중치는 0으로 설정되어선 안 된다. 초기 가중치를 0으로 설정하게되면,
승리하거나 패배 하였을 때의 평가값이 산출되므로 초기화는 선수를 두는
플레이어는 1로 후수를 두는 플레이어는 2로 설정하도록 하였다.
- 평가에 대한 공정성을 유지하기 위함.
• 평가함수 구현에 사용된 메소드
// 가중치를 초기화하는 메소드
public void initEval(){
p1Bonus = 1;
p2Bonus = 2;
}
public int evaluate(Table t, int player){
// Table클래스로부터 int형의 테이블을 가져온다.
int [] table = t.getTableArray();
// 가중치값
int bonus = 0;
// 같은줄에 같은 마크가 2개 있는지 검사하기위한 카운터
int c;
// 가로 세로줄 대각선에서 자리가 비어있는지를 확인하기 위한 논리값.
boolean b;
// 속한 줄에서의 상대가 방해하고 있는지 확인하기 위한 논리값
boolean interrupt;
// x좌표 루프
for(int x = 0; x < 3; x++)
// y좌표 루프
for(int y = 0; y < 3; y++){
// 2차원배열을 1차원으로 변환하였을때의 좌표
int coord = x + 3 * y;
// 테이블값이 플레이어가 점하고 있을때
if(table[coord] == player){
// 확인하기 위한 변수값들을 초기화한다.
b = true; interrupt = false; c = 0;
// 3목이므로 해당하는 3칸에 대한 루프를 발생시킨다. 가로줄에 속한 값들을 확인하는 루프
for(int i = 0; i < 3; i++){
// 조건값이다. 논리값을 설정할 때 좌표에대한 계산을 최소화하기 위해 변수로 설정.
int cond = table[((x + i) % 3) + 3 * y];
// 방해받고 있는지 확인하는 논리연산이다.
interrupt |= (cond != 0) && (table[coord] != cond);
// 같은줄에서의 자리에 대한 가중치를 설정하기 위한 논리연산이다.
b &= (player == cond) || (cond == 0);
// 같은줄에 자신과 똑같은 마크가 있을 때 카운터를 1 증가시킨다.
c += table[coord] == cond ? 1 : 0;
}
// 만약 같은줄에대해 빈자리만이 있을 때 가중치를 1 증가시킨다.
if(b) bonus += 1;
// 방해받고있지 않고 카운터가 2 이상이며 자신의 차례일 때, 결정타를 주기위한 가중치를
// 4 증가시킨다.
if(!interrupt && c == 2 && t.whoseTurn() == player) bonus += 4;
// 검사를 끝내고 변수들을 초기화한다.
b = true; interrupt = false; c = 0;
// 세로줄에 대한 검사도 똑같이 실시한다.
for(int i = 0; i < 3; i++){
int cond = table[x + 3 * ((y + i) % 3)];
interrupt |= (cond != 0) && (table[coord] != cond);
b &= (player == cond) || (cond == 0);
c += table[coord] == cond ? 1 : 0;
}
if(b) bonus += 1;
if(!interrupt && c == 2 && t.whoseTurn() == player) bonus += 4;
// 왼쪽 위에서 오른쪽 아래로 향하는 대각선에 대한 검사를 실시한다.
if(coord == 0 || coord == 4 || coord == 8){
for(int i = 0; i < 3; i++){
int cond = table[i * 4];
interrupt |= (cond != 0) && (table[coord] != cond);
b &= (player == cond) || (cond == 0);
c += table[coord] == cond ? 1 : 0;
}
if(b) bonus += 1;
if(!interrupt && c == 2 && t.whoseTurn() == player) bonus += 4;
}
b = true; interrupt = false; c = 0;
// 오른쪽 위에서 왼쪽 아래로 향하는 대각선에 대한 검사를 실시한다.
if(coord == 2 || coord == 4 || coord == 6){
for(int i = 0; i < 3; i++){
int cond = table[(coord + 2 * i) % 6 == 0 ? 6 : (coord + 2 * i) % 6];
interrupt |= (cond != 0) && (table[coord] != cond);
b &= (player == cond) || (cond == 0);
c += table[coord] == cond ? 1 : 0;
}
if(b) bonus += 1;
if(!interrupt && c == 2 && t.whoseTurn() == player) bonus += 4;
}
b = true; interrupt = false; c = 0;
}
}
return bonus;
}
// 최종 평가모듈이다.
public void evaluate(){
// p1과 p2에 대한 가중치를 초기화한다.
initEval();
if(table != null){
// 승자에 대한 평가값을 설정한다.
if(table.isWinner(Table.player1)){
p1Bonus = 1;
p2Bonus = 0;
}else if(table.isWinner(Table.player2)){
p1Bonus = 0;
p2Bonus = 1;
// 만약 테이블이 꽉 찼을때는 무승부를 위한 평가값 0을 설정한다.
}else if(t.isFull())
eval = 0;
// 평가를 실시한다.
}else{
// 각 플레이어에 대한 가중치를 설정한다.
p1Bonus += evaluate(table, Table.player1);
p2Bonus += evaluate(table, Table.player2);
// 플레이어의 차례에 대한 가중치를 부여한다.
if(table.whoseTurn() == Table.player1)
p1Bonus++;
else
p2Bonus++;
}
// 최종 평가값을 계산한다.
eval = (p2Bonus + p1Bonus) == 0 ? 1 :
(((double)p1Bonus / ((double)p2Bonus+(double)p1Bonus)) * 2) - 1;
}
}
• minMax 알고리즘의 구현
// 현재 가능한 수들의 좌표를 구하는 메소드이다
public int [] getPossibleMove(Table t){
int [] move;
int [] table = t.getTableArray();
int counter = 0;
// 빈 공간을 확인하면서 카운터를 증가시킨다.
for(int i = 0; i < 9; i++)
if(table[i] == 0)
counter++;
// 빈 공간 만큼의 배열을 생성한다.
move = new int[counter];
// 카운터 초기화
counter = 0;
// 루프를 발생시켜 빈공간인 좌표를 등록한다.
for(int i = 0; i < 9; i++)
if(table[i] == 0){
move[counter] = i;
counter++;
}
return move;
}
// 차례에 놓을 수 있는 수들에 대한 상태들을 리스트로 리턴하는 메소드이다.
public ArrayList<TableState> getNextMove(){
ArrayList<TableState> list = new ArrayList<TableState>();
// 빈공간인 좌표들을 확인하고
int [] possibleMove = getPossibleMove(t);
for(int i = 0; i < possibleMove.length; i++){
Table temp = new Table(t);
// 빈 공간으로 수를 둔 후에
temp.setMark(possibleMove[i]);
// 예상한 수에 대한 상태를 리스트에 추가한다.
list.add(new TableState(temp));
}
return list;
}
// minMax알고리즘으로 구성된 메소드 최적의 수를 둔 상태를 리턴한다.
public TableState getBestPosition(ArrayList<TableState> list){
// 처음 테이블상태는 예상 수들 중 아무거나 선택하게된다.
TableState bestPos = list.get((int)(Math.random() * list.size()));
for(TableState b : list){
// 시퀀스 값에 따라 최대, 최소 평가값을가진 상태를 bestPos로 지정한다.
if(t.getSequence())
bestPos = b.getEval() > bestPos.getEval() ? b : bestPos;
else
bestPos = b.getEval() < bestPos.getEval() ? b : bestPos;
}
return bestPos;
}
// 테스트 모듈
public static void main(String [] args){
Table t = new Table();
TableState temp = new TableState(t);
// 게임 테이블이 끝났는지를 확인하면서 루프를 발생
while(!t.isFinished()){
temp.getEval();
// 게임의 상태를 출력한다
System.out.println(temp + "\n");
// 컴퓨터가 최적의 위치를 결정하고 수를 둔다.
t.setMark(temp.getBestPosition(temp.getNextMove()).getTable().
getLatestPosition()
);
}
}• 실행 결과(minMax 알고리즘으로 구성된 인공지능 플레이어 두 명이 게임을 진행)
TableState33263331 Table@61de33[tableCount=0][sequence=O][isFull=false][isFinished=false]
- - -
- - -
- - -
[eval=0.0] [p1=2] [p2=2]
TableState33263331 Table@61de33[tableCount=1][sequence=X][isFull=false][isFinished=false]
- - -
- O -
- - -
[eval=0.25] [p1=5] [p2=3]
TableState33263331 Table@61de33[tableCount=2][sequence=O][isFull=false][isFinished=false]
X - -
- O -
- - -
[eval=0.11111111111111116] [p1=5] [p2=4]
TableState33263331 Table@61de33[tableCount=3][sequence=X][isFull=false][isFinished=false]
X - -
- O -
O - -
[eval=0.19999999999999996] [p1=6] [p2=4]
TableState33263331 Table@61de33[tableCount=4][sequence=O][isFull=false][isFinished=false]
X - X
- O -
O - -
[eval=0.0] [p1=5] [p2=5]
TableState33263331 Table@61de33[tableCount=5][sequence=X][isFull=false][isFinished=false]
X O X
- O -
O - -
[eval=0.11111111111111116] [p1=5] [p2=4]
TableState33263331 Table@61de33[tableCount=6][sequence=O][isFull=false][isFinished=false]
X O X
- O -
O X -
[eval=0.0] [p1=3] [p2=3]
TableState33263331 Table@61de33[tableCount=7][sequence=X][isFull=false][isFinished=false]
X O X
- O O
O X -
[eval=0.0] [p1=3] [p2=3]
TableState33263331 Table@61de33[tableCount=8][sequence=O][isFull=false][isFinished=false]
X O X
X O O
O X -
[eval=0.0] [p1=2] [p2=2]
TableState33263331 Table@61de33[tableCount=9][sequence=X][isFull=true][isFinished=true]
X O X
X O O
O X O
[eval=0.0] [p1=2] [p2=2]
'AI' 카테고리의 다른 글
Neural Network - 기계학습(machine learning) (0) | 2011.10.26 |
---|---|
인공지능에서의 스키마(Schema) (0) | 2011.04.24 |
용어정리.. (0) | 2010.07.19 |
SOM 구현 가중치 설정 가우스분포, 정규분포 (0) | 2010.06.22 |
정리 - 인공 신경망 (1) | 2010.03.26 |