티스토리 뷰
C언어로 콘솔 지뢰찾기 게임을 구현합니다.
시작-코드-코드분석 순으로 작성되었습니다.
시작
[C프로그래밍 실습] 자료들을 정리하다가 지뢰 찾기 게임 자료를 발견했습니다. 완전히 까먹고 있었는데 새록새록 기억이 나더라고요. 1학년 때에는 반복문조차 이해를 못 하고 울며 겨자 먹기 식으로 코딩을 했었는데요. (지뢰 찾기 게임의 룰도 몰랐어요..)
담당 교수님께서는 과제를 보고서 형식으로 받으셔서 그 때 어떤 생각으로 저런 코드를 작성했는지 알 수 있었습니다. 정말 총체적 난국이더라고요. 난해한 변수명, 쓸데없는 반복문, 자료형 오류까지.. 심지어는 제대로 작동하지도 않았습니다 ㅎ
그래서 저때보다는 성장했다는 마음으로 코드를 완전히 뜯어고쳐 보았습니다.
코드
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 10
// 주변의 지뢰 개수 반환
int count_mine(char b[][SIZE], int r, int c)
{
int cnt = 0;
if (r == 0 && c == 0) { // (0, 0)
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
}
else if (r == 0) { // (0, y)
if (b[r][c - 1] == '#')cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
}
else if (c == 0) { // (x, 0)
if (b[r + 1][c] == '#')cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else if (r == SIZE - 1 && c == SIZE - 1) { // (9, 9)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else if (r == SIZE - 1) { // (9, y)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#')cnt++; // left
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r][c + 1] == '#')cnt++; // bottom
}
else if (c == SIZE - 1) { // (x, 9)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else { // 위 경우가 아니라면 팔방 검사
if (b[r][c - 1] == '#') cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r - 1][c] == '#') cnt++; // left
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
}
return cnt;
}
// 주변 지뢰 탐색
void search(char b[][SIZE], int r, int c) {
if (b[r][c] != '.')
return;
if (r < 0 || r > SIZE - 1 || c < 0 || c > SIZE - 1)
return;
int cnt = count_mine(b, r, c);
b[r][c] = cnt;
if (!cnt) {
search(b, r, c - 1); // top
search(b, r + 1, c - 1); // top right corner
search(b, r + 1, c); // right
search(b, r + 1, c + 1); // bottom right corner
search(b, r, c + 1); // bottom
search(b, r - 1, c + 1); // bottom left corner
search(b, r - 1, c); // left
search(b, r - 1, c - 1); // top left corner
}
}
// 이겼는지 판별
int check_win(char b[][SIZE])
{
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '.') // '.'인 요소가 남아있으면 계속 진행
return 0;
}
}
return 1;
}
// 보드 출력
void print_board(char b[][SIZE]) {
// 행 번호 출력
printf("%3c", ' ');
for (int i = 0; i < SIZE; i++)
printf(" %d ", i);
printf("\n");
for (int i = 0; i < SIZE; i++) {
printf(" %d ", i); // 열번호 출력
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '#')
printf(" _ ");
else if (b[i][j] == '.')
printf(" _ ");
else
printf("(%d)", b[i][j]);
}
printf("\n");
}
printf("\n\n");
}
// 정답 출력
void print_answer(char b[][SIZE]) {
// 행 번호 출력
printf("%3c", ' ');
for (int i = 0; i < SIZE; i++)
printf(" %d ", i);
printf("\n");
for (int i = 0; i < SIZE; i++) {
printf(" %d ", i); // 열번호 출력
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '#')
printf(" # ");
else if (b[i][j] == '.')
printf(" . ");
else
printf("(%d)", b[i][j]);
}
printf("\n");
}
printf("\n\n");
}
int main()
{
int r, c;
char board[SIZE][SIZE] = { 0 }; // 보드 이차원 배열
srand(time(NULL)); // 시드 설정
// 보드 초기화 및 출력
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if ((rand() % 100) < 10) // 10%의 확률로 지뢰 세팅
board[i][j] = '#'; // # : 지뢰
else
board[i][j] = '.'; // . : 지뢰가 아님
}
}
print_board(board);
// 게임 시작
while (1) {
printf("좌표를 입력하세요 (행, 열) : ");
scanf_s("%d %d", &r, &c);
if (board[r][c] == '#') { // 지뢰를 골랐다면
printf("\n지뢰가 폭발하였습니다. \n");
break;
}
else {
search(board, r, c);
print_board(board);
}
if (check_win(board)) {
printf("\n!! 성공 !! \n");
break;
}
}
printf("\n============= 정답 =============\n");
print_answer(board);
return 0;
}
코드 분석
먼저 지뢰게임의 룰을 아셔야 합니다. 지뢰게임은 지뢰를 피해 칸들을 선택해 지뢰만 남게 되면 이기는 게임입니다. 숫자는 그 칸을 중심으로 한 3 × 3 영역에 몇 개의 지뢰가 존재하는가를 나타냅니다. 만약 숫자가 3이면 주변 8개의 칸에 3개의 지뢰가 있다는 뜻이겠죠. 이렇게 나타나는 숫자들을 가지고 인접한 8칸에 숨어 있는 지뢰를 피해 영역을 선택하면 됩니다.
저는 콘솔로 구현하였기 때문에 보드는 이차원 배열로 지뢰는 '#'으로 표현하였습니다. 또 주변에 지뢰가 없다면 빈칸이 아니라 0으로 표시되고 안전한 칸은 '.'으로 초기화됩니다.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 10
먼저 헤더 파일과 기호 상수 선언입니다. rand 함수를 이용해 지뢰를 랜덤으로 배치하기 위해 <stdlib.h> 라이브러리를 적재하였습니다. <time.h>는 rand 함수의 시드를 설정하는데에 사용되었습니다. 기호상수 SIZE는 보드의 크기로 반복문 및 사용에 편리하도록 따로 정의해 주었습니다.
// 주변의 지뢰 개수 반환
int count_mine(char b[][SIZE], int r, int c)
{
int cnt = 0;
if (r == 0 && c == 0) { // (0, 0)
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
}
else if (r == 0) { // (0, y)
if (b[r][c - 1] == '#')cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
}
else if (c == 0) { // (x, 0)
if (b[r + 1][c] == '#')cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else if (r == SIZE - 1 && c == SIZE - 1) { // (9, 9)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else if (r == SIZE - 1) { // (9, y)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#')cnt++; // left
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r][c + 1] == '#')cnt++; // bottom
}
else if (c == SIZE - 1) { // (x, 9)
if (b[r][c - 1] == '#') cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
if (b[r - 1][c] == '#') cnt++; // left
}
else { // 위 경우가 아니라면 팔방 검사
if (b[r][c - 1] == '#') cnt++; // top
if (b[r + 1][c - 1] == '#') cnt++; // top right corner
if (b[r + 1][c] == '#') cnt++; // right
if (b[r + 1][c + 1] == '#') cnt++; // bottom right corner
if (b[r][c + 1] == '#') cnt++; // bottom
if (b[r - 1][c + 1] == '#') cnt++; // bottom left corner
if (b[r - 1][c] == '#') cnt++; // left
if (b[r - 1][c - 1] == '#') cnt++; // top left corner
}
return cnt;
}
count_mine 함수는 주변의 지뢰 개수를 세어 반환해 주는 함수입니다. 이차원 배열을 사용해 구현한 보드는 인덱스 범위에 제약을 받습니다. 보드의 가장자리를 사용자가 선택한 경우, 검사해야 하는 칸의 범위가 달라집니다. 예를 들어 (0, 0)의 경우 위칸은 조사할 수 없고 오른쪽, 오른쪽 아래 대각, 아래쪽만 조사가 가능하죠. 이처럼 사용자가 선택한 좌표에 따라 주변을 검사할 수 있도록 if-else문을 작성해 주었습니다.
// 주변 지뢰 탐색
void search(char b[][SIZE], int r, int c) {
if (b[r][c] != '.') // 이미 검사했다면 반환
return;
if (r < 0 || r > SIZE - 1 || c < 0 || c > SIZE - 1) // 인덱스 범위를 벗어났다면 반환
return;
int cnt = count_mine(b, r, c);
b[r][c] = cnt;
if (!cnt) {
search(b, r, c - 1); // top
search(b, r + 1, c - 1); // top right corner
search(b, r + 1, c); // right
search(b, r + 1, c + 1); // bottom right corner
search(b, r, c + 1); // bottom
search(b, r - 1, c + 1); // bottom left corner
search(b, r - 1, c); // left
search(b, r - 1, c - 1); // top left corner
}
}
search 함수는 앞서 살펴보았던 count_mine 이용해 주변의 영역을 검사하는 함수인데요. 만약 숫자가 0이 나온다면, 주변에 지뢰가 하나도 없다는 뜻이기에 조사하는 것이 무의미 해집니다. 이 기능이 없으면 쓸데없이 주변 8구역을 사용자가 선택해야 하는 일이 생기죠.
저는 재귀를 통해 이를 구현하였습니다. 이미 검사하지 않았고, 보드의 인덱스의 범위 내라면 count_mine으로 주변 지뢰 개수를 반환받아 cnt에 저장합니다. 저장된 값을 보드에 삽입하고 만약 cnt가 0이라면 주변 모든 좌표를 인수로 search 함수를 호출합니다. 결과적으로 이어진 0 값들과 그 경계선을 찾아낼 수 있죠.
// 이겼는지 판별
int check_win(char b[][SIZE])
{
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '.') // '.'인 요소가 남아있으면 계속 진행
return 0;
}
}
return 1;
}
check_win 함수는 승리하였는지 판별하는 함수입니다. 지뢰를 피해 모든 칸을 잘 선택하였다면, 초기에 설정했던 '.'이 들어있는 칸은 한 개도 없을 것입니다. 이중 for문을 통해 모든 보드의 값을 검사하고 '.'이 발견되면 즉시 0을 반환합니다. 전체 보드를 검사해 이 중 for문이 종료된 경우, 승리한 것으로 1을 반환합니다.
// 보드 출력
void print_board(char b[][SIZE]) {
// 행 번호 출력
printf("%3c", ' ');
for (int i = 0; i < SIZE; i++)
printf(" %d ", i);
printf("\n");
for (int i = 0; i < SIZE; i++) {
printf(" %d ", i); // 열번호 출력
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '#')
printf(" _ ");
else if (b[i][j] == '.')
printf(" _ ");
else
printf("(%d)", b[i][j]);
}
printf("\n");
}
printf("\n\n");
}
// 정답 출력
void print_answer(char b[][SIZE]) {
// 행 번호 출력
printf("%3c", ' ');
for (int i = 0; i < SIZE; i++)
printf(" %d ", i);
printf("\n");
for (int i = 0; i < SIZE; i++) {
printf(" %d ", i); // 열번호 출력
for (int j = 0; j < SIZE; j++) {
if (b[i][j] == '#')
printf(" # ");
else if (b[i][j] == '.')
printf(" . ");
else
printf("(%d)", b[i][j]);
}
printf("\n");
}
printf("\n\n");
}
print_board와 print_answer은 보드를 출력하는 함수로 정답(지뢰의 위치)을 숨기느냐 보이느냐만 다르고 동일하게 작성되었습니다. 사용자의 편의를 위해 행 번호와 열 번호가 출력되게 하였으며, 이중 for문을 사용해 모든 요소를 화면에 출력합니다.
int main()
{
int r, c;
char board[SIZE][SIZE] = { 0 }; // 보드 이차원 배열
srand(time(NULL)); // 시드 설정
// 보드 초기화 및 출력
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
if ((rand() % 100) < 10) // 10%의 확률로 지뢰 세팅
board[i][j] = '#'; // # : 지뢰
else
board[i][j] = '.'; // . : 지뢰가 아님
}
}
print_board(board);
main 함수의 초반 부분입니다. 사용자로부터 선택 좌표를 입력받기 위해 변수 r(row), c(col)을 선언하였으며, 보드의 선언, rand 함수의 시드 설정을 확인할 수 있습니다. 나머지 연산으로 (rand() % 100 < 10) 10% 확률로 보드에 지뢰가 설치되고 print_board로 화면에 보드의 초기 모습을 출력하면서 게임이 시작됩니다.
while (1) {
printf("좌표를 입력하세요 (행, 열) : ");
scanf_s("%d %d", &r, &c);
if (board[r][c] == '#') { // 지뢰를 골랐다면
printf("\n지뢰가 폭발하였습니다. \n");
break;
}
else {
search(board, r, c);
print_board(board);
}
if (check_win(board)) {
printf("\n!! 성공 !! \n");
break;
}
}
printf("\n============= 정답 =============\n");
print_answer(board);
return 0;
}
게임이 시작되면 먼저 scanf_s를 이용해 사용자로부터 행과 열로 이루어진 좌표를 입력받습니다. 해당 좌표값 따라 게임이 진행됩니다. 만약 지뢰가 있는 좌표 라면 '지뢰가 폭발하였습니다.'라는 문구와 함께 while 문을 빠져나가며(break) 게임이 종료됩니다. 지뢰가 아니라면 해당 좌표의 범위를 검사하기 위해 search 함수가 호출하고 갱신된 보드를 화면에 출력합니다. 보드는 다시 check_win 함수를 통해 검사 되고 승리했다면 if 문이 실행되면서 '!! 성공 !!'을 출력하고 무한루프가 종료됩니다. 마지막으로 print_answer 함수로 정답이 출력되며 게임의 결과를 확인할 수 있습니다.
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'C | 자료구조' 카테고리의 다른 글
[C/자료구조] 순환을 이용해 셀 채우기 (0) | 2023.10.27 |
---|---|
[C/자료구조] Ackermann 함수 (0) | 2023.10.27 |
[C] C프로그래밍 실습 (3) (0) | 2022.10.06 |
[C] C프로그래밍 실습 (2) (1) | 2022.10.04 |
[C] C프로그래밍 실습 (1) (0) | 2022.09.22 |