본문 바로가기

C# WPF

C# WPF 카드게임(Memory Card Game)

Memory Card Game은 카드 52장을 뒤집어두고, 마우스로 클릭하여 두장씩 같은 숫자가 나오면 open되는 게임입니다. 기억력, 집중력 향상이 도움이 되기 때문에 Memory Card Game이라고 이름 붙였습니다.모든 카드가 open 되면 게임이 끝나고, 몇번 클릭했는지가 점수가 됩니다.

4개의 버튼이 있습니다.

  • Hint 버튼은 뒤집힌 카드를 열어서 보여줍니다. 시작부터 또는 중간에 언제라도 Hint 버튼을 누를 수 있습니다.
  • Resume 버튼은 Hint 버튼으로 일시 중단된 게임을 계속합니다.
  • Start 버튼은 다시 게임을 시작할 때 사용합니다.
  • Quit 버튼은 게임을 끝낼 때 사용합니다.

Xaml 파일은 다음과 같습니다.

[MainWindow.xaml]

<Window x:Class="card.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="580" Width="1040" Background="Green">
    <Grid Width="1004" Height="540" >
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30" FontWeight="Bold" FontStyle="Italic" Foreground="WhiteSmoke">Memory Card Game</TextBlock>
        <Line Stroke="LightSteelBlue" Grid.Row="0" VerticalAlignment="Bottom" Stretch="Fill" X2="950"></Line>
        <Grid Name="board" ShowGridLines="False" Grid.Row="1"  Background="Green"></Grid>
        <Line Stroke="LightSteelBlue" Grid.Row="2" VerticalAlignment="Top" Stretch="Fill" X2="950"></Line>
        <StackPanel Grid.Row="2" Orientation="Horizontal" >
            <Button Width="100" Height="30" FontSize="12" Margin="30,5" VerticalContentAlignment="Center" HorizontalAlignment="Center" Click="btnHint_Click">Hint</Button>
            <Button Width="100" Height="30" FontSize="12" Margin="30,5" VerticalContentAlignment="Center" HorizontalAlignment="Center" Click="btnStart_Click">Start</Button>
            <Button Width="100" Height="30" FontSize="12" Margin="30,5" VerticalContentAlignment="Center" HorizontalAlignment="Center" Click="btnResume_Click">Resume</Button>
            <Button Width="100" Height="30" FontSize="12" Margin="30,5" VerticalContentAlignment="Center" HorizontalAlignment="Center" Click="btnQuit_Click">Quit</Button>
            <TextBlock Name="tbClicks" Margin="30,5" FontSize="12" Foreground="WhiteSmoke" VerticalAlignment="Center" >Clicks = 0</TextBlock>
            <TextBlock Margin="160,5" VerticalAlignment="Center" FontSize="12" FontStyle="Italic" Foreground="WhiteSmoke">BeeEye Dmu</TextBlock>
        </StackPanel>
        </Grid>
</Window>

Xaml 파일에서는 디자인과 코드에서 사용할 콘트롤의 이름을 정해줍니다.

다음은 초기 화면의 모습입니다.

아래 그림은 게임이 진행되는 중간의 모습입니다. 카드가 52개나 되니 쉽게 맞추지 못하겠습니다. 색깔과는 관계없이 숫자만 맞추면 오픈이 되게 했습니다. 색깔까지 맞추게 하면 더 어렵겠지요?

여기에서 Hint버튼을 누르면 뒤집혀져 있는 카드가 오픈되어 보입니다. 반대로 오픈된 카드는 뒤집혀보이게 했습니다. 이렇게 하는게 더 알아보기 쉽습니다.

Resume 버튼을 누르면 다시 계속해서 게임을 진행할 수 있고, Start 버튼은 아예 처음부터 새로 게임을 시작하게 합니다.

MainWindow.xaml.cs 파일의 code를 살펴보겠습니다.   

먼저 자료구조입니다. 솔루션탐색기에서 이미지 파일들이 들어있는 image1 폴더를 추가합니다. cardName[0] 는 뒤집힌 카드의 이미지 이름, card[1]~card[52] 에는 스페이드, 하트, 다이아몬드, 클럽의 첫자(S,H,D,C)에 숫자를 표시하는 문자(A,J,Q,K) 또는 숫자(2~10)가 더해진 이름을 갖습니다.

     // card number에 대응하는 이미지 파일의 이름들

private string[] cardName = { "Back",
        "SA", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "SJ", "SQ", "SK",
        "HA", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", "HJ", "HQ", "HK",
        "DA", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "DJ", "DQ", "DK",
        "CA", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "CJ", "CQ", "CK"};

카드의 위치는 4열 13행의 2차원 배열에 카드 번호를 넣어서 표시합니다. 이때 이차원배열에 저장되는 숫자는 카드번호로 위의 cardName의 인덱스에 해당합니다.

  private int[,] cards = new int[4, 13]; // card의 배치표

카드가 open 상태인지 hidden 상태인지를 표시하기 위해서 cardOpened[] 라는 bool 배열을 사용합니다. 마찬가지로 이 배열의 인덱스는 cardName의 인덱스입니다.

  private bool[] cardOpened = new bool[53]; // open 된 카드이면 true를 표시하는 flag 배열

다음은 메인 함수입니다.

        public MainWindow()
        {
            InitializeComponent();

            IntializeCardOpened();      // 카드를 모드 hidden으로 표시 
            CardSlot();                      // grid를 [4,13] 으로 나누어 줌
            RandomCards();              // 카드를 랜덤하게 위치시킴
            DrawBoard();                  // 카드를 그려줌
        }

        // flag 초기화
        private void IntializeCardOpened()
        {
            for (int i = 0; i <= 52; i++)
                cardOpened[i] = false;
        }

        // [4, 13]으로 grid를 나눔
        private void CardSlot()
        {
            for (int i = 0; i < 4; i++)
            {
                RowDefinition row = new RowDefinition();
                board.RowDefinitions.Add(row);
            }
            for (int i = 0; i < 13; i++)
            {
                ColumnDefinition col = new ColumnDefinition();
                board.ColumnDefinitions.Add(col);
            }
        }

        // card를 random 하게 배치
        private void RandomCards()
        {           
            int[] flag = new int[53];
            Random r = new Random();

            for (int row = 0; row < 4; row++)
                for (int col = 0; col < 13; col++)
                {
                    int n = r.Next(1, 53);
                    while (flag[n] != 0)
                        n = r.Next(1, 53);
                    flag[n] = 1;
                    cards[row, col] = n;
                }
        }

        // 전체 카드를 그려줌
        private void DrawBoard()
        {
            for(int row=0; row<4; row++)
                for(int col=0; col<13; col++) {                   
                    DrawCard(row, col, cards[row, col], cardOpened[cards[row,col]]);                   
                }
        }

        // cards[row,col] 에 있는 p 번 카드를 그려줌, p_2는 flag(open/close)
        // 이미지가 솔루션탐색기에서 추가된 image1 폴더에 있음
        private void DrawCard(int row, int col, int p, bool p_2)
        {
            BitmapImage card = new BitmapImage();
            string uri = "";

            if (p_2 == true)    // open
            {
                uri = "pack://application:,,/image1/";
                uri += cardName[p] + ".png";
            }
            else // hidden
                uri = "pack://application:,,/image1/Back.png";

          card.BeginInit();
          card.UriSource = new Uri(uri);
          card.EndInit();

            Image myImage = new Image();
            myImage.Source = card;
            myImage.Margin = new Thickness(3);
            myImage.MouseDown += myImageMouseDown;
            myImage.Tag = row * 100 + col;

            board.Children.Add(myImage);
            Grid.SetRow(myImage, row);
            Grid.SetColumn(myImage, col);
        }

코드에서 이미지를 삽입하려면 pack://application:{경로}/{image name} 으로 써야합니다.

위의 코드에서 보는 것 처럼 BitmapImage를 표시하기 위해서는 BeginInit()와 EndInit() 사이에 UriSource 를 써줍니다. 각 이미지에 MouseDown 이벤트 함수인 myImageMouseDown을 추가하고, Tag에 row*100 + col 으로 좌표값을 코딩하여 넣어줍니다. Grid.SetRow, Grid.SetColumn 으로 좌표값에 해당하는 그리드에 해당 그림을 그립니다.

제일 중요한 부분인 myImageMouseDown 이벤트 처리 메소드입니다. 이 부분이 제법 복잡합니다.

먼저 카드하나를 누르면, 이미지 Tag에 코딩했던 위치를 해독하여 row, col을 찾아냅니다. 이제부터는 2개씩 쌍을 이루어 두개의 이미지가 같은 숫자인지를 체크하고, 같으면 open 해주면 되겠습니다. 두번째 카드가 눌리면, 첫번째 카드와 두번째 카드가 같은 번호인지를 체크하는 checkMatch(firstCard, sndCard); 메소드를 부릅니다. 이를 위해 카드번호, row, col 정보를 담고 있는 card 클래스를 하나 만들었습니다.

        // image 위에서 MouseDown event 처리 메소드
        private void myImageMouseDown(object sender, EventArgs e)
        {
            Image img = sender as Image;
           
            int row = Int32.Parse(img.Tag.ToString()) / 100;
            int col = Int32.Parse(img.Tag.ToString()) % 100;

            // 이미 매치되어 오픈된 카드는 다시 선택되지 않도록 함
            if (cardOpened[cards[row, col]] == true)
                return;

            first = !first;

            // 현재 뒤집은 카드(firstCard)를 다시 클릭하는 경우를 체크
            if (first == false && cards[row, col] == firstCard.num)
            {
                DrawCard(row, col, cards[row, col], false);
                firstCard = new card(-1, -1, -1);
                return;
            }

            DrawCard(row, col, cards[row, col], true);
            clicks++;
            tbClicks.Text = "Clicks = " + clicks.ToString();
            
            if (first == false)  // 두번째 눌림
            {
                card sndCard = new card();
                sndCard.num = cards[row, col];
                sndCard.row = row;
                sndCard.col = col;
                checkMatch(firstCard, sndCard);           
            }
            else
            {
                firstCard.num = cards[row, col];
                firstCard.row = row;
                firstCard.col = col;
            }
        }

checkMatch 메소드에서는 두 카드의 번호를 13으로 나눈 나머지가 카드의 번호이므로 이를 비교하여 같으면 cardOpened[] flag를 true로 바꾸어 주고, 52장이 모두 찾아졌는지 체크해서 프로그램을 종료시킵니다.

두 카드가 서로 다른 번호라면, 두번째 카드가 open 된 후, 잠시 유지되다가 hidden 으로 바뀌어야합니다. 이렇게 하지 않으면 카드가 열리자마자 닫히므로 무슨 카드인지 알아 볼 수가 없습니다. 이를 위해 Thread.Sleep() 메소드를 사용하는데 WPF에서는 꼭 다음과 같이 Dispatcher.Invoke를 먼저 써주어야 합니다. 단위는 밀리초이며 Thread.Sleep(300); 이라고 하면 0.3초간 다음 문장의 실행을 연기합니다.

        private void checkMatch(card fCard, card sCard)
        {
            if (fCard.num % 13 == sCard.num % 13)
            {
                cardOpened[fCard.num] = true;
                cardOpened[sCard.num] = true;
                matched += 2;
                if (matched == 52)
                {
                    MessageBoxResult result = MessageBox.Show(
                        "Congraturations! Complete in " + clicks.ToString() + " clicks.\nWant to Play Again?",
                        "Completed!", MessageBoxButton.YesNo, MessageBoxImage.Exclamation, MessageBoxResult.Yes);
                    if (result == MessageBoxResult.Yes)
                    {
                        RandomCards();
                        btnStart_Click(null, null);
                    }
                    else
                        this.Close();
                }
            }
            else
            {
                Delay(300);
                DrawCard(fCard.row, fCard.col, fCard.num, false);
                DrawCard(sCard.row, sCard.col, sCard.num, false);              
            }
        }

        // delay in WPF
        private void Delay(int millisec)
        {
            this.Dispatcher.Invoke((ThreadStart)(() => { }), DispatcherPriority.ApplicationIdle);
            Thread.Sleep(millisec);
        }

card의 num, row, col을 저장하기 위해 class를 정의했습니다.

        int clicks = 0;    // 몇번 클릭했는지 카운트

        class card
        {
            public int num;
            public int row;
            public int col;

            public card(int n, int r, int c)
            {
                num = n;   row = r;   col = c;
            }
            public card()  { }
        }

        card firstCard = new card(-1, -1, -1);  // 초기화 num, row, col 모두 0에서 시작하므로 -1로 초기화
        private int matched = 0;    // 맞춘 카드 수
        bool first = false;  // 첫번째 카드냐? 아니면 false, 맞으면 true

버튼 4개의 이벤트 처리 메시지는 다음과 같습니다.

       private void btnQuit_Click(object sender, RoutedEventArgs e)
        {
            this.Close();
        }

        // 게임 중 언제라도 뒤집힌 카드의 앞면을 보여줌
        private void btnHint_Click(object sender, RoutedEventArgs e)
        {
            for (int row = 0; row < 4; row++)
                for (int col = 0; col < 13; col++)
                {
                    if(cardOpened[cards[row,col]] == false)
                        DrawCard(row, col, cards[row, col], true);
                    else
                        DrawCard(row, col, cards[row, col], false);
                }
        }

        // 게임 계속
        private void btnResume_Click(object sender, RoutedEventArgs e)
        {
            DrawBoard();
        }

        // 새로 시작
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            for (int i = 0; i <= 52; i++)
                cardOpened[i] = false;
            DrawBoard();
            tbClicks.Text = "Clicks = 0";
            clicks = 0;
            matched = 0;
        }

코드를 정리해 보니 간단한데, 코딩은 생각보다 쉽지 않았습니다. 고민해서 사용해보기 바랍니다.

BeeEye Dmu

'C# WPF' 카테고리의 다른 글

C#, WPF 점선그리는 법  (0) 2013.11.27
C# WPF delay 주는 방법  (0) 2013.11.22
C# WPF SnakeBite Game  (0) 2013.11.21
C#에서 실행시간 체크  (0) 2013.11.21
Thread.Sleep() 문제  (0) 2013.11.14