이제 플레이어와 컴퓨터에게 모두 카드를 나누어 주었습니다.

이제 남은건 플레이어가 카드를 내면, 바닥의 패 중에 같은 종류의 카드가 있는지 비교해서,

같은 종류의 카드가 있다면 먹고 없다면 그냥 바닥에 버리는 작업이 필요하겠네요.


같은 종류의 카드인지 비교하는 건 카드 인덱스를 4로 나누워 주면 된다고, 방금 말씀드렸습니다.

카드를 비교하는 방법을 알았으니, 카드를 내고 뒤집는 동작만 구현하면 되겠네요.


카드를 내는 작업도 후다닥 해치워 버릴 수도 있겠지만,

그렇게 하면, 플레이어가 자신이 무슨 카드를 먹었는지, 컴퓨터가 뭘 먹었는지 분간을 할수가 없겠죠?

카드를 내는 것 역시, 일정 delay가 지난 후에 다음 동작을 수행하도록 할 필요가 있겠네요.

GameView 클래스의 키입력 부분에 다음과 같이 추가해줍니다.

/**
 * 키 입력.
 */
public void keyPressed(int keyCode)
{
    switch(keyCode)
    {

/**
*
* ... 생략 ...
*
*/

default: // 그 외. short numKey = (short)(keyCode - 48); if(numKey >= 0 && numKey <= 9) // 숫자키가 눌렸다면. { // 게임 진행중이라면, if( curViewState == State.GAME_RUNNING ) { throwCard(numKey); room.sortMonth(); System.out.println("가운데... = " + room.getNumOfCenterCard()); }
}
}
}


해당 숫자를 누르면, 카드를 낼 것이므로, 숫자 입력 부분에다가 카드를 내는 동작을 구현해 주어야 하겠지요?

case KEY_NUM0:
.
.
.

case KEY_NUM9

이렇게 지정해 주어도 되겠지만, 편의상, 함수호출하기도 쉽게, default에다가 구현을 했습니다.

숫자 0의 ASCII CODE 값은 48 이므로, 0이 눌렸을때 keyCode에서 48을 빼면, 그 값이 0이 되겠지요.

1, 2, 3, 4 .... 모두 마찬가지로, 해당 키코드에서 48을 빼면 그에 해당하는 정수 값을 얻어 올수 있습니다.

만약, 0 에서 9까지의 숫자키가 눌렸고, 게임이 진행중이라면, 해당 카드를 내라는 뜻이겠죠.


throwCard 함수에 누른 번호를 넘겨 주면, 해당 번호의 카드가 바닥에 내어 질 것입니다.

카드를 낸 후에는 다시 sortMonth() 함수를 호출해서 각 월의 카드를 정렬해 주고 있습니다.


자, 그럼 throwCard 함수를 보도록 할까요.

/**
 * 카드를 낸다.
 * @param numKey
 */
private void throwCard(int numKey)
{
    byte handCard, reverseCard;
    int handCardIdx, reverseCardIdx;

    numKey = (numKey == 0)? 9 : (short)(numKey - 1);

    handCard = room.getUserHand(curTurn, numKey);

    // 누른 번호의 슬롯에 카드가 없다면,
    if(room.getUserHand(curTurn, numKey) == room.CARD_NULL)
    {
        System.out.println(numKey + "번 카드는 존재하지 않음.");
        // 그냥 종료.
        return;
    }

    // 추가 카드라면,
    if( room.isAdditionalCard(handCard) )
    {
        // 해당 피를 먹고,
        room.addPee(curTurn, handCard);
        // 가운데서 새로운 피를 가져옴.
        room.setUserHand(curTurn, numKey, room.getTopCard());
        repaints();
        // 그냥 종료.
        return;
    }
    else // 아무것도 아니라면,
    {
        // 손에 든 카드를 냄.
        handCardIdx = room.throwCard(curTurn, handCard);
        // 낸 자리의 카드 삭제.
        room.setUserHand(curTurn, numKey, room.CARD_NULL);
        // 빈 공간을 메꿈.
        room.deleteEmptySpace(curTurn);
        repaints();
        Sleep.wait(this, 300);
    }

    // 중앙의 카드를 뒤집음.
    reverseCard = room.getTopCard();
    System.out.println("뒤집은 카드는 : " + reverseCard);

    do {
        // 추가 카드라면,
        if(room.isAdditionalCard(reverseCard))
        {
            // 해당 피를 플레이어가 먹고,
            room.addPee(curTurn, reverseCard);
            // 다시 한장 뒤집고, 체크.
            reverseCard = room.getTopCard();
            repaints();
            Sleep.wait(this, 300);
        }
        else // 추가 카드가 아니라면,
        {
            // 뒤집은 카드를 낸다.
            reverseCardIdx = room.throwCard(curTurn, reverseCard);
            repaints();
            Sleep.wait(this, 300);
            break;
        }
    } while(true);

    // 손에서 냈던 카드와 뒤집어서 낸 카드가 같은 슬롯에 있다면,
    if(handCardIdx == reverseCardIdx)
    {
        int i, j;
        // 뻑 체크.
        for(i=0; i<4; i++)
        {
            if(room.getMonth(handCardIdx)[i] == room.CARD_NULL)
            {
                switch(i)
                {
                case 2: // 손에서 낸 카드와, 뒤집어 낸 카드 둘뿐이라면,
                    // 두장 다 먹는다.
                    room.addPae(curTurn, room.getMonth(handCardIdx)[0]);
                    room.addPae(curTurn, room.getMonth(handCardIdx)[1]);
                    room.getMonth(handCardIdx)[0] = room.CARD_NULL;
                    room.getMonth(handCardIdx)[1] = room.CARD_NULL;

                    repaints();
                    break;
                case 3: // 손에서 낸 카드와, 뒤집어 낸 카드와 원래 카드 한장.
                    System.out.println(" # Msg : 뻑!!");
                    // 아무것도 먹지 못하고 턴을 변경.
                    break;
                }
            } // end if(room.getMonth(handCardIdx)[i] == room.CARD_NULL).
            else
            {
                if(i == 3)
                {
                    // 모두 먹는다.
                    for(j=0; j<4; j++)
                    {
                        room.addPae(curTurn, room.getMonth(handCardIdx)[j]);
                        room.getMonth(handCardIdx)[j] = room.CARD_NULL;
                    }
                }
            }
        }
    }
    else // 손에서 낸 카드와 뒤집어 낸 카드가 다른 슬롯에 있다면,
    {
        room.processCard(curTurn, handCardIdx);
        room.processCard(curTurn, reverseCardIdx);

        if(room.bSelectTwoCard) // 두장의 카드중 선택해야 한다면,
        {
            curViewState = State.GAME_SELECTTWOCARDS;
            repaints();
            return;
        }
    }

    curTurn = (curTurn==User.PLAYER)? User.COMPUTER : User.PLAYER;

    repaints();
}

게임을 실행했을때 숫자를 출력하는 순서가 다음과 같죠.

 

카드의 번호는 0, 1, 2, .... , 8, 9. 와 같이 0 부터 9까지 이지만,

실제 게임 상에 출력되는 숫자는, 1, 2, 3, ... , 9, 0 과 같이 1 부터 0 까지입니다.

즉, 0번을 눌렀을때, 0번 카드를 내면 이상한 결과가 나오겠지요.

0번을 누르면, 실제 카드 인덱스에서는 9번 카드를 내야 합니다.


그래서 가장 첫줄에서 numKey를 다음과 같이 변경해 줍니다.

numKey = (numKey == 0)? 9 : (short)(numKey - 1);

numKey가 0이라면 9번 카드를 뒤집고, 아니라면 numKey에서 1을 뺀 인덱스의 카드를 뒤집어라.

뭐 이런 뜻이 되겠네요.


그 뒤, room 클래스의 getUserHand() 함수를 이용해서, 해당 슬롯의 카드 인덱스를 얻어 온 후,

만약 이 카드가 CARD_NULL 이라면, 빈 공간을 선택한 것이므로, 아무런 처리도 하지 않고 return 해 줍니다.


만약 이 카드가 추가 카드라면,

// 추가 카드라면,
if( room.isAdditionalCard(handCard) )
{
    // 해당 피를 먹고,
    room.addPee(curTurn, handCard);
    // 가운데서 새로운 피를 가져옴.
    room.setUserHand(curTurn, numKey, room.getTopCard());
    repaints();
    // 그냥 종료.
    return;
}
else // 아무것도 아니라면,
{
    // 손에 든 카드를 냄.
    handCardIdx = room.throwCard(curTurn, handCard);
    // 낸 자리의 카드 삭제.
    room.setUserHand(curTurn, numKey, room.CARD_NULL);
    // 빈 공간을 메꿈.
    room.deleteEmptySpace(curTurn);
    repaints();
    Sleep.wait(this, 300);
}

추가카드는 사용자가 먹고, 서비스패가 있던 자리에는 중앙의 카드를 뒤집어 새로운 카드를 가져옵니다.

만약에 추가 카드가 아닌 다른 카드라면, room 클래스의 throwCard()를 호출해 카드를 바닥에 냅니다.

room 클래스의 throwCard() 함수를 한번 보면,

/**
 * 카드를 냄.
 * @param userType
 * @param cardNum
 * @return
 */
public int throwCard(int userType, int cardNum)
{
    for(i=0; i<12; i++)
    {
        // 같은 종류의 카드를 발견했다면,
        if(cardsMonth[i][0]/4 == cardNum/4)
        {
            // 널 카드라면, 다음 루프.
            if(cardsMonth[i][0] == CARD_NULL)
                continue;

            for(j=0; j<4; j++)
            {
                if(cardsMonth[i][j] == CARD_NULL)
                {
                    cardsMonth[i][j] = (byte)cardNum;
                    return i;
                }
            }
        }
    }

    // 한바퀴 돌았지만, 같은 종류의 카드를 발견하지 못했다면,
    for(i=0; i<12; i++)
    {
        // 아무곳이나 빈곳에 카드를 붙임.
        if(cardsMonth[i][0] == CARD_NULL)
        {
            cardsMonth[i][0] = (byte)cardNum;
            return i;
        }
    }

    return -1;
}

이 함수는 루프를 돌다가 같은 종류의 카드를 발견 하면, 그 월의 맨 뒤에 카드를 붙여줍니다.

만약에 같은 종류의 카드를 발견하지 못했다면, 아무곳이나 빈 곳에 카드를 붙여주고요.

만약에 냈다면, 몇월에 카드를 붙였는지를 리턴해 줍니다.


일단은 사용자에게 사용자가 냈다는 걸 보여주기 위해, 어디에다 냈는지만 기억해 둡니다.

그리고 다시 Sleep클래스의 wait() 함수를 이용해서 잠시 시간을 지연 시킵니다.


맞고에서 한 사람이 카드를 내면 다음엔 중앙의 카드를 뒤집어서 또 비교를 하죠.

중앙의 카드를 뒤집었을때 추가 카드가 나왔다면, 사용자가 먹고, 중앙의 카드를 한번 더 뒤집어야합니다.

	do {
        // 추가 카드라면,
        if(room.isAdditionalCard(reverseCard))
        {
            // 해당 피를 플레이어가 먹고,
            room.addPee(curTurn, reverseCard);
            // 다시 한장 뒤집고, 체크.
            reverseCard = room.getTopCard();
            repaints();
            Sleep.wait(this, 300);
        }
        else // 추가 카드가 아니라면,
        {
            // 뒤집은 카드를 낸다.
            reverseCardIdx = room.throwCard(curTurn, reverseCard);
            repaints();
            Sleep.wait(this, 300);
            break;
        }
    } while(true);

그래서 위와 같이 while 루프를 돌면서, 추가 카드가 아닌 것이 나올 때까지, 계속 뒤집어 줍니다.


이제, 손에 들고 있던 카드와 중앙에서 뒤집은 카드를 모두 냈습니다.

손에 들고 있던 카드를 낸 월은 handCard에,

중앙에서 뒤집은 카드를 낸 월은 reverseCard에 저장이 되어있습니다.


이걸 handCard와 reverseCard를 낸 월이 같을때와 같지 않을때로 나누어서 생각해야 합니다.

왜냐 하면, 만약, handCard와 reverseCard가 같은 월에 있다면, 뻑의 위험이 있기 때문이죠.

// 손에서 냈던 카드와 뒤집어서 낸 카드가 같은 슬롯에 있다면,
if(handCardIdx == reverseCardIdx)
{
    int i, j;
    // 뻑 체크.
    for(i=0; i<4; i++)
    {
        if(room.getMonth(handCardIdx)[i] == room.CARD_NULL)
        {
            switch(i)
            {
            case 2: // 손에서 낸 카드와, 뒤집어 낸 카드 둘뿐이라면,
System.out.println(" # Msg : 쪽입니다!"); // 두장 다 먹는다. room.addPae(curTurn, room.getMonth(handCardIdx)[0]); room.addPae(curTurn, room.getMonth(handCardIdx)[1]); room.getMonth(handCardIdx)[0] = room.CARD_NULL; room.getMonth(handCardIdx)[1] = room.CARD_NULL; repaints(); break; case 3: // 손에서 낸 카드와, 뒤집어 낸 카드와 원래 카드 한장. System.out.println(" # Msg : 뻑!!"); // 아무것도 먹지 못하고 턴을 변경. break; } } // end if(room.getMonth(handCardIdx)[i] == room.CARD_NULL). else { if(i == 3) { // 모두 먹는다. for(j=0; j<4; j++) { room.addPae(curTurn, room.getMonth(handCardIdx)[j]); room.getMonth(handCardIdx)[j] = room.CARD_NULL; } } } } }

만약, handCard와 reverseCard가 같을 때,


한 슬롯 안에 카드가 두장 뿐이라면, 쪽이 되겠네요. 지금은 애니메이션 처리는 구현하지 않았으므로,

임시로 println() 함수를 이용해서 debug 창에만 출력을 해 줍니다.


한 슬롯 안에 카드가 세장이라면, 손에서 낸 카드와, 뒤집어 낸 카드와 원래 카드가 한장 더 있다는거네요.

그러면 바로 이것이 "뻑!!" 인것이죠. 역시 마찬가지로 애니메이션 처리는 아직 구현하지 않았으므로,

임시로 println() 함수를 이용해서 debug 창에만 출력을 해 주었습니다.


그러나, 해당 월을 쭉 살펴 보았는데, CARD_NULL 인 것이 없다면, 해당 월이 가득 찬...

즉, 카드 4장이 한 슬롯에 있다는 의미겠죠. 그럴때는 4장을 모두 해당 플레이어가 먹게 됩니다.

else // 손에서 낸 카드와 뒤집어 낸 카드가 다른 슬롯에 있다면,
{
    room.processCard(curTurn, handCardIdx);
    room.processCard(curTurn, reverseCardIdx);

    if(room.bSelectTwoCard) // 두장의 카드중 선택해야 한다면,
    {
        curViewState = State.GAME_SELECTTWOCARDS;
        repaints();
        return;
    }
}

이번에는 handCard와 reverseCard가 다른 슬롯에 있을때의 처리를 해 줍니다.

각자 다른 슬롯에 있다면, 슬롯 마다 각각 처리를 해 주어야 겠네요.

각각의 슬롯에 붙여진 카드를 처리해 주는 함수는 processCard() 함수 입니다.


/**
 * 낸 카드를 처리한다.
 * @param userType
 * @param monthIdx
 */
public void processCard(byte userType, int monthIdx)
{
    for(i=0; i<4; i++)
    {
        if(cardsMonth[monthIdx][i] == CARD_NULL)
        {
            if(i == 0 || i == 1 ) // 슬롯에 아무것도 없거나, 한장 뿐이면,
            {
                ;
            }
else if(i == 2 ) // 슬롯에 낸 카드와, 원래 카드 한장있다면, { // 둘다 먹는다. addPae(userType, getMonth(monthIdx)[0]); getMonth(monthIdx)[0] = CARD_NULL; addPae(userType, getMonth(monthIdx)[1]); getMonth(monthIdx)[1] = CARD_NULL; } else if( i == 3 ) // 슬롯에 낸 카드와 원래 카드 두장 있다면, { // 만약 둘다 같은 종류의 카드라면, if(gCard.getBigType(getMonth(monthIdx)[0]) == gCard.getBigType(getMonth(monthIdx)[1])) { // 둘장 아무거나 하나를 먹고, addPae(userType, getMonth(monthIdx)[Etc.getRandomInt(2)]); getMonth(monthIdx)[Etc.getRandomInt(2)] = CARD_NULL; // 마지막 카드(사용자가 낸 카드)를 먹는다. addPae(userType, getMonth(monthIdx)[i-1]); getMonth(monthIdx)[i-1] = CARD_NULL; break; }

// 두장이 다른 카드라면, 둘중 하나 선택. bSelectTwoCard = true; for(j=0; j<2; j++) { if(slot[j] == CARD_NULL) { slot[j] = (byte)monthIdx; break; } } slotLast[j] = (byte)(i-1); } break; } else if(i == 3) // 슬롯에 카드가 가득 차 있다면, { // 모두 먹는다. for(j=0; j<4; j++) { addPae(userType, getMonth(monthIdx)[j]); getMonth(monthIdx)[j] = CARD_NULL; } } } }

앞서, twogo1p.GameView 클래스의 throwCard() 함수와 비슷한 형태로 검사를 합니다.

해당 월을 루프를 돌면서 검사를 해서,

슬롯에 플레이어가 낸 카드 한장과 원래 카드 한장이 있다면, 둘다 먹어야 하겠지요.

슬롯에 낸 카드와 원래 카드가 두장이 있다면, 둘중 하나를 선택해야 합니다.

그런데 두장의 카드가 같은 종류의 카드라면, 즉 둘다 피이거나, 열이거나 하는 경우에는, 선택할 필요가 없겠죠.

그래서 같은 종류의 카드 일때에는 그냥 컴퓨터가 알아서 렌덤으로 둘중 하나를 먹고 루프를 빠져나갑니다.


그러나 둘다 다른 종류의 카드라면, 플레이어에게 어떤 카드를 먹을 것인지 선택할 수 있도록 해 주어야합니다.

// 두장이 다른 카드라면, 둘중 하나 선택.
bSelectTwoCard = true;

for
(j=0; j<2; j++) { if(slot[j] == CARD_NULL) { slot[j] = (byte)monthIdx; break; } }
slotLast[j] = (byte)(i-1);

room 클래스 내에서는, 키 입력을 받을 수 없으므로,

processCard() 함수를 빠져 나간 뒤에 두장의 카드를 선택 받기 위해서,

bSelectTwoCard 라는 함수가 true 일대에는, 두장의 카드 중 하나를 선택해야 한다는 의미가 됩니다.

slot[0]과 slot[1]에는, 선택해야 할 카드가 있는 월의 인덱스가 들어가고,

slotLast[0]과 slotLast[1] 에는 해당 카드 수가 들어갑니다.


위와 같이 해 준뒤에, processCard() 함수를 벗어 난 뒤에,

if(room.bSelectTwoCard) // 두장의 카드중 선택해야 한다면,
{
    curViewState = State.GAME_SELECTTWOCARDS;
    repaints();
    return;
}

위와 같이, bSelectTwoCard 플래그를 체크해 주어,

카드를 선택해야 한다면, curViewState를 State.GAME_SELECTTWOCARDS로 변경해서,

두장의 카드중 하나를 선택하는 키 입력을 받습니다.


아래는 GameView 클래스의 keyPressed() 함수 중, 숫자 키 입력을 처리 하는 부분에 추가된 부분입니다.

if(curViewState == State.GAME_SELECTTWOCARDS)
{
    int index;

    System.out.println(room.slot[0] + ", " + room.slot[1]);

    if(room.slot[0] != room.CARD_NULL)
        index = 0;
    else /* if(room.slot[1] != room.CARD_NULL) */
        index = 1;

    if(room.getMonth(room.slot[index])[numKey-1] != room.CARD_NULL)
        room.addPae(curTurn, room.getMonth(room.slot[index])[numKey-1]);
    if(room.getMonth(room.slot[index])[room.slotLast[index]] 
        != room.CARD_NULL)
        room.addPae(curTurn, 

room.getMonth(room.slot[index])[room.slotLast[index]]);
room.getMonth(room.slot[index])[numKey-1] = room.CARD_NULL; room.getMonth(room.slot[index])[room.slotLast[index]] = room.CARD_NULL; room.sortMonth(room.slot[index]); room.slot[index] = room.CARD_NULL;

// 모두 선택했다면, 다시 게임으로 돌아감. if(room.slot[0] == room.CARD_NULL && room.slot[1] == room.CARD_NULL) { room.bSelectTwoCard = false; curViewState = State.GAME_RUNNING; // 턴을 변경. curTurn = (curTurn==User.PLAYER)? User.COMPUTER : User.PLAYER; repaints(); return; } }

복잡해보이지만, 1번을 누르면, 1번 카드를 먹고, 2번을 누르면 2번 카드를 먹게 되는 간단한 코드입니다.

if(room.getMonth(room.slot[index])[numKey-1] != room.CARD_NULL)
    room.addPae(curTurn, room.getMonth(room.slot[index])[numKey-1]);
if(room.getMonth(room.slot[index])[room.slotLast[index]] 
!= room.CARD_NULL) room.addPae(curTurn,

room.getMonth(room.slot[index])[room.slotLast[index]]);
room.getMonth(room.slot[index])[numKey-1] = room.CARD_NULL; room.getMonth(room.slot[index])[room.slotLast[index]] = room.CARD_NULL; room.sortMonth(room.slot[index]); room.slot[index] = room.CARD_NULL;

1번을 누르면 numkey 에 1이 들어 오므로, 1번 을 누르면 0번 슬롯을 선택하고,

2번을 누르면 1번 슬롯의 카드를 addPae() 함수를 이용해서 플레이어가 먹고,

slotLast에 저장된 마지막 인덱스의 카드(사용자가 낸 카드도)도 먹어주어야 겠지요.

그 다음 해당 월에 중간 중간 카드를 먹어서 생긴 공백을 sortMonth() 함수를 이용해서 제거해주고,

repaints()로 화면을 다시 갱신해 줍니다.


두장의 카드를 선택중일때에는 단순히 화면에 어떤 카드를 선택할 것인지 물어 보면 됩니다.

그러나 아직 이번시간에는 디스플레이 요소들은 삽입하지 않았으므로,

글씨를 출력해서 어떤 카드를 선택 할 것인지를 물어보게 했습니다.

두 장의 카드를 선택중일 때의 화면은 drawSelectTwoCards() 함수로 그려줍니다.

/**
 * 카드 둘중 하나를 선택할때...
 * @param g
 */
private void drawSelectTwoCards(Graphics g) { drawRunningGame(g); g.drawString("1번 2번 어떤 카드 드실래요?", 0, 0, Graphics.LEFT|Graphics.TOP); }

간단히 drawRunningGame() 을 호출해서 게임 진행중의 화면을 먼저 그려준 뒤,

문자 열을 출력 합니다. 조금 허접한 문자열이 출력 되겠지만,

다음 시간에 디스플레이 요소를 삽입해서 멋지게 물어본다면 그럴 듯 해지겠지요^^;

Posted by maysent
:
지난 시간까지 작성한 코드에서는 게임 시작 대기 화면에서 확인 버튼을 누르면,

바로 State.GAME_RUNNING으로 GameView.curViewState가 변경 되었지만,


이번에는 State.GAME_DISTRIBUTECARDS 로 변경되도록 바꾸어 주고 나서,

카드를 나누어 주는 것을 구현해 주어야 하겠지요?

/**
 * 키 입력.
 */
public void keyPressed(int keyCode)
{
    switch(keyCode)
    {

/**
*
* ... 생략 ...
*
*/


case KEY_COMR: case KEY_FIRE: case KEY_POUND: /* '#' */ if(curViewState == State.GAME_WAIT) { byte i; curViewState = State.GAME_DISTRIBUTECARDS;
distributeStep = 0; // 나눠주기 단계 0으로 초기화. curTurn = User.PLAYER; repaints(); Sleep.wait(this, 500); distributeStep = 1; // 나눠주기 단계 1로 초기화. room.init(); repaints(); Sleep.wait(this, 500); for(i=2; i<10; i++) { distributeStep = i; // 나눠주기 단계 설정. // 컴퓨터에게 카드를 줄때는, if(distributeStep == 4 || distributeStep == 6) curTurn = User.COMPUTER; else curTurn = User.PLAYER; room.distributeCards(distributeStep); repaints(); Sleep.wait(this, 500); } // 카드를 다 나누어주었다면, 게임 시작. curViewState = State.GAME_RUNNING; curTurn = User.PLAYER; repaints(); } }
}

다른 키 입력은 모두 생략하고, 게임 대기 화면에서 확인키를 눌렀을 때만을 살펴봅니다.
(다른 키 입력은 지난 시간에 작성한 내용과 동일 합니다.)

가장 먼저, 확인키를 누르면, curViewState를 State.GAME_DISTRIBUTECARDS로 초기화를 해서,

카드를 나눠주는 화면을 그려줄 수 있도록 합니다.

distributeStep은 카드를 나눠주는 단계를 저장하는 변수인데, 잠시후에 나옵니다.


그 다음에는, distributeStep을 하나씩 증가 해줄때마다,

repaints() 함수를 호출해서 전체화면을 다시 그려주어, 나눠주는 것을 플레이어가 확인하게 하는 것이죠.


카드를 나누어 주는 동작은 0번, 1번은 keyPressed에서 직접 처리해주고,

2번 ~ 9번까지의 동작은, gostop.GameRoom 클래스에서 distributeCards() 함수로 처리합니다.

/**
 * 카드를 나눠준다.
 * @param distributeStep 나눠주기 단계.
 */
public void distributeCards(int distributeStep)
{
    switch(distributeStep)
    {
    case 2: // 중앙에 카드를 뒤집어 바닥에 4장의 카드를 깜.
       for(i=0; i<4; i++)
            setMonth(i, suffleCards.removeLast(), false);
        break;
    case 3: // 플레이어에게 먼저 5장의 카드를 줌.
       for(i=0; i<5; i++)
            setUserHand(User.PLAYER, i, suffleCards.removeLast());
        break;
    case 4: // 컴퓨터에게 5장.
        for(i=0; i<5; i++)
            setUserHand(User.COMPUTER, i, suffleCards.removeLast());
        break;
    case 5: // 다시 바닥에 4장을 깜.
        for(i=4; i<8; i++)
            setMonth(i, suffleCards.removeLast(), false);
        break;
    case 6: // 컴퓨터에게 5장.
        for(i=5; i<10; i++)
            setUserHand(User.COMPUTER, i, suffleCards.removeLast());
        break;
    case 7: // 플레이어에게 5장.
        for(i=5; i<10; i++)
            setUserHand(User.PLAYER, i, suffleCards.removeLast());
        break;
        }
    }

Step별로 중앙에 카드를 뿌려주고, 컴퓨터와 플레이어에게도 각각 나눠줍니다.

이때 나눠주는 카드는 모두 중앙의 카드에서 위에서부터 하나씩 차례로 뒤집어 나누어주는 것이며,

앞서 살펴보았던 BCollector 클래스의 removeLast() 함수를 호출해서 뒤집어 줍니다.


이제 카드를 나누어 주는 동작은 모두 구현했습니다.

동작을 구현했더라도 그 동작을 화면상에 그려주는 작업도 필요하겠지요.

카드를 나누어 주는 장면은 drawDistributeCards() 함수가 처리합니다.

/**
 * 카드를 나눠주는 중의 화면을 그려준다.
 * @param g
 */
private void drawDistributeCards(Graphics g)
{
    int i, j;

    // Step 0: 아무것도 그리지않는다.
    if(distributeStep == 0)
        return;


    // Step 1: 화투를 섞어 중앙에 쌓아놓음.
    drawCard(floorCenterPos.left-TRANS_WIDTH, 
floorCenterPos.top+TRANS_HEIGHT*curTurn, CARD_BACK); if(distributeStep == 1) return; // Step 2: 중앙의 카드중 위에서 4장을 바닥에 깜. for(i=0; i<4; i++) { if(room.getMonth(i)[0] != room.CARD_NULL) drawCard(floorCardPos[i].left-TRANS_WIDTH,
floorCardPos[i].top+TRANS_HEIGHT*curTurn,
CARD_BACK); } if(distributeStep == 2) return; // Step 3: 중앙의 카드중 위에서 5장을 플레이어에게... for(i=0; i<5; i++) drawCard(i*12, height-CARD_HEIGHT+TRANS_HEIGHT*curTurn,
CARD_BACK); if(distributeStep == 3) return; // Step 4: 중앙의 카드중 위에서 5장을 컴퓨터에게... if(curTurn == User.COMPUTER) for(i=0; i<5; i++) drawSmallCard((i+1)*5, -TRANS_HEIGHT, CARD_BACK); if(distributeStep == 4) return; // Step 5: 중앙의 카드중 위에서 4장을 바닥에 깜. for(i=4; i<8; i++) { if(room.getMonth(i)[0] != room.CARD_NULL) drawCard(floorCardPos[i].left-TRANS_WIDTH,
floorCardPos[i].top+TRANS_HEIGHT*curTurn,
CARD_BACK); } if(distributeStep == 5) return; // Step 6: 중앙의 카드중 위에서 5장을 컴퓨터에게... if(curTurn == User.COMPUTER) for(i=5; i<10; i++) drawSmallCard((i+1)*5, -TRANS_HEIGHT, CARD_BACK); if(distributeStep == 6) return; // Step 7: 중앙의 카드중 위에서 5장을 플레이어에게... for(i=5; i<10; i++) drawCard(i*12, height-CARD_HEIGHT+TRANS_HEIGHT*curTurn,
CARD_BACK); if(distributeStep == 7) return; // Step 8: 바닥에 깔린 패를 모두 뒤집어 준다. drawMonths(g); /* * 중앙의 패 중에, 서비스 패가 있다면, 실행. */ for(i=0; i<8; i++) {
/* 서비스 패가 있다면, */ if( room.isAdditionalCard(room.getMonth(i)[0]) ) { if(i < 4) room.addPee(User.COMPUTER, room.getMonth(i)[0]); else room.addPee(User.PLAYER, room.getMonth(i)[0]); room.setMonth(i, true); } } if(distributeStep == 8) return; // Step 9: 각 월 정렬. room.sortMonth(); // 월 정렬. drawPee(); // 플레이어가 먹은 서비스피를 그려준다. }

distributeStep은, 한번 나눠줄때마다 keyPressed() 함수에서 distributeStep을 증가 시켜주고

다시 drawDistributeCards()를 호출하므로, 카드를 나눠주는 동작을 한단계 한단계 확인할수 있습니다.


drawDistributeStep()은 보시는 것과 같이 한단계 한단계, 단계별로 나눠주는 동작이 구현되어 있습니다.

1번 단계에서는 섞은 카드를 중앙에 그려줍니다.

말이 섞은 카드를 중앙에 그려주는 것이지, 결국엔 화투의 뒷면을 게임 화면 중앙에 그려주는 것 뿐입니다.

아래는 1번 단계의 수행 결과입니다. 그림과 비교하시면서 보시는 것이 더 이해가 빠를것 같네요.


< Step #1 >

2단계 에서는, 섞은 카드에서 위에서 4장을 바닥에 깔아주는 동작을 합니다.

// Step 2: 중앙의 카드중 위에서 4장을 바닥에 깜.
for(i=0; i<4; i++)
{
    if(room.getMonth(i)[0] != room.CARD_NULL)
        drawCard(floorCardPos[i].left-TRANS_WIDTH, 
floorCardPos[i].top+TRANS_HEIGHT*curTurn,
CARD_BACK); }

0번 부터 3번까지의 자리에 해당 자리에 카드가 있다면,(널 카드가 아니라면,) 카드를 그려줍니다.

아래는 2번 단계의 수행 결과입니다.


< Step #2 >

마찬가지로 3번 과정 역시 그려주는 위치만 floorCardPos가 아닐뿐... 나머지 과정은 모두 동일합니다.


같은 방식으로 7단계를 실행하고 나면, 다음과 같은 결과를 볼 수 있습니다.

 
< Step #7 >

이제 카드를 뒤집어야 하는데, 바닥의 패 중에 서비스 패가 있어서는 안되겠지요.

그래서 바닥의 패를 뒤집고 난 후, 바닥에 서비스 패가 있는지 체크합니다.

/*
 * 중앙의 패 중에, 서비스 패가 있다면, 실행.
 */
for(i=0; i<8; i++)
{
/* 서비스 패가 있다면, */ if( room.isAdditionalCard(room.getMonth(i)[0]) ) { if(i < 4) room.addPee(User.COMPUTER, room.getMonth(i)[0]); else room.addPee(User.PLAYER, room.getMonth(i)[0]); room.setMonth(i, true); } }

만약 바닥에서 서비스 패를 발견했다면,

앞서 나눠준 바닥 인덱스 3번 이하에 있는 서비스 패는 컴퓨터가 먹고,

나중에 나눠준 7번 이하의 서비스 패는 플레이어가 먹습니다.

그리고 서비스패가 빠져나간 바닥 슬롯의 마지막에 중앙의 카드에서 한장을 뒤집어 배치 해 주면 되겠죠.


그리고 나서, sortMonth() 함수를 호출 해주면, 같은 종류의 카드들은 한 슬롯에 몰아 줍니다.

Step 9의 결과는 다음과 같습니다.


< Step #9>

Step 8을 거치고 나면, sortMonth() 함수에 의해서 각 월이 위와 같이 정렬이 됩니다.

그럼 각 월을 정렬해 주는 sortMonth() 함수는 어떤식으로 이루어 져 있는지 한번 살펴보도록 하죠.

/**
 * 각 월을 정렬한다.
 */
public void sortMonth()
{
    int k;

    for(i=0; i<12; i++)
        for(j=0; j<12; j++)
        {
// 카드가 존재하지 않는 곳과는 비교하지 않는다.
if(cardsMonth[i][0] == CARD_NULL
|| cardsMonth[j][0] == CARD_NULL) continue;
// 같은 종류의 패라면, if((i != j) && cardsMonth[i][0]/4 == cardsMonth[j][0]/4 && cardsMonth[i][0] != CARD_NULL
&& cardsMonth[j][0] != CARD_NULL) { for(k=0; k<4; k++) { if(cardsMonth[i][k] == CARD_NULL) { cardsMonth[i][k] = cardsMonth[j][0]; cardsMonth[j][0] = CARD_NULL; } } } } }


sortMonth() 함수는 같은 종류의 카드는 같은 슬롯에 몰아 주는 역할을 합니다.

이미지 배열이 쭈르륵 있는데, 그중에 같은 종류의 카드인지를 도대체 어떻게 판든을 해야 할까요?

그 정답은 바로 화투 그림의 배열 속에 있습니다.

다시 한번 화투 이미지를 살펴보면,

 

위와 같이 되어 있습니다.

위를 보면 각 월 별로, 1월, 2월, 3월, 4월, 5월, 쭈르륵... 12월, 그리고 서비스패, 뒷면.

이런식으로 되어 있죠.


각 월마다가 같은 종류의 카드이고, 각 월은 모두 4장씩입니다.

그러므로, 해당 카드의 인덱스를 4로 나눠서 몫이 같다면, 바로 같은 종류의 패가 되겠지요.

그러나 CARD_NULL 상수가 -1인데, -1을 4로 나눠도 몫은 0이므로, 1월 카드로 오인할 소지가 있습니다.

그러므로, CARD_NULL 이라면, 계산하지 않아야 하겠지요.

 

Posted by maysent
:
이번 시간에는 맞고의 기본적인 게임 시스템을 구현 해보는 시간을 갖도록 하겠습니다.

먼저 맞고의 패들을 특정 위치에 그려주기 위하여, 좌표를 설정해 주어야 하는데,

좀 더 편한 좌표의 관리를 위해 common 패키지에 Coord라는 class를 추가합니다.

* common.Coord.java.
package common;

/**
 * x,y좌표를 알고 있어야 할때 사용
 */

public class Coord
{
    public short left, top;

    public Coord(int x, int y)
    {
        this.left = (short)x;
        this.top = (short)y;
    }
}

이 Coord 클래스는 단순히, left와 top 이라는 두 멤버를 가지며,

생성자를 통해, left와 top을 초기화 시켜주는 그야 말로 좌표 변수 두개만을 관리해 주는 클래스입니다.

별로 특별한 설명이 필요없는 클래스네요.


그리고 지난 시간까지 만들었던 twogo1p.GameView에 이제 특정 위치에 카드를 출력해 주어야 하므로,

GameView 클래스의 생성자 부분에 각각의 카드의 위치를 초기화 해 줍니다.

/**
 * 각 개체의 위치 설정.
 */
private void initPos()
{
    rectFloor = new Rect(0,33,128,54);
    cardWindowHeight = (short)((totalHeight-imgFloorBg.getHeight())/2);

    /*
     * 게임 내 화투 패의 좌표 설정.
     */
    /* 중앙의 패 */
    floorCenterPos = new Coord(54, 48);

    /* 바닥에 깔릴 패의 좌표를 각각 지정해 준다. */
    floorCardPos = new Coord[12];
    floorCardPos[0] = new Coord(37,34);
    floorCardPos[1] = new Coord(72,34);
    floorCardPos[2] = new Coord(37,63);
    floorCardPos[3] = new Coord(72,63);
    floorCardPos[4] = new Coord(19,34);
    floorCardPos[5] = new Coord(19,63);
    floorCardPos[6] = new Coord(91,34);
    floorCardPos[7] = new Coord(91,63);
    floorCardPos[8] = new Coord(0,34);
    floorCardPos[9] = new Coord(0,63);
    floorCardPos[10] = new Coord(110,34);
    floorCardPos[11] = new Coord(110,63);

    /* 플레이어와 컴퓨터의 패 위치 설정. */
    // 플레이어가 먹은 패.
    playerCardPos = new Coord[4];
    playerCardPos[KWANG] = new Coord(27,97);    // 광.
    playerCardPos[YEUL] = new Coord(58,97);     // 열.
    playerCardPos[DDEE] = new Coord(58,111);    // 단.
    playerCardPos[PEE] = new Coord(92,97);      // 피.

    // 컴퓨터가 먹은 패.
    computerCardPos = new Coord[4];
    computerCardPos[KWANG] = new Coord(24+3,3); // 광.
    computerCardPos[YEUL] = new Coord(58,3);    // 열.
    computerCardPos[DDEE] = new Coord(58,17);   // 단.
    computerCardPos[PEE] = new Coord(92,3);     // 피.

/* 먹은 패 보기 상태일때의 카드 위치 세팅. */ acquireCardPos = new Coord[4]; acquireCardPos[0] = new Coord(1,11); //광 acquireCardPos[1] = new Coord(1,36); //열 acquireCardPos[2] = new Coord(1,61); //단 acquireCardPos[3] = new Coord(1,86); //피 }

그냥 일일이 카드의 좌표를 하나하나 지정해 주었습니다.

initPos() 함수를 클래스 생성자 부분에서 호출해주면 클래스의 생성과 동시에 좌표도 초기화 되겠지요.


이제 카드의 위치를 각각 지정해 주었으니, 카드를 그려주는 함수를 구현해야하는게 당연하겠죠.

카드의 종류는 일반 사이즈의 카드와 작은 사이즈의 카드 두 조율가 있으므로, 함수도 두개가 되겠죠.

/**
 * 화면의 특정 위치에 카드를 그려준다.
 * @param x
 * @param y
 * @param cardNum
 */
protected void drawCard(int x, int y, int cardNum)
{
    g2d.drawImage(x,y, imgCard, (cardNum*CARD_WIDTH), 0, 
CARD_WIDTH, CARD_HEIGHT, Graphics2D.DRAW_COPY); } /** * 화면의 특정 위치에 작은 카드를 그려준다. * @param x * @param y * @param cardNum */ protected void drawSmallCard(int x, int y, int cardNum) { g2d.drawImage(x,y, imgSmallCard, (cardNum*SMALLCARD_WIDTH), 0,
SMALLCARD_WIDTH, SMALLCARD_HEIGHT, Graphics2D.DRAW_COPY); }

drawCard와 drawSmallCard는 완전히 동일하며, 단지 읽어오는 이미지가 다르다는 것만이 다르네요.

각 함수의 인자로 받는 x, y를 통해서 카드를 그려줄 x, y 좌표를 지정하고,

cardNum을 지정해서 어떤 카드를 출력할 것인지 지정합니다.

일반 카드와 작은 카드는 다음과 같이 이미지화 되어 있습니다.





위의 큰 카드 이미지는 화면의 제약 때문에 약간 가로로 줄였더니 길쭉하게 보이네요.

어쨋든 위와 같이 되어 있기 때문에, 0번 카드를 출력하고 싶다면, 0, 0 부터 읽어 오고,

1번 카드를 출력하고 싶다면, CARD_WIDTH * 1, 0 부터 읽어오고, 2번은 CARD_WIDTH * 2, 0부터...

이런식으로 되는 것이죠.


이제 카드를 출력하기 위한 모든 준비가 끝났네요.

그러나 게임상에서 1번 카드, 2번 카드 이렇게 처리를 하다보면, 1번 카드가 뭔지 2번 카드가 뭔지,

프로그래머도 사람인 이상 그걸 일일이 기억하고 있을 순 없으니, 당연히 헷갈리겠죠.


그렇기 때문에 카드의 Type을 직접 정의해 주어서 편리하게 관리를 할 필요가 있습니다.

각각의 CardType 상수는 따로 Interface로 만들어 관리하는 것이 좋겠지요.

* gostop.CardConstants.java.

package gostop;

/**
 * 카드와 관련된 상수들을 정의 한다.
 */

public interface CardConstants
{
    /*
     * 카드 사이즈 상수.
     */
    /* 큰 카드 사이즈. */
    final int CARD_WIDTH = 16;
    final int CARD_HEIGHT = 24;
    /* 작은 카드 사이즈. */
    final int SMALLCARD_WIDTH = 8;
    final int SMALLCARD_HEIGHT = 12;

    final int CARD_GOOKJIN = 32; // 국진 이미지의 인덱스.
    final int CARD_BACK = 54; // 카드 뒷면 이미지의 인덱스.

    /*
     * Big Type 상수.
     */
    final byte KWANG = 0; // 광.
    final byte YEUL = 1; // 열.                 
    final byte DDEE = 2; // 띠, 단.             
    final byte PEE = 3; // 피.          
    final byte NULL = 4; // 널.

    /*
     * Card Type 상수.
     */
    final byte CARDTYPE_KWANG = 0; // 광.       
    final byte CARDTYPE_BIKWANG = 1;    // 비광.
    final byte CARDTYPE_YEUL = 2; // 열.
    final byte CARDTYPE_GODORI = 3; // 고도리.
    final byte CARDTYPE_CHODAN = 4; // 초단.
    final byte CARDTYPE_CHUNGDAN = 5; // 청단.
    final byte CARDTYPE_HONGDAN = 6;    // 홍단.
    final byte CARDTYPE_PEE = 7;    // 피.
    final byte CARDTYPE_SSANG_PEE = 8; // 쌍피.
    final byte CARDTYPE_THREE_PEE = 9; // 쓰리피.
    final byte CARDTYPE_OJARI = 10; // 오자리.
    final byte CARDTYPE_NULL = 11; // 널 카드.          
}

이 interface에는 카드의 타입과 관련된 상수들이 정의되어 있습니다.

CARD_WIDTH/CARD_HEIGHT는 카드의 높이와 폭.


그 외에 국진의 번호나, 카드 뒷면의 번호는 또 따로 관리를 해 줍니다.

그리고 카드의 타입을 나타내는 상수가 Big type과 일반 Card type 두 종류가 정의되어 있는데,

Big type은 그냥 해당 카드가 어떤 종류의 카드인지, 광이면 광, 열이면 열, 띠면 띠, 피면 피.

또 Card type은 광이라고 해도, 점수 계산을 할때에는 비광이 포함되면 또 달라지죠.

그래서 일반 광과 비광을 따로 처리 하며, 그외에도 고도리나, 청단, 홍단, 쌍피, 쓰리피, 오자리 등은

모두 점수 계산을 위해 세부적으로 분류된 상수입니다.


이제 앞서 정의된 final 상수 인터페이스를 가지고, 각각의 카드 타입을 지정해 줄 필요가 있겠죠.

GostopCard라는 클래스를 또 하나 추가해서 각각의 카드 정보를 하나의 클래스가 관리 하도록 해 줍니다.

package gostop;

/**
 * 고스톱에 관련된 카드의 기본 정보를 기억하고 있는 클래스
 * 추가쌍피 및 추가쓰리피는 뒤쪽에 계속 추가할수 있다.
 * 카드 구조
 * 0번에서 47번 : 일반카드패
 * 48번에서 n : 추가쌍피패, 언제든 추가할수 있음
 * n번 : 널카드패
 */

public class GostopCard implements CardConstants
{
    /* 추가 쌍피를 제외한 실제 화투패의 수. */
    public final byte MAX_ORIGINAL_CARD = 48;
    /* 전체 카드 갯수. */
    private byte MAX_CARD = MAX_ORIGINAL_CARD;
    /* 널 카드 번호. */
    public byte CARD_NULL = MAX_CARD;

    /*
     * 추가 쌍피정의
     */
    /* 추가 쌍피 갯수. */
    public byte ADD_SSANG_PEE;
    /* 추가 쓰리피 갯수. */
    public byte ADD_THREE_PEE;

    /* 추가 쌍피 시작 인덱스. */
    private byte ADDCARD_IDX_MIN = MAX_CARD;
    /* 추가 쌍피들의 끝값. */
    private byte ADDCARD_IDX_MAX = (byte)(MAX_CARD-1);

private byte[] cardType; private byte[] bigType; /** * Constructor. */ GostopCard() {} /** * 최종 게임 생성되는 부분에서 아래의 init를 꼭 불려줘야 함 * @param numAddCard2 추가 쌍피 개수. * @param numAddCard3 추가 쓰리피 개수. */ public void init(int numAddCard2, int numAddCard3) { short cnt; ADD_SSANG_PEE = (byte)numAddCard2; ADD_THREE_PEE = (byte)numAddCard3; // 추가쌍피 추가. for(int i=0; i<(numAddCard2+numAddCard3); i++) addAdditionalCard(); // Card Type 배열을 생성한다. cardType = new byte[MAX_CARD+1]; bigType = new byte[MAX_CARD+1]; initCardType(); // Card Type 초기화.

// 쌍피 추가. for(cnt=0; cnt<numAddCard2; cnt++) initTypeAdditionalCard(cnt, (short)2); // 쓰리피 추가. for(cnt=0; cnt<numAddCard3; cnt++) initTypeAdditionalCard((short)(cnt+numAddCard2), (short)3); } /** * 자원 해제. */ public void cleanUp() { cardType = null; bigType = null; } /** * Card Type 세팅. */ private void initCardType() { // 카드 타입을 기억한다. cardType[0] = CARDTYPE_KWANG; cardType[1] = CARDTYPE_HONGDAN; cardType[2] = CARDTYPE_PEE; cardType[3] = CARDTYPE_PEE; cardType[4] = CARDTYPE_GODORI; cardType[5] = CARDTYPE_HONGDAN;

/**
* : *
* 중간 생략. 소스 코드에서 확인하세요^^ * * :
*/


cardType[45] = CARDTYPE_OJARI; cardType[46] = CARDTYPE_YEUL; cardType[47] = CARDTYPE_SSANG_PEE; cardType[CARD_NULL] = CARDTYPE_NULL;
bigType[0] = KWANG; bigType[1] = DDEE; bigType[2] = PEE; bigType[3] = PEE; bigType[4] = YEUL; bigType[5] = DDEE;
/**
* : *
* 중간 생략. 소스 코드에서 확인하세요^^ * * :
*/

bigType[45] = DDEE; bigType[46] = YEUL; bigType[47] = PEE; bigType[CARD_NULL] = NULL; } /** * 카드의 종류를 리턴한다. * @param cardNum * @return */ public byte getType(byte cardNum) { return cardType[cardNum]; } /** * 카드의 종류를 리턴한다. * @param cardNum * @return */ public byte getBigType(byte cardNum) { return bigType[cardNum]; } /** * 인자로 받은 번호의 카드가 추가 쌍피인지 체크. * @param cardNum * @return */ public boolean isAdditionalCard(byte cardNum) { /* 추가 쌍피라면, */ if((cardNum>=ADDCARD_IDX_MIN) && (cardNum<=ADDCARD_IDX_MAX)) return true; return false; // 추가 쌍피가 아님. } /** * 해당 피가 피가 맞는지 리턴. * @return */ public boolean isPee(int cardNum) { if(bigType[cardNum] == PEE) return true; return false; } /** * 해당 피가 일반피인지 리턴. * @return */ public boolean isNormalPee(int cardNum) { if(cardType[cardNum] == CARDTYPE_PEE) return true; return false; } /** * 해당 피가 쌍피인지 리턴. * @return */ public boolean isSsangPee(int cardNum) { if(cardType[cardNum] == CARDTYPE_SSANG_PEE) return true; return false; } /** * 해당 피가 쓰리피인지 리턴. * @return */ public boolean isThreePee(int cardNum) { if(cardType[cardNum] == CARDTYPE_THREE_PEE) return true; return false; } /** * 인자로 받은 번호의 카드가 널카드인지 체크. * @param cardNum * @return */ public boolean isNullCard(byte cardNum) { if(cardNum == CARD_NULL) { return true; } return false; } /** * 쌍피를 추가 해줌. */ public void addAdditionalCard() { MAX_CARD++; ADDCARD_IDX_MAX++; CARD_NULL++; } /** * 추가 쌍피를 추가한다. * @param idx * @param numPee 쌍피, 쓰리피. */ private void initTypeAdditionalCard(short idx, short numPee) { switch(numPee) { case 1: // 일반 피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; case 2: // 쌍피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_SSANG_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; case 3: // 쓰리피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_THREE_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; } } /** * 총 카드의 수 리턴. * @return */ public byte getMaxCard() { return MAX_CARD; } /** * 추가 쌍피의 마지막 인덱스 리턴.. * @return */ public byte getAdditionalMax() { return ADDCARD_IDX_MAX; } /** * 추가 쌍피의 첫 인덱스 리턴. * @return */ public byte getAdditionalMin() { return ADDCARD_IDX_MIN; } /** * 널 카드의 인덱스 리턴. * @return */ public byte getNullIndex() { return CARD_NULL; } }

먼저, 쌍피나, 쓰리피등의 추가 서비스 패를 제외한 본래의 고스톱 카드의 장수는,

하나의 월마다 4장의 카드씩으로, 4*12 = 48. 그래서 48장입니다.

먼저 초기화 함수인 init()에서 호출하는 initCardType() 함수를 보면,

각각의 cardType과 bigType 배열에 CARDTYPE 상수와 Big Type 상수를 일일이 대입해 주고 있습니다.

그리고 init() 함수의 마지막 부분에서 initTypeAdditionalCard()를 호출해서 서비스 패를 초기화 해줍니다.

/**
 * 추가 쌍피를 추가한다.
 * @param idx
 * @param numPee 쌍피, 쓰리피.
 */
private 
void initTypeAdditionalCard(short idx, short numPee) { switch(numPee) { case 1: // 일반 피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; case 2: // 쌍피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_SSANG_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; case 3: // 쓰리피. cardType[ADDCARD_IDX_MIN+idx]=CARDTYPE_THREE_PEE; bigType[ADDCARD_IDX_MIN+idx]=PEE; break; } }

inittypeAdditionalcard() 함수는 두번째 매개변수인 numPee가 1이면, 일반피로 처리하고,

numPee가 2면 쌍피로 처리, numPee가 3이라면 쓰리 피로 처리해 줍니다.


그리고 마지막으로 중앙에 쌓아놓는 카드를 관리하기 위해,

숫자 배열을 관리해 주는 BCollector라는 클래스를 하나 생성합니다.

* common.BCollector.java.
package common;

/**
 * 숫자배열을 관리하는 class.
 */

public class BCollector
{
    private boolean bSort = false; // 자료의 정렬 여부.
    private byte elementDataA[];
    private int size;

    /**
     * Constructor.
     */
    public BCollector()
    {
        this(10); // Default 인덱스는 10.
    }

    /**
     * Constructor.
     * @param maxSize
     */
    public BCollector(int maxSize)
    {
        elementDataA = new byte[maxSize];
        size = 0;
    }

    /**
     * 자료를 넣을때 정렬해서 넣을건지 결정한다.
     * @param bSort
     */
    public void setSort(boolean bSort)
    {
        this.bSort = bSort;
    }


    /**
     * 숫자를 추가한다.
     * @param num
     */
    public void addNum(byte num)
    {
        // 정렬을 해서 넣어야 한다면,
        if(bSort == true)
        {
            int cnt;

            for(cnt=0; cnt<size; cnt++)
            {
                if(num < elementDataA[cnt])
                {
                    addData(num, cnt);
                    return;
                }
            }
        }

        addData(num);
    }

    /**
     * data를 맨 뒤에 삽입한다.
     * @param data
     */
    private synchronized void addData(byte data)
    {
        elementDataA[size++] = data;
    }

    /**
     * 맨뒤의 data를 삭제한다.
     * @return 삭제한 index의 data.
     */
    public byte removeLast()
    {
        byte value;

        value = elementDataA[size-1];
        removeData(size-1);

        return value;
    }

    /**
     * 특정 index에 data를 삽입한다.
     * @param data
     * @param index
     */
    private synchronized void addData(byte data, int index)
    {
        // 해당 index 부터 한칸 뒤로 미룬다.
        System.arraycopy(elementDataA, index, 
elementDataA, index + 1, size - index); // index에 데이터 삽입. elementDataA[index] = data; size++; } /** * 지정한 숫자를 찾아서 제거한다. * @param num * @return Result -1:찾지못함 0:찾아서삭제함 */ public byte removeNum(byte num) { int cnt; /* * 루프를 돌며, 찾고자하는 데이터가 있는지 검사한다. */ for(cnt=0; cnt<size; cnt++) { if(num == elementDataA[cnt]) { removeData(cnt); return 0; } } return -1; } /** * 해당하는 index의 자료를 삭제한다. * @param idx * @return 삭제한 index의 data. */ public byte removeNumByIndex(int idx) { byte value; value = elementDataA[idx]; removeData(idx); return value; } /** * 지정한 index의 자료를 삭제한다. * @param idx */ private synchronized void removeData(int idx) { int numMoved = size - idx - 1; // 삭제 할 index 뒤의 데이터를 한칸씩 앞으로 당긴다. if (numMoved > 0) System.arraycopy(elementDataA, idx+1,
elementDataA, idx, numMoved); elementDataA[--size] = 0; // 맨 뒤 인덱스 초기화. } /** * 자료를 모두 초기화 한다. */ public void clear() { int cnt; for(cnt=0; cnt<size; cnt++) { elementDataA[cnt] = 0; } size=0; } /** * 지정한 index의 번호를 반환한다. * @param idx * @return index에 해당하는 data */ public byte getNum(int idx) { return elementDataA[idx]; } /** * 넣어둔 자료의 개수를 반환한다. * @return 자료개수 */ public int getCount() { return size; } }

BCollector 클래스는 숫자 배열을 좀더 편하게 관리하기 위해 만든 클래스입니다.

BCollector 객체 생성시, 디폴트 생성자를 호출한다면,

Default 생성자에서 기본으로 10개의 숫자 배열을 생성/관리해줍니다.

BCollector 객체 생성시, 객체의 갯수를 지정해 준다면, 해당 개수의 숫자 배열을 관리할수 있게 됩니다.


쭈욱 보면, 많은 함수가 있지만, 우리가 주의깊게 살펴보아야 할 함수는 몇개 되지 않습니다.

가장 먼저, 숫자 배열의 맨 뒤에 특정 값을 추가하는, addNum() 함수를 보면,

/**
 * 숫자를 추가한다.
 * @param num
 */
public void addNum(byte num)
{
    // 정렬을 해서 넣어야 한다면,
    if(bSort == true)
    {
        int cnt;

        for(cnt=0; cnt<size; cnt++)
        {
            if(num < elementDataA[cnt])
            {
                addData(num, cnt);
                return;
            }
        }
    }

    addData(num);
}

addNum() 함수는 만약, 정렬을 해서 넣어야 한다면, 해당 값이 들어가야 할 위치를 찾아서,

해당 위치에 삽입하고, 정렬을 할 필요가 없다면, 그냥 맨 마지막에 데이터를 삽입합니다.


다음으로, removeLast() 함수를 보시면,

/**
 * 맨뒤의 data를 삭제한다.
 * @return 삭제한 index의 data.
 */
public byte removeLast()
{
    byte value;

    value = elementDataA[size-1];
    removeData(size-1);

    return value;
}

removerLast() 함수는 맨 마지막 데이터 하나를 삭제 합니다.

removeData() 함수는 인자로 받은 인덱스의 데이터를 삽입해 주므로,

removeData() 함수에 현재 숫자 배열의 마지막 인덱스를 넘겨 줌으로써, 마지막 데이터를 삭제합니다.

마지막 데이터를 삭제 하기전에 value라는 변수에 해당 값을 저장했다가,

삭제가 된 후에 리턴 해 주어 현재 삭제된 데이터가 무엇인지 알려주도록 합니다.


이제 게임 시작 전에, 중앙에 쌓아 놓을 카드 배열을 room.GostopRoom.java에 다음과 같이 선언합니다.

protected BCollector suffleCards; // 섞은 카드를 모아두는곳(제일 윗 카드 : 0번)

그리고 GostopRoom::init() 함수에서 다음과 같이 카드 배열을 53개 미리 생성해 줍니다.

suffleCards = new BCollector(53); // 카드 53장(0~52)을 미리 만들어 놓음.

그리고, twogo1p.room 클래스에 카드를 섞어주는 generateCards() 라는 함수를 구현합니다.

/**
 * 카드를 섞는다.
 */
private void generateCards()
{
    byte cnt;
    byte temp;
    int randIdx;

    byte maxCard = getMaxCard();
    byte[] cards = new byte[maxCard];

    for(cnt=0; cnt<maxCard; cnt++)
    {
        cards[cnt]=cnt;
    }

    // 카드를 섞는 횟수 결정
    byte suffleCount = (byte)(Etc.getRandomInt(3)+1);

// shuffleCount에 지정한 수만큼 전체를 섞어준다. for(byte suffleCnt=0; suffleCnt<suffleCount; suffleCnt++) { // 전체 카드를 한번씩 섞어준다. for(cnt=0; cnt<maxCard; cnt++) { // 렌덤한 index를 얻어 온후, randIdx = Etc.getRandomInt(maxCard-1); /* * 렌덤하게 얻어온 Index와 * 현재 Index의 카드의 위치를 바꾸어준다. */ temp = cards[cnt]; cards[cnt] = cards[randIdx]; cards[randIdx] = temp; } } /* * 섞인 카드를 suffleCards에 저장한다. */ for(cnt=0; cnt<maxCard; cnt++) { suffleCards.addNum(cards[cnt]); } }

카드가 원래의 화투 패 48장과 추가 쌍피 3장, 추가 쓰리피 2장 합이 모두 53장이므로,

먼저 숫자형 배열 53개를 만들어서, 차례로 0부터 52까지의 수를 대입해 줍니다.


그리고 Etc 클래스의 getRandomInt() 함수는 0부터 해당 숫자-1까지의 수 중에서

아무 숫자나 렌덤하게 하나를 리턴해 주는 역할을 하는 함수입니다.


즉, getRandomInt(3)과 같이 해주면 0부터 2까지의 수를 렌덤하게 리턴해 주므로, 거기에 1을 더해,

화투를 최소 한번에서 세번까지 렌덤하게 섞어줍니다.


그 다음 부분이 바로 카드를 섞는 부분인데,

// 전체 카드를 한번씩 섞어준다.
for(cnt=0; cnt<maxCard; cnt++)
{
    // 렌덤한 index를 얻어 온후,
    randIdx = Etc.getRandomInt(maxCard-1);

    /*
     * 렌덤하게 얻어온 Index와 
     * 현재 Index의 카드의 위치를 바꾸어준다.
     */
    temp = cards[cnt];
    cards[cnt] = cards[randIdx];
    cards[randIdx] = temp;
}

카드의 수만큼 루프를 돌며 현재 Index와 렌점하게 얻어온 Index의 카드를 바꾸어 주는 동작을 합니다.

이걸 suffleCount 만큼 반복하면 골고루 자~알 섞이겠네요^^;

모두 섞었다면, 잘 섞인 카드를 suffleCards 배열에 저장합니다.


이제 고스톱 게임을 제작하기 위한 모든 준비가 끝났습니다.

이제 실제로 고스톱 게임 시스템을 구현하는 것만이 남았네요.



휴... 소스 코드의 양이 꽤 많은 데다가, 함수가 한 두개가 아니다보니,

소스 코드의 전부를 설명해 드리는 것은 거의 불가능 할 것 같아서,

중요하거나 혹은 헷갈릴 만한 요소가 있는 부분만을 설명했는데요.


아래의 링크를 클릭하셔서 소스 코드 전체를 다운 받으신 후,

강좌에서 설명 되지 않은 부분중에서 궁금하시거나 헷갈리시는 부분이 있다면,

언제든지 질문 남겨 주세요.


아래는 이번시간에 작성 된 소스의 실행 화면입니다.


< 플레이어의 턴 일때 >

< 컴퓨터의 턴 일때 >

< 플레이어의 획득 카드 표시 >

< 컴퓨터의 획득 카드 표시 >

< 게임이 끝났음 >

그럼 다음시간에 뵙도록 할께요.

 

Posted by maysent
: