13-3-라.Quiz 게임

구조체를 배웠으니 종합 실습편의 예제를 하나 분석해 보도록 하자. Quiz 게임은 구조체 배열에 여러 가지 문제에 대한 정보들을 작성해 놓고 난수로 문제를 선택하여 출제하는 프로그램이다. 전체 소스는 다음과 같다. 별로 길지도 않으며 모든 코드는 main 함수에 작성되어 있으므로 분석하기 편하도록 되어 있다.

 

: Quiz

#include <Turboc.h>

#include <ctype.h>

 

// 문제 하나에 대한 정보를 가지는 구조체

struct tag_Munje {

     char *Question;

     char *Case[3];

     int Answer;

     BOOL Used;

};

 

// 문제 배열

struct tag_Munje Munje[]={

     {"다음 타입중 기본형이 아닌 것은","정수형", "배열","문자형",2,FALSE},

     {"다음 중 반복문이 아닌 것은","switch", "for","while",1,FALSE},

     {"구조체를 선언할 때 사용하는 키워드는","int", "goto","struct",3,FALSE},

     {"다음 중 가장 크기가 큰 타입은","int", "double","char",2,FALSE},

     {"열거형을 선언할 때 사용하는 키워드는","enum", "alias","define",1,FALSE},

     {"다음 중 단항 연산자는","++", "?","=",1,FALSE},

     {"문자 배열의 내용을 바꿀 때 사용하는 함수는","strassign", "strmove","strcpy",3,FALSE},

     {"구조체의 멤버를 읽을 때 사용하는 연산자는","->", "*",".",3,FALSE},

     {"정수형 타입이 아닌 것은","unsigned", "short","float",3,FALSE},

     {"C 프로그램에서 반드시 필요한 함수는","entry", "main","WinMain",2,FALSE},

};

 

void main()

{

     int num;

     int count;

     int i;

     char ch;

 

     randomize();

     for (count=0;count<5;count++) {

          // 출제되지 않은 문제 하나를 고른다.

          do {

               num=random(sizeof(Munje)/sizeof(Munje[0]));

          } while (Munje[num].Used == TRUE);

          Munje[num].Used=TRUE;

 

          // 문제를 출력한다.

          clrscr();

          gotoxy(2,2);

          printf("%s?",Munje[num].Question);

          for (i=0;i<3;i++) {

              gotoxy(2,5+i*2);

              printf("(%d) %s",i+1,Munje[num].Case[i]);

          }

          gotoxy(2,12);

          printf("1,2,3 중 하나를 선택하세요.끝낼때는 Q : ");

 

          // 정답을 입력받아 판정해 준다.

          ch=getch();

          if (tolower(ch) == 'q') {

              break;

          }

          ch=ch-'0';

          gotoxy(2,15);

          if (ch == Munje[num].Answer) {

              printf("정답입니다.");

          } else {

              printf("틀렸습니다. 정답은 %d번입니다.",Munje[num].Answer);

          }

          delay(1000);

     }

 

     gotoxy(2,17);

     printf("수고하셨습니다.\n");

}

 

tag_Munje 타입은 문제 하나에 대한 정보를 저장할 수 있는 구조체로 정의되어 있다. 문제는 질문과 답보기, 그리고 정답으로 구성되며 이외에 중복 문제 출제를 방지하기 위한 Used 멤버가 포함되어 있다. 질문인 Question은 문자열이므로 char *형으로 선언되어 있으며 답보기인 Case도 문자열이되 3개의 답보기를 보여줄 것이므로 크기 3의 char * 배열로 선언했다. Case1, Case2, Case3를 각각의 멤버로 선언할 수도 있지만 반복적인 출력을 위해 배열을 사용하는 것이 더 효율적이다.

정답인 Answer 멤버는 세 개의 답보기 중 어떤 것이 정답인지 답보기 번호를 기억하므로 정수형이면 충분하다. 사실 가능한 값이 1, 2, 3 중 하나이기 때문에 int형 대신 short나 char로 선언해도 정답을 기억하는데는 부족하지 않다. Used 멤버는 이 문제가 이전에 출제되었는지 아닌지를 기억하는데 최초 모두 FALSE값으로 초기화되며 문제가 출제되면 TRUE로 변경된다. 문제 선택 루틴에서는 Used가 TRUE인 문제는 채택하지 않음으로써 이미 출제된 문제가 다시 출제되지 않도록 하였다. Used 멤버처럼 실행중에만 사용되는 정보를 런타임 데이터라고 하는데 파일로 영구적으로 저장할 때는 대상에서 제외된다.

tag_Munje 타입을 정의한 후 이 타입의 구조체 배열 Munje를 초기화하는데 앞에서 알아본 바대로 { } 괄호안에 각 멤버의 초기값을 나열하기만 하면 된다. 한 행에 한 문제씩 질문과 세 개의 답보기, 그리고 정답을 기록했으며 Used 멤버는 모두 FALSE로 초기화했다. Munje 배열의 크기값은 생략했는데 이렇게 해 두면 컴파일러가 초기값의 개수를 헤아려서 배열의 크기를 자동으로 계산해 줄 것이며 문제를 더 늘리고 싶다면 초기식의 끝에 초기값만 계속 입력하면 된다.

표현하고자 하는 정보가 어떤 식으로 구성되어 있는지를 관찰해 보면 원하는 자료 구조를 금방 설계할 수 있다. 이 프로그램이 사용할 문제는 질문, 답보기, 정답 등 각각 다른 타입의 변수들을 필요로 하므로 배열로는 표현할 수 없으며 구조체가 필요하다. 또한 이런 문제들이 여러 개 필요하므로 구조체 배열이 되어야 하는데 그 결과가 바로 코드의 Munje 구조체 배열이다. 자료 구조가 다 작성되었으면 다음으로 코드를 작성하는데 main 함수의 구조는 다음과 같다.

 

for (count...) {

     문제 선택

     선택된 문제 출력

     정답 입력 및 판정

}

 

제어 변수 count로 for 루프를 다섯 번 돌도록 했으므로 문제는 총 다섯 개가 출제된다. 각 문제 출제 루틴은 문제 선택, 출력, 판정으로 세분되는데 먼저 문제를 선택하는 do~while 루프를 보자. num 변수에 출제할 문제를 선택하는데 무작위로 문제를 출제하기 위해 random 함수를 사용했다. 출제될 문제의 범위는 0~Munje 구조체의 크기까지인데 이 프로그램은 총 10개의 문제를 정의하고 있으므로 0~9 중 하나가 선택될 것이다.

num에 출제할 문제를 무작위로 선택했다고 해서 이 문제를 곧바로 출력하지 않으며 while문의 조건식에서 이미 출제된 문제인지 점검한다. 모든 문제들의 Used멤버는 FALSE로 초기화되어 있으며 출제하기 직전에 TRUE로 변경한다. 그래서 Used 멤버가 TRUE이면 이미 출제된 문제이므로 다른 문제를 고르도록 했다. 방금 출제한 문제가 다시 출제된다면 별로 보기에 좋지 않을 것이다.

do~while 루프는 출제할 문제를 무작위로 선택하되 이미 출제된 문제는 제외한다. 난수로 num을 고른 후에 이 문제가 출제되었는지 점검해야 하므로 선실행 후평가 반복문인 do~while 루프가 가장 적당하다. do~while루프에서 출제할 문제를 선택하면 다음 번에 이 문제가 다시 출제되지 않도록 Used멤버에 TRUE를 대입해 놓는다.

문제를 화면으로 출력하는 코드는 아주 간단하다. 이전에 출력된 문제를 지우기 위해 clrscr 함수를 먼저 호출하고 적당한 위치로 커서를 옮긴 후 Question, Case 등을 출력한다. 답보기 멤버인 Case는 배열이므로 루프를 돌면서 차례대로 출력했다. 각 문자열이 출력될 수평 좌표는 모두 2이며 수직 좌표는 다음과 같다.

질문이 (2,2)에 출력되고 답보기들은 차례대로 (2,5), (2,7), (2,9)에 한칸씩 여백을 두면서 출력했다. 이때 제어 변수 i값인 0, 1, 2와 각 답보기의 수직 출력좌표 5, 7, 9사이에는 5+i*2라는 함수 관계를 찾을 수 있으므로 이 식을 gotoxy의 두 번째 인수로 사용했다. 제어 변수 i는 0부터 시작하지만 답보기 번호는 1부터 시작하므로 %d 서식에 대응되는 번호는 i+1로 주어야 한다.

문제를 출력한 후 사용자로부터 정답을 입력받는데 이때는 키를 누르는 즉시 값을 돌려주는 getch 함수를 사용하는 것이 가장 좋다. scanf로도 정답을 입력받을 수 있지만 이 함수는 반드시 Enter를 눌러야 하므로 불편하다. getch로 문자를 입력받은 후 먼저 이 값이 'Q'나 또는 소문자 'q'인지를 점검해 보고 그렇다면 break로 for 루프를 탈출하도록 했다.

getch 함수가 입력받은 값은 문자 코드이기 때문에 이 값을 Munje 구조체의 Answer 멤버와 직접 비교하려면 문자 코드를 정수값으로 변환해야 한다. 문자 '1'의 코드는 0x31의 값을 가지므로 이 값을 정수 1로 바꾸려면 0x30을 빼면 된다. 0x30은 문자 '0'와 같으므로 ch에서 '0'를 빼면 Answer와 비교 가능한 정수가 된다.

ch에 사용자가 입력한 정답의 번호를 구했으면 Munje 구조체의 Answer 멤버와 비교해 보고 정답을 입력했는지 오답을 입력했는지 판별한다. if 문으로 이 두 값을 비교해 보고 정답인 경우는 정답이라는 메시지만 출력하고 오답인 경우는 정답을 가르쳐 주도록 했다. delay(1000) 문장은 메시지를 출력한 후 1초간 대기하여 사용자가 결과를 볼 수 있도록 한다.

이런 식으로 count 루프를 다섯 번 돌면서 문제 다섯 개를 출력하면 프로그램이 종료된다. 그다지 어려운 예제가 아니므로 분석에 큰 어려움은 없었을 것이다. 문제를 더 많이 입력해 놓았다면 더 많은 문제를 출제할 수 있을 것이며 좀 더 재미있게 진행하도록 수정해 볼 수 있는 여지가 많다. 문제를 다 푼 후에 맞춘 개수와 틀린 개수를 보여준다든가 문제에 응답하는 시간 제한을 두는 식으로 개작해 볼 수 있다.