모바일 맞고 게임8-1(간단한 A.I. 첨가 #1 - 기본 게임 진행 기능 구현)
DevSource/맞고 2012. 12. 27. 22:18 |지난 시간까지 완성한 소스 만으로도 어느 정도의 맞고 게임 진행은 가능했었습니다.
이번 시간에는 지금까지 만든 게임에 A.I.를 추가해서 좀더 그럴듯 한 게임을 만들어보도록 하지요.
A.I. 라는 것이 정말 세밀하고 복잡하게 만들 자면, 한도 끝도 없는 분야입니다.
그러나 단순한 맞고 게임에서 그 정도의 세밀한 A.I.까지 사용할 필요는 없겠지요.
간단하게 만들자면, 그냥 우리가 맞고 게임을 할때 생각하는 과정을 그대로 코딩해 주면 되겠죠?
즉, 예를 들자면 이러한 경우가 있을 수 있겠네요.
바닥에 쌍피인 똥과 일반피인 똥이 있을때, 일반적인 경우라면 일부러 일반 피를 먹을 일은 없겠죠?
당연히 쌍피를 먹어야 합니다.
또, 이런 경우도 있을 수 있습니다.
국진 패를 먹었습니다. 그렇다면 국진 패를 열끗으로 써야 할까요, 쌍피로 써야 할까요?
쌍피가 좋으니까 그냥 무조건 쌍피로 써야 할까요? 아니면 무조건 열끗으로?
이건 그냥 함부로 결정할 수 있는 문제는 아니겠죠.
그때 그때의 상황에 따라서 적절한 판단을 해 주어야 합니다.
즉, 열끗이 5장 이상 있다면, 멍따의 가능성이 있으므로, 열끗으로 사용해주는 것도 좋겠죠.
그러나 열끗이 몇장 없다면, 쌍피로 사용한다면 피 두장과 같은 효과를 가지므로 더 좋겠네요.
이와 같은 알고리즘을 Alpha-Beta Search라고 하지요.
Alpha-Beta Search는 경우의 수를 나열 해 놓고 좀 더 낳은 선택을 고르는 방식이죠.
Alpha-Beta Search는 일단 알고리즘이 간단하고, 쉽게 구현할 수 있다는 장점이 있습니다.
그러나 A.I.를 똑똑하게 하려면, 모든 상황을 전부 다 나열해야 하므로, 소스 코드가 길어지고,
모든 상황을 전부 나열한다고 해도 언제나 그 상황에서 예외는 있기 마련이므로,
어느 한계점에 이르면 더 이상 A.I.가 똑똑해지기 힘들다는 단점들이 있죠.
그러나, 소스 코드 한줄을 추가하는 것만으로도 컴퓨터가 확연히 똑똑해지는 것이 눈에 보이고,
간단한 코딩만으로도 꽤 똑똑한 A.I.를 구현해 낼 수가 있으므로, 주로 보드게임에서 많이 사용되죠.
자, 이제 본격적으로 지난 시간까지 만들었던 소스에 A.I. 코드를 추가해 보도록 하죠.
A.I.를 실행 하려면 게임 진행 중에 어느 시점에서 호출 해 주는 것이 가장 좋을까요?
아무래도 A.I.는 컴퓨터의 턴에 실행이 되는 것이므로,
당연히 턴을 변경한 직후에, A.I.를 호출 해 주는 것이 좋겠지요?
twogo1p.GameView 클래스의 턴을 변경해 주는 함수인 changeTurn() 함수에서 A.I.를 호출해 줍시다.
☆ ☆ ☆ ☆ ☆ ☆ |
/** * 턴을 변경한다. */ private void changeTurn() { // 쓸 체크. if(checkSseul() == true) { curViewState = State.GAME_SHOWMESSAGE; curMsgState = CyDialog.MSG_SSEUL; repaints(); Sleep.wait(this, 300); curViewState = State.GAME_RUNNING; repaints(); } // 턴 변경. curTurn = (curTurn==User.PLAYER)? User.COMPUTER : User.PLAYER; if(curTurn == User.COMPUTER) { do { runAI(); } while(agent.wasAdditionalCard() == true); } } |
자, 새롭게 추가된 부분(☆)만 살펴 보도록 하겠습니다.
턴을 변경을 하고 난 후에, 변경된 턴이 컴퓨터의 턴이라면, runAI() 라는 함수를 호출하고 있습니다.
그런데 여기서 runAI()는 AI를 실행해 주는 함수라고 하지만,
runAI() 함수가 do ~ while() 안에서 루프를 돌고 있습니다.
while()문의 조건을 보면, agent라는 클래스의 wasAdditionalCard() 라는 함수를 호출하는데요.
agent 클래스는 A.I.를 담당하는 함수들을 모아놓은 클래스이고, 잠시 후에 살펴 볼 것입니다.
그리고 wasAdditionalCard() 함수는 A.I.가 방금 낸 패가 추가 쌍피였는지를 리턴합니다.
즉, 추가 쌍피를 내면, 중앙의 패를 뒤집어 한장을 갖고, 한번 더 내야 하죠?
그렇기 때문에, 추가피를 냈다면, 루프를 돌며 추가피가 아닌 패를 낼때까지 A.I.를 호출하는 것이죠.
그 다음으로 방금전에 열심히 루프를 돌며 호출했던 runAI() 함수를 한번 보도록 하지요.
/**
* A.I. 실행.
*/
public void runAI()
{
System.out.println("A.I. 수행 시작.");
throwCard( agent.run(room) );
room.sortMonth();
repaints();
} |
twogo1p.GameView 클래스의 runAI() 함수는 단순히, agent.run() 함수를 호출해주는 것 뿐입니다.
agent.run() 함수는 컴퓨터가 들고 있는 10장의 카드 중에서 낼 카드의 번호를 리턴해 줍니다.
그러므로 agent.run() 함수가 리턴한 결과 값을 throwCard() 함수에 인자로 넘겨주면,
agent.run() 함수를 호출 후 선택된 카드를 바닥에 내 주겠죠.
이제 앞에서 몇번 언급이 되었던 A.I.를 담당하는 Agent 클래스를 만들어 봅시다.
/**
* A.I. 수행.
*/
public byte run(Room room)
{
byte result;
wasAdditionalCard = false;
System.out.println("A.I. 수행하고 있음...");
this.room = room;
|
run() 함수에서는 Room 클래스를 인자로 받고 있는데,
Room 클래스의 멤버들은 카드를 한번 낼때마다 각각의 데이터가 계속 변하므로,
A.I.를 수행 할때마다 업데이트 된 Room 클래스를 받아 와야 하겠지요.
일단 run() 함수의 전체적인 A.I.의 흐름만을 본다면 별것이 없습니다.
컴퓨터가 들고 있는 패 중에서, 서비스 패가 있다면, 서비스 패를 내고, 중앙의 패를 한장 먹습니다.
그러나, 컴퓨터가 들고 있는 패 중에 추가 서비스 패가 없다면,
광이 있는지를 체크하고, 광이 없다면, 비광이 있는지 체크를 하고,
일반 광과 비광이 없다면, 고도리, 청단, 홍단, 초단, 등을 체크하고,
그 다음은 쌍피, 피 순으로 체크를 해 줍니다.
위의 상황에 아무것도 해당되는 것이 없다면, 들고 있는 카드 중에서 아무거나 바닥에 내는 것이죠.
가장 먼저 컴퓨터의 패 중에서 추가 서비스 패가 있는지를 체크해서, 서비스 패의 인덱스를 리턴해주는,
findAdditionalCard() 함수를 한번 보도록 하겠습니다.
/**
* 추가 쌍피가 있는지 체크.
* @return
*/
private byte findAdditionalCard()
{
byte i;
/* 서비스 패 체크 */
for(i=0; i<room.numOfComputerCard; i++)
{
// 서비스 패라면,
if( room.isAdditionalCard(room.getUserHand(Room.COMPUTER, i)))
{
return convertIndex(i);
}
}
return -1;
} |
findAdditional() 함수는 컴퓨터의 패 중에서 추가 쌍피가 존재하는지를 체크 합니다.
numOfComputercard 는 컴퓨터가 몇개의 카드를 가지고 있는지를 기억하고 있는 변수입니다.
컴퓨터가 가지고 있는 카드의 수만 큼 루프를 돌아서 추가 쌍피라면, 해당 인덱스를 리턴합니다.
그런데 i가 인덱스인데 convertIndex()라는 함수를 거쳐서 리턴을 해 주고 있는데요.
/**
* 게임 설정에 맞게 인덱스를 변경한다.
* @param convertIdx
* @return
*/
private byte convertIndex(byte convertIdx)
{
/*
* 손에 든 카드의 인덱스는, 0~9가 아니라, 1~9, 0이므로,
* 내기 전에 렌덤하게 선택된 인덱스를 변경해 준다.
*/
if(room.isAdditionalCard(room.getUserHand(Room.COMPUTER,convertIdx)))
{
wasAdditionalCard = true;
}
convertIdx = (++convertIdx == 10)? 0 : convertIdx;
return convertIdx;
}
|
카드 배열은 0~9까지로 되어 있지만, 실제 throwCard에서는 1~9, 0과 같이 계산을 하므로,
0~9를 1~9, 0으로 변환 해 준 뒤에 리턴을 해야 하겠죠?
convertIndex() 함수가 바로 그 작업을 해줍니다.
만약 해당 카드가 서비스 패라면, 추가 서비스 패를 냈었다는 것을 wasAdditionalCard에 기억해둡니다.
그럼 다음으로 findCard() 함수를 볼까요?
findCard() 함수는 현재 컴퓨터의 패 중에서 해당 패가 있는지,
혹은, 이 패를 내면 바닥에서 해당 패를 먹을 수 있는지를 체크 합니다.
/**
* 해당 패를 먹을수 있는지 체크.
* @return
*/
private byte findCard(byte cardType)
{
byte i, j, k;
/* 패 체크 */
for(i=0; i<room.numOfComputerCard; i++)
{
// 낼수 있는 패라면,
if(room.compareFloor(room.getUserHand(Room.COMPUTER, i)) != -1)
{
// 낼수 있는 패가 인자로 받은 Card Type 이라면,
if(gCard.getType(room.getUserHand(Room.COMPUTER, i))
== cardType)
{
return convertIndex(i);
}
// 바닥 검사.
for(j=0; j<12; j++)
{
if(room.getMonth(j)[0] == room.CARD_NULL)
continue;
// 같은 종류의 패가 있는 월을 찾았다면,
if( gCard.getType(room.getMonth(j)[0])
== gCard.getType(room.getUserHand(Room.COMPUTER, i)))
{
for(k=0; k<4; k++)
{
if(room.getMonth(j)[k] == room.CARD_NULL)
break;
// 해당 월에 해당 Card Type이 있다면,
if(gCard.getType(room.getMonth(j)[k]) == cardType)
{
return convertIndex(i);
}
}
break;
}
}
}
} // for(i=0; i<room.numOfComputerCard; i++).
return -1;
} |
낼 수 없는 패는 체크 할 필요도 없겠죠. 따라서 낼 수 있는 패만을 검사합니다.
낼 수 있는 패라면, 해당 패가 인자로 받은 cardType과 동일한 카드 타입인지 체크한 후,
같다면 해당 카드의 인덱스를 리턴하고, 카드 타입이 일치하지 않는다면, 바닥을 검사해서,
바닥에 cardType과 동일한 카드타입의 카드가 존재하는지를 체크해서 있다면 인덱스를 리턴하죠.
결국, cardType과 동일한 종류의 카드를 들고 있거나, 해당 카드를 먹을 수 있는지를 체크하는거죠.
그런데 고도리, 청단, 홍단, 초단을 체크할때에는 조건이 조금 많죠?
/* 고도리 체크 */ if(bNoFindGodori == false || (room.getCntGodori(Room.PLAYER) >= 2 && room.getCntGodori(Room.COMPUTER) == 0)) { if(room.getCntGodori(Room.PLAYER) >= 1) { bNoFindGodori == true; } |
가장 먼저 bNoFind... 로 시작되는 boolean 변수를 체크하죠.
bNoFind... 변수들은 고도리, 청단, 홍단, 초단 패를 검색할 필요가 있는지를 체크하는 변수입니다.
예를 들어, 사용자가 이미 고도리 패를 가지고 있다면, 고도리 패는 이제 그냥 일반 열끗일뿐이죠.
따라서, bNoFindGodori 가 false라고 해도, 사용자가 한장이라도 고도리 패를 가지고 있다면,
고도리 패를 애써 먹으려고 노력할 필요는 없으므로 먹지 않을 수 있는 것이죠.
그러나 비록 사용자가 고도리 패를 가지고 있다고 해도, 사용자가 고도리를 할 위기에 처했다면,
고도리 패를 먹음으로써, 사용자의 고도리를 막아야 하겠지요.
만약 플레이어의 고도리가 2장인데, 우리가 한장도 가지고 있지 않다면, 그 패를 어서 먹어야 하겠지요.
청단, 홍단, 초단도 모두 마찬가지의 상황을 체크 하고 있습니다.
// 위의 상황에 해당 하는 것이 없다면, |
run() 함수의 가장 마지막 부분입니다.
위에서 광, 청단, 홍단, 초단, 그리고 쌍피와 피 등등의 체크 과정을 모두 거쳤는데도
위의 상황에 아무것도 해당 되는 것이 없다면, 그냥 무작정 아무 패나 낼 수는 없고,
가능하다면 아무거나 패를 획득 할 수 있는 패를 내 주어야 하겠죠.
역시 마찬가지로 루프를 돌면서 먹을수 있는 패가 있다면, 그것을 내고,
아무것도 먹을 수 있는 것이 없다면, 렌덤하게 아무 카드나 내 줍니다.
사실 이렇게 처리를 해주면, 먹을게 없을때, 광이나 쌍피와 같이 중요한 카드를 내버리는 경우가 있겠죠.
물론 이러한 경우를 대비한 상황도 처리를 해 주어야 하지만, 강좌가 길어지므로, 일단은 생략했습니다.
그러나 이 부분은 뭐 그렇게 까다로운 부분도 아니고 해서 소스 코드에만 첨가를 시켜 놓았으므로,
소스 코드를 참조하시면 되리라고 봅니다.
이걸로 어느 정도 간단한 A.I.는 구현이 되었습니다. A.I.를 좀더 향상 시키고 싶다면,
각종 상황을 좀더 추가를 시켜 주면 되겠죠?
그러나 아직, 흔듬 여부 선택이나, 국진의 사용 여부 선택, 고/스톱 선택 등등의 상황에서
컴퓨터가 상황에 맞게 선택하는 기능이 구현되어 있지 않네요.
'DevSource > 맞고' 카테고리의 다른 글
모바일 맞고 게임9(사운드 출력과 진동) (0) | 2012.12.27 |
---|---|
모바일 맞고 게임8-2(간단한 A.I. 첨가 #2 - 상황 선택 기능 구현) (0) | 2012.12.27 |
모바일 맞고 게임7-2(디스플레이 요소 추가 #2) (0) | 2012.12.27 |
모바일 맞고 게임7-1(디스플레이 요소 추가 #1) (0) | 2012.12.27 |
모바일 맞고 게임6-3(추가 규칙과 벌칙, 그리고 결과 출력 구현 ) (0) | 2012.12.27 |