Deep Studying

[프라우드넷] 원카드 서버 만들기(2) GameRoom 클래스 구성하기 본문

게임서버

[프라우드넷] 원카드 서버 만들기(2) GameRoom 클래스 구성하기

miniSeop 2022. 1. 30. 19:54

 

이전 포스트: 원카드 서버 만들기(1) 프로젝트 시작하기

 

이전 포스트에서는 프로젝트의 구상이나 사전 준비에 대해서만 얘기했습니다.

이번 포스트에서 본격적으로 프로젝트의 내용을 채워보겠습니다.

 

다만 공격카드나 7,J,Q,K와 같은 특수 카드의 처리는 이번 포스트의 내용에서는 제외하겠습니다.

게임을 시작하고, 카드를 내고, 카드를 드로우하는 플로우에만 초점을 두고 내용을 전개하도록 하고, 이후 포스트에서 디테일한 부분을 채워나가면 좋을 것 같습니다.

 

Common > Common.cs

하단에 보이는 GamePlayer 클래스를 추가해줍니다.

모든 플레이어는 핸드를 가지고있으며 카드를 내거나 뽑는게 자유로워야함으로 List를 사용했습니다.

 

using Nettention.Proud;

namespace OnecardCommon
{
    public class Vars
    {
        public static System.Guid m_Version = new System.Guid("{ 0x3ae33249, 0xecc6, 0x4980, { 0xbc, 0x5d, 0x7b, 0xa, 0x99, 0x9c, 0x7, 0x39 } }");
        public static int m_serverPort = 33334;

        static Vars()
        {

        }
	}

    public class User
    {
        static int UserId = 0;
        public HostID HostId { get; set; }
        public string UserName { get; set; }
        public int UserID { get; set; }
        public int RoomNumber { get; set; }
        public User(string UserName, HostID HostId)
        {
            UserID = ++UserId;
            this.UserName = UserName;
            this.HostId = HostId;
            RoomNumber = 0;
        }
        public User()
        {
            UserName = "Unknown";
            RoomNumber = 0;
        }
    }

    public class GameCard
        {
        public int shape;
        public int number;
        public GameCard()
        {

        }
        public GameCard(int number)
        {
            this.shape = number % 4 + 1;
            this.number = number / 4 + 1;
        }
        public int toNumber()
        {
            return (shape - 1) + (number - 1) * 4;
        }
        public string toString()
        {
            string shape = "";
            string number = "";
        switch (this.shape)
        {
            case 1:
                shape = "♠";
                break;
            case 2:
                shape = "♥";
                break;
            case 3:
                shape = "♣";
                break;
            case 4:
                shape = "◆";
                break;
            default:
                shape = "■";
                break;
        }
        switch (this.number)
        {
            case 0:
                number = " ";
                break;
            case 1:
                number = "A";
                break;
            case 10:
                number = "T";
                break;
            case 11:
                number = "J";
                break;
            case 12:
                number = "Q";
                break;
            case 13:
                number = "K";
                break;
            default:
                number = this.number.ToString();
                break;
            }
            return String.Format("{0}{1}", shape, number);
        }
        public bool Match(GameCard card)
        {
            return this.shape == card.shape || this.number == card.number;
        }
    }

// GamePlayer Class 추가
    public class GamePlayer
    {
        public User user;
        public List<GameCard> hand = new List<GameCard>();
        public GamePlayer()
        {

        }
        public GamePlayer(User user)
        {
            this.user = user;
        }
    }
}

 

GameRoom 클래스 세팅

Server > GameRoom.cs

using OnecardCommon;
namespace OnecardServer
{
	public class GameRoom
	{
		public int status;	// 0이면 대기중, 1이면 게임중
		public int turn;	// 현재 플레이어의 턴

		public List<GamePlayer> players = new List<GamePlayer>();

		public Stack<GameCard> usedDeck = new Stack<GameCard>();
		public Stack<GameCard> unusedDeck = new Stack<GameCard>();
		public GameCard lastCard = new GameCard();

		public void EnterPlayer(User user)
		{
		}
		public bool InitializeGame()
		{
			return true;
		}
		public void EndGame()
		{
			status = 0;
		}
		public void setNextTurn()
		{
			if (++turn == players.Count()) turn = 0;
		}
		public int DrawHand()
		{
			return 1;
		}
		public bool PlayHand(int hand)
		{
			return false;
		}
	}
}

 

 

멤버변수부터 살펴보자면 status와 turn은 주석의 내용과 같습니다.

players는 현재 게임에 참여중인 플레이어들 입니다.

usedDeck은 사용된 카드들, unusedDeck은 미사용된 카드들이 있는 곳입니다.

lastCard는 마지막으로 낸 카드입니다. usedDeck의 마지막 element를 이용하지 않고 따로 멤버변수로 둔 이유는 7을 냈을 때, 실제 카드의 속성은 변하지 않으면서 마지막 카드의 모양이 바뀌어야하기 때문입니다.

 

멤버 함수는 이전 포스트에서 계속 얘기했던 플레이어의 액션과 관련된 함수들입니다.

 

RoomList 추가 ( ServerLauncher.cs )

namespace OnecardServer
{
	public class ServerLauncher
	{
		...
        
		static public GameRoom[] RoomArray = new GameRoom[10];
        
		...
	}
}

서버에 1번~10번의 Room이 있다고 생각하고 Array를 생성하였습니다.

 

룸 생성하기 ( ServerLauncher.cs )


public void InitialzieServerParameter()
{
    var parameter = new StartServerParameter();
    parameter.protocolVersion = new Nettention.Proud.Guid(Vars.m_Version);
    parameter.tcpPorts.Add(Vars.m_serverPort);
    NetServer.Start(parameter);
    
    // 아래 내용 추가
    for (int i = 0; i < roomArray.Length; i++)
        RoomArray[i] = new GameRoom();
}

 유저가 방을 만들고 닫는 구조가 아니라 미리 생성되어있다고 가정하였으므로 배열에 GameRoom 객체를 모두 초기화해줍니다. 만약 방을 직접 만드려고 한다면 방을 생성하는 요청이 PIDL에 추가되어야 할 것입니다.

또한 동적으로 방을 관리한다면 방마다 ID를 붙여주고, RoomArray는 배열이 아니라 List가 되어야겠죠. 저는 이러한 과정이 사족이 될 것 같아 우선은 모두 배제하였습니다.

 

 

GameRoom 메서드 작성하기

bool EnterPlayer( User user )

유저가 방에 들어갈 때의 로직을 처리합니다. 들어가는데 성공했다면 true를, 실패했다면 false를 리턴합니다.

public bool EnterPlayer(User user)
{
    int numPlayer = players.Count();
    if (numPlayer >= 4) return false;

    GamePlayer newPlayer = new GamePlayer(user);

    players.Add(newPlayer);
    return true;
}

players 리스트를 조회하고 4명 이상이면 실패, 4명 미만이면 players 리스트에 새 유저를 추가합니다.

 

bool InitializeGame( )

게임을 시작하는 메서드입니다. 시작에 성공하면 true를, 실패하면 false를 반환합니다.

해야하는 일은 3가지입니다.

- status를 1로 바꿉니다. ( 게임중 상태로 바꿉니다. )

- 덱을 랜덤으로 세팅합니다. ( SetDeck 메서드 )

- 플레이어에게 카드를 나누어줍니다. ( SplitCard 메서드 )

 

public bool InitializeGame()
{
    if (status == 1 || players.Count < 2)
        return false;
    status = 1;
    Console.WriteLine("Game Start!");

    SetDeck();

    SplitCard();

    return true;
}

private void SetDeck()
{
    GameCard[] deck = new GameCard[53];
    Random random = new Random();
    
    // 배열에 카드를 랜덤으로 생성합니다.
    for (int i = 0; i < deck.Length - 1;)
    {
        int rand = random.Next(0, 52);
        if (deck[rand] == null) deck[rand] = new GameCard(i++);
    }
    
    // usedDeck에 카드를 추가합니다.
    for (int i = 0; i < deck.Length - 1; i++)
        usedDeck.Push(deck[i]);

    // usedDeck에 있는 카드를 섞어 unusedDeck으로 옮깁니다.
    MergeDeck();
}

private void MergeDeck()
{
    GameCard[] deck = new GameCard[53];
    Random random = new Random();

    while( usedDeck.Count() > 0)
    {
        int rand = random.Next(0, 52);
        if (deck[rand] == null)
        deck[rand] = usedDeck.Pop();
    }
    for(int i=0; i<deck.Length; i++)
    {
        if (deck[i] == null) continue;
        unusedDeck.Push(deck[i]);
    }
}

private void SplitCard()
{
    for (int j = 0; j < 7; j++)
        for (int i = 0; i < players.Count(); i++)
            players[i].hand.Add(unusedDeck.Pop());

    usedDeck.Push(unusedDeck.Pop());
    lastCard = usedDeck.Peek();
}

 

int DrawHand( )

카드 드로우를 처리합니다. 뽑은 카드의 개수를 리턴해줍니다.

public int DrawHand()
{
    GameCard gameCard = unusedDeck.Pop();
    players[turn].hand.Add(gameCard);
    return 1;
}

 

 원래라면 공격카드가 들어왔는지 여부를 확인해서 여러 장을 드로우 할 수 있게 처리해야겠지만 이번 포스트에서는 무조건 한 장을 드로우하도록 하겠습니다.

 

bool PlayHand( int hand )

카드를 냅니다. 유효한 카드라면 로직 수행 후 true를 리턴하고, 유효하지 않은 카드라면 false를 리턴합니다.

public bool PlayHand(int hand)
{
    // 핸드 개수 확인
    int currentHand = players[turn].hand.Count();
    if (hand < 0 || hand >= currentHand)
    	return false;

    GameCard gameCard = players[turn].hand[hand];

    bool isMatched = getLastCard().Match(gameCard);
    if (isMatched)
    {
        usedDeck.Push(gameCard);
        players[turn].hand.RemoveAt(hand);

        return true;
    }
    return false;
}

 이 메서드에서도 이전 카드가 공격카드인지 여부를 확인해서 낼 수 있는지 없는지 파악해야하지만 지금은 넘어가도록하겠습니다.

 

HostID[ ] GetHostIDs( ) 와 int GetPlayerID( User user )

 두 가지 메서드를 더 추가해야합니다.

하나는 해당 방에 있는 모든 플레이어에게 패킷을 전달하기 위해 HostID들을 가져오는 메서드입니다.

또 다른 하나는 유저의 정보를 이용해서 몇 번째 플레이어인지를 찾는 메서드입니다.

위 두 메서드는 뒤에 나올 Stub 메서드를 작성하는 데에 이용됩니다.

public HostID[] GetHostIDs()
{
    HostID[] users = players
        .Select(p => p.user.HostId)
        .ToArray();
    return users;
}
public int GetPlayerID(User user)
{
    GameRoom room = ServerLauncher.RoomArray[user.RoomNumber];
    return room.players.FindIndex(p => p.user.HostId == user.HostId);
}

 

위 내용을 모두 작성했다면 GameRoom.cs는 다음과 같습니다.

더보기
더보기
using Nettention.Proud;
using OnecardCommon;
namespace OnecardServer
{
    public class GameRoom
    {
        public int status;
        public int turn;

        public List<GamePlayer> players = new List<GamePlayer>();

        public Stack<GameCard> usedDeck = new Stack<GameCard>();
        public Stack<GameCard> unusedDeck = new Stack<GameCard>();
        public GameCard lastCard = new GameCard();

        public bool EnterPlayer(User user)
        {
            int numPlayer = players.Count();
            if (numPlayer >= 4) return false;

            GamePlayer newPlayer = new GamePlayer(user);

            players.Add(newPlayer);
            return true;
        }
        public bool InitializeGame()
        {
            if (status == 1 || players.Count < 2)
                return false;
            status = 1;

            SetDeck();

            SplitCard();

            return true;
        }

        private void SetDeck()
        {
            GameCard[] deck = new GameCard[53];
            Random random = new Random();

            // 배열에 카드를 랜덤으로 생성합니다.
            for (int i = 0; i < deck.Length - 1;)
            {
                int rand = random.Next(0, 52);
                if (deck[rand] == null) deck[rand] = new GameCard(i++);
            }

            // usedDeck에 카드를 추가합니다.
            for (int i = 0; i < deck.Length - 1; i++)
                usedDeck.Push(deck[i]);

            // usedDeck에 있는 카드를 섞어 unusedDeck으로 옮깁니다.
            MergeDeck();
        }

        private void MergeDeck()
        {
            GameCard[] deck = new GameCard[53];
            Random random = new Random();

            while (usedDeck.Count() > 0)
            {
                int rand = random.Next(0, 52);
                if (deck[rand] == null)
                    deck[rand] = usedDeck.Pop();
            }
            for (int i = 0; i < deck.Length; i++)
            {
                if (deck[i] == null) continue;
                unusedDeck.Push(deck[i]);
            }
        }

        private void SplitCard()
        {
            for (int j = 0; j < 7; j++)
                for (int i = 0; i < players.Count(); i++)
                    players[i].hand.Add(unusedDeck.Pop());

            usedDeck.Push(unusedDeck.Pop());
            lastCard = usedDeck.Peek();
        }
        public void EndGame()
        {
            status = 0;
        }

        public void setNextTurn()
        {
            if (++turn == players.Count())
            turn = 0;
        }
        public int DrawHand()
        {
            GameCard gameCard = unusedDeck.Pop();
            players[turn].hand.Add(gameCard);
            return 1;
        }
    
        public bool PlayHand(int hand)
        {
            // 핸드 개수 확인
            int currentHand = players[turn].hand.Count();
            if (hand < 0 || hand >= currentHand)
                return false;

            GameCard gameCard = players[turn].hand[hand];

            bool isMatched = getLastCard().Match(gameCard);
            if (isMatched)
            {
                usedDeck.Push(gameCard);
                players[turn].hand.RemoveAt(hand);

                return true;
            }
            return false;
        }

        public HostID[] GetHostIDs()
        {
            HostID[] users = players
                .Select(p => p.user.HostId)
                .ToArray();
            return users;
        }
        public int GetPlayerID(User user)
        {
            GameRoom room = ServerLauncher.RoomArray[user.RoomNumber];
            return room.players.FindIndex(p => p.user.HostId == user.HostId);
        }

        public GameCard getLastCard()
        {
            return usedDeck.Peek();
        }

    }
}

 

테스트 해보기 ( ServerLauncher.cs )

Stub과 Proxy를 구성하기 전에 Initialize( ) 함수가 잘 실행되나 출력해보고 넘어가겠습니다. ServerLauncher의 ServerStart( )함수 안에 다음과 같은 내용을 임시로 작성해주세요. 테스트가 끝나면 내용을 지워주세요

 

public void ServerStart()
{
    InitializeHandler();
    InitializeStub();
    InitialzieServerParameter();
    RunLoop = true;

    // 이하 내용 추가 
    GameRoom tempRoom = new GameRoom();
    tempRoom.EnterPlayer(new User());
    tempRoom.EnterPlayer(new User());
    tempRoom.EnterPlayer(new User());
    tempRoom.EnterPlayer(new User());

    tempRoom.InitializeGame();
    
    // 플레이어 핸드 출력
    for (int i = 0; i < tempRoom.players.Count; i++) {
        String str = String.Format("Player{0} :", i);
        for (int j = 0; j < tempRoom.players[i].hand.Count; j++)
            str += tempRoom.players[i].hand[j].toString() + "  ";
        Console.WriteLine(str);
    }
    // 처음 세팅된 카드 출력
    Console.WriteLine("First Card: " + tempRoom.lastCard.toString());
    // 미사용 덱 출력
    string deck = "Unused:";
    while (tempRoom.unusedDeck.Count > 0)
        deck += tempRoom.unusedDeck.Pop().toString() + "  ";
    Console.WriteLine(deck);
}

 

테스트 결과

테스트 해보면 랜덤하게 카드가 배분되고 있는 것을 확인할 수 있습니다.

또한 플레이어 수를 다르게 해도 출력된 모든 카드 개수를 합쳐보면 52장이 되는 것을 볼 수 있습니다.

 

실행 결과 1
실행 결과 2
실행 결과 3

 

 

Client -> Server Stub 구성하기

InitStub( )

저번 포스트에서 작성한 GameRoomProcessor.cs의 내용에 InitStub( )을 추가해줍니다.

using Nettention.Proud;
using OnecardCommon;

namespace OnecardServer.process
{
    public class GameRoomProcess : CommonProcess
    {
        public new void InitStub()
        {
            C2SStub.Start = Start;
            C2SStub.PlayCard = PlayCard;
            C2SStub.DrawCard = DrawCard;
            C2SStub.ChangeShape = ChangeShape;
        }
        static public bool Start(HostID remote, RmiContext rmiContext)
        {
            return true;
        }
        static public bool PlayCard(HostID remote, RmiContext rmiContext, GameCard card)
        {
            return true;
        }
        static public bool DrawCard(HostID remote, RmiContext rmiContext)
        {
            return true;
        }
        static public bool ChangeShape(HostID remote, RmiContext rmiContext, int shape)
        {
            return true;
        }
    }
}

static으로 선언된 C2SStub과 S2CProxy를 사용하기 위해 CommonProcess를 extend하였습니다.

또한 InitStub( )을 오버라이딩 해서 C2SStub에 메서드들을 등록해줍니다.

 

Start

위에서 테스트한 InitializeGame 함수로 게임을 시작합니다.

게임이 시작되었다면 NotifyStartGame함수로 플레이어들에게 게임이 시작되었단 사실을 알립니다.

그리고 어떤 카드를 나눠받았는지 각각 플레이어들에게 알립니다.

 

static public bool Start(HostID remote, RmiContext rmiContext)
{
    ServerLauncher.UserList.TryGetValue(remote, out User user);
    int RoomNumber = user.RoomNumber;
    if (RoomNumber == 0) return true;

    GameRoom room = ServerLauncher.RoomArray[RoomNumber];

    // 실패 처리
    bool isSuccess = room.InitializeGame();
    if (!isSuccess)
        return true;
        
    // 성공 처리
    // Game시작 알리기
    S2CProxy.NotifyStartGame(room.GetHostIDs(), RmiContext.ReliableSend, 0, room.lastCard);
    for(int i = 0; i < room.players.Count; i++)
    {
        // 플레이어 각각에게 핸드 목록 알리기
        GamePlayer player = room.players[i];
        S2CProxy.ResponseDraw(player.user.HostId, rmiContext, player.hand);
    }
    return true;
}

 

PlayCard

플레이어가 카드를 냈을 때 호출하는 Stub메서드입니다.

유저가 방에 들어가있는지, 자신의 턴인지 등을 판단하고 올바른 요청일 경우 이를 처리합니다.

static public bool PlayCard(HostID remote, RmiContext rmiContext, GameCard card)
{

    ServerLauncher.UserList.TryGetValue(remote, out User user);
    int RoomNumber = user.RoomNumber;
    // 방에 들어가지 않은 유저
    if (RoomNumber == 0)
        return true;

    GameRoom room = ServerLauncher.RoomArray[RoomNumber];
    int playerID = room.GetPlayerID(user);
    
    // 자신의 턴이 아닌 플레이어
    if (playerID != room.turn)
        return true;

    GamePlayer player = room.players[playerID];
    int countHand = player.hand.Count;
    int hand = player.hand.FindIndex(p => p.toNumber() == card.toNumber());
    bool isSuccess = room.PlayHand(hand);
 
    // 유효하지 않은 카드
    if (!isSuccess)
        return true;

    var hosts = room.GetHostIDs();
    S2CProxy.ChangeLastCard(hosts, rmiContext, room.getLastCard());
    S2CProxy.ChangeHand(hosts, rmiContext, playerID, player.hand.Count());

    // 승리
    if (countHand == 1)
    {
        room.EndGame();
        S2CProxy.NotifyEndGame(hosts, rmiContext, playerID);
    }
    // 다음 턴으로
    else
    {
        room.setNextTurn();
        S2CProxy.ChangeTurn(hosts, rmiContext, room.turn);
    }

    return true;
}

 

 

DrawCard

플레이어가 카드를 드로우했을 때 호출하는 Stub메서드입니다.

위의 PlayCard와 같이 유효한 요청인지 검증합니다.

 static public bool DrawCard(HostID remote, RmiContext rmiContext)
{
    ServerLauncher.UserList.TryGetValue(remote, out User user);
    int RoomNumber = user.RoomNumber;
    if (RoomNumber == 0)
        return true;

    GameRoom room = ServerLauncher.RoomArray[RoomNumber];
    int playerID = room.GetPlayerID(user);

    if (playerID != room.turn)
        return true;
    
    GamePlayer player = room.players[playerID];
    int count = room.DrawHand();
    if (count > 0)
    {
        var hosts = room.GetHostIDs();
        S2CProxy.ChangeHand(hosts, rmiContext, playerID, player.hand.Count());
        S2CProxy.ResponseDraw(player.user.HostId, rmiContext, player.hand);
        room.setNextTurn();
        S2CProxy.ChangeTurn(hosts, rmiContext, room.turn);
    }
    
    return true;
}

 

완성된 GameRoomProcess.cs 파일은 다음과 같습니다.

더보기
더보기

 

using Nettention.Proud;
using OnecardCommon;

namespace OnecardServer.process
{
    public class GameRoomProcess : CommonProcess
    {
        public new void InitStub()
        {
            C2SStub.Start = Start;
            C2SStub.PlayCard = PlayCard;
            C2SStub.DrawCard = DrawCard;
            C2SStub.ChangeShape = ChangeShape;
        }
        static public bool Start(HostID remote, RmiContext rmiContext)
        {
            ServerLauncher.UserList.TryGetValue(remote, out User user);
            int RoomNumber = user.RoomNumber;
            if (RoomNumber == 0) return true;

            GameRoom room = ServerLauncher.RoomArray[RoomNumber];

            bool isSuccess = room.InitializeGame();
            if (!isSuccess)
                return true;

            S2CProxy.NotifyStartGame(room.GetHostIDs(), RmiContext.ReliableSend, 0, room.lastCard);
            for (int i = 0; i < room.players.Count; i++)
            {
                GamePlayer player = room.players[i];
                S2CProxy.ResponseDraw(player.user.HostId, rmiContext, player.hand);
            }
            return true;
        }
        static public bool PlayCard(HostID remote, RmiContext rmiContext, GameCard card)
        {
            ServerLauncher.UserList.TryGetValue(remote, out User user);
            int RoomNumber = user.RoomNumber;
            if (RoomNumber == 0)
                return true;

            GameRoom room = ServerLauncher.RoomArray[RoomNumber];
            int playerID = room.GetPlayerID(user);

            if (playerID != room.turn)
                return true;
            
            GamePlayer player = room.players[playerID];
            int countHand = player.hand.Count;
            int hand = player.hand.FindIndex(p => p.toNumber() == card.toNumber());
            bool isSuccess = room.PlayHand(hand);
            if (!isSuccess)
                return true;

            var hosts = room.GetHostIDs();
            S2CProxy.ChangeLastCard(hosts, rmiContext, room.getLastCard());
            S2CProxy.ChangeHand(hosts, rmiContext, playerID, player.hand.Count());

            if (countHand == 1)
            {
                room.EndGame();
                S2CProxy.NotifyEndGame(hosts, rmiContext, playerID);
            }
            else
            {
                room.setNextTurn();
                S2CProxy.ChangeTurn(hosts, rmiContext, room.turn);
            }
            
            return true;
        }
        static public bool DrawCard(HostID remote, RmiContext rmiContext)
        {
            ServerLauncher.UserList.TryGetValue(remote, out User user);
            int RoomNumber = user.RoomNumber;
            if (RoomNumber == 0)
                return true;

            GameRoom room = ServerLauncher.RoomArray[RoomNumber];
            int playerID = room.GetPlayerID(user);

            if (playerID != room.turn)
                return true;

            GamePlayer player = room.players[playerID];
            int count = room.DrawHand();
            if (count > 0)
            {
                var hosts = room.GetHostIDs();
                S2CProxy.ChangeHand(hosts, rmiContext, playerID, player.hand.Count());
                S2CProxy.ResponseDraw(player.user.HostId, rmiContext, player.hand);
                room.setNextTurn();
                S2CProxy.ChangeTurn(hosts, rmiContext, room.turn);
            }

            return true;
        }
        static public bool ChangeShape(HostID remote, RmiContext rmiContext, int shape)
        {
            return true;
        }
    }
}

 

 

이제 서버쪽의 인게임 구성을 마쳤습니다.

다음 포스트에서는 클라이언트의 Stub메서드와 함께 아웃게임 구성 요소들을 작성해보겠습니다.