[JavaScript] 미니게임 프로젝트 | 카드 짝 맞추기🃏

승연·2022년 7월 16일
5

Game World

목록 보기
4/6
post-thumbnail

Game World


미니 프로젝트 세 번째 게임으로 카드 짝 맞추기 게임을 만들었습니다.

🙋‍♀️ 게임 설명 좀 해주세요

카드를 뒤집어 같은 그림의 카드를 찾는 게임입니다.

🤔 어떤 기능이 있나요?

  • 카드 짝 맞추기
    • 사용자는 주어진 시간 내에 같은 그림의 카드를 전부 찾아야 한다.
    • 사용자에겐 60초가 주어진다.
    • 게임판은 6x4 크기로 고정된다.
    • 같은 그림의 카드를 전부 찾은 경우 다음 스테이지로 넘어간다.
    • 스테이지가 진행될 때마다 주어진 시간은 5초씩 줄어든다.

  • 게임 종료 후 결과 출력
    • 게임은 남은 시간이 0초가 됐을 때 종료된다.
    • 게임 종료 시 사용자가 도달한 스테이지 정보를 출력한다.

🔎 사용 기술은 무엇인가요?

HTML, CSS, JavaScript를 이용하여 만들었습니다.

🙊 어디서 해볼 수 있나요?

https://sypear.github.io/game-world/game-cm.html



Game World - 카드 짝 맞추기🃏


1. 어떻게 만들어야 할까?

정리 내용 요약

  • 사용자가 카드를 눌렀을 때, 첫 번째로 뒤집은 카드인지 두 번째로 뒤집은 카드인지 판별하기
    • 첫 번째 카드인 경우, 두 번째 카드인 경우 동작이 달라짐
  • 두 번째로 뒤집은 경우에는 카드 일치 여부 확인 필요
    • 일치, 불일치에 따라 동작이 달라짐
  • 카드를 다 뒤집은 경우 스테이지 종료 후 다음 스테이지로 이동
    • 다음 스테이지 이동 시 설정값 변경(시간 5초씩 감소, 스테이지 카운트 증가)
  • 시간이 0초가 되는 경우 게임 전체 종료
    • 게임 전체 종료 시 설정 초기화 및 화면에 결과 출력



2. 카드 만들기

2-1. 게임에 사용할 카드 덱 만들기

카드 이미지

카드 이미지는 무료 아이콘 사이트인 flaticon에서 BomSymbols님의 Animals Icon Pack을 다운로드받아 이용했습니다.

카드 덱 구조

게임에 이용할 카드들을 저장하고 있는 카드 덱 Array는
cardDeck = [{ card: 카드 명, isOpen: 값, isMatch: 값 }]과 같이 구성하였습니다.

card: 카드 명

  • 카드 명을 저장합니다.
    • ex) card: "dolphin"

isOpen: 값

  • 카드의 상태를 저장합니다.
    • ex) isOpen: true (카드가 앞면인 경우)
    • ex) isOpen: false (카드가 뒷면인 경우)

isMatch: 값

  • 카드의 매칭 여부를 저장합니다.
    • ex) isMatch: true (카드의 짝을 맞춘 경우)
    • ex) isMatch: false (카드의 짝을 맞추지 않은 경우)

카드 덱 만들기

const BORAD_SIZE = 24; // 게임판 사이즈

// 카드 덱 생성
function makeCardDeck() {
    let randomNumberArr = [];

    // 이미지는 27개인데 필요한 카드는 12개로 고정되어 있기 때문에 27개의 이미지 중 랜덤으로 12개를 뽑도록 구현
    for (let i = 0; i < BORAD_SIZE / 2; i++) {
        // 랜덤 값 뽑기
        let randomNumber = getRandom(27, 0);

        // 중복 검사
        // randomNumberArr 안에 생성한 난수 값이 없다면 randomNumberArr에 추가
        // randomNumberArr 안에 생성한 난수 값이 있으면 인덱스 1 감소
        if (randomNumberArr.indexOf(randomNumber) === -1) {
            randomNumberArr.push(randomNumber);
        } else {
            i--;
        }
    }

    // 카드는 두 장씩 필요하므로 한 번 더 추가
    randomNumberArr.push(...randomNumberArr);

    ...
    
}

// 난수 생성
function getRandom(max, min) {
    return parseInt(Math.random() * (max - min)) + min;
}

👩‍💻 설명

  1. 랜덤으로 카드 이미지 뽑기
    • 제가 갖고 있는 카드 이미지는 27개인데, 게임에 필요한 카드는 12개(게임판 사이즈의 반)로 고정되어 있기 때문에 27개의 이미지 중 랜덤으로 12개를 뽑도록 구현했습니다.
    • 뽑은 랜덤값은 이미지 인덱스 값을 저장하는 배열에 추가했습니다.

  2. 랜덤 값 중복 검사
    - 이때, 중복 이미지가 선택되면 안 되므로 중복 검사 코드를 추가하였습니다.
    - 중복 검사는 indexOf() 메소드를 이용하여 구현했습니다.

  3. 뽑은 카드들 짝꿍 만들어주기
// 카드는 두 장씩 필요하므로 한 번 더 추가
    randomNumberArr.push(...randomNumberArr);

2-2. 무작위로 카드 섞기

Array.Prototype.Sort()

function shuffle(array) {
    array.sort(() => Math.random() - 0.5);
}

랜덤으로 카드를 섞기 위해서 sort() 메소드를 이용했습니다.

🤷‍♀️ sort()를 이용해서 어떻게 무작위로 섞을 수 있나요?

구문 : arr.sort([compareFunction])

sort() 메소드는 매개변수로 정렬 순서를 정의하는 함수를 받고,
반환 값으로는 정렬한 원 배열을 반환합니다.

무작위로 섞기 위해서는
sort() 메소드의 매개변수로 무작위로 섞을 수 있는 함수를 주면 됩니다.

😵 그게 무슨 말이죠?

sort() 메소드에 compareFunction가 전달되는 경우 배열은 compareFunction의 반환 값에 따라 정렬됩니다.

예를 들어 a와 b가 비교되는 두 요소라면 아래와 같이 정렬을 진행합니다.

매개변수 반환 값이 음수 : a를 b보다 낮은 인덱스로 정렬(그대로 유지)
매개변수 반환 값이 0 : 인덱스 위치 그대로 유지
매개변수 반환 값이 양수 : b를 a보다 낮은 인덱스로 정렬

즉, 인덱스 교환 여부를 결정하는 유일한 요소는 비교 함수의 반환 값입니다.

따라서 반환 값을 무작위로 만들어내는 함수를 매개변수로 주면
무작위로 카드 섞기 기능을 만들 수 있습니다.


🤮 규칙은 알겠어요. 그럼 반환 값을 무작위로 만들어내는 함수는 어떻게 만들면 되나요?

Math.random() - 0.5를 이용하면 됩니다.


🤬 그게 무슨 말인가요!!!!!!!

(소제목과 소제목 이모지는 이해가 안 돼서 답답해하던 당시의 제 마음이 반영 되었습니다)
모던 JavaScript 튜토리얼 예제 문제를 보고도 이해를 못하다가 최고의 답변을 보고 이해했습니다. (질문 글 작성자님의 마음 == 나의 마음)

Math.random()는 0부터 1 미만의 값을 반환하는 함수입니다.
그러므로 Math.random() - 0.5-0.5부터 +0.5 미만의 값을 반환합니다.

따라서 Math.random() - 0.5를 통해 음수, 양수 중 무작위로 반환 값이 나오도록 할 수 있는 겁니다!


2-3. 화면에 카드 출력

HTML을 어떻게 짜야 할까요?

<div class="card" data-id="1" data-card="whale">
	<div class="card__back"></div>
	<div class="card__front"></div>
</div>

👩‍💻 설명

위의 코드는 카드 한 장의 HTML 구조입니다.

.card

  • data-id를 통해 카드의 인덱스를 구분할 수 있도록 했습니다.
  • data-card를 통해 카드 명을 구분할 수 있도록 했습니다.


.card__back

  • 배경 이미지로 카드 뒷장의 이미지를 줬습니다.


.card__front

  • 배경 이미지로 카드 명에 해당하는 이미지를 줬습니다.

카드 뒤집기 효과

backface-visibility, rotateY() 속성을 이용하여 카드 뒤집기 효과를 구현했습니다.

👩‍💻 설명

backface-visibility

  • 요소의 뒷면이 보여야 하는지 지정하는 속성
    • visible : 뒷면이 보인다.
    • hidden : 뒷면이 보이지 않는다.
    • transform을 이용해서 요소의 앞, 뒤가 변형될 때 어색한 CSS 효과를 없애기 위해 hidden 값을 주는 방식으로 주로 사용된다.
    • 참고한 글 : CSS3 이면가시성(backface-visibility) 속성

rotateY(angle)

  • 매개 변수로 지정된 각도를 통해 Y축으로 회전 처리 하는 속성
    • 매개 변수의 타입은 정수이며 'deg' 단위를 사용한다.
    • 참고한 글 : devdic rotateY()

CSS

.card__back,
.card__front {
    ...
    backface-visibility: hidden;
    ...
}

JavaScript

// 카드 오픈(뒷면에서 앞면으로)
function openCard(id) {
    cardBack[id].style.transform = "rotateY(180deg)";
    cardFront[id].style.transform = "rotateY(0deg)";
  
  	...
    
}
    
// 카드 클로즈(앞면에서 뒷면으로)
function closeCard(indexArr) {
  
    ...
    
    cardBack[indexArr[i]].style.transform = "rotateY(0deg)";
    cardFront[indexArr[i]].style.transform = "rotateY(-180deg)";
  
    ...
    
}

카드를 뒷장에서 앞장으로 뒤집는 경우 카드 뒷장을 Y축 기준으로 180도만큼 회전시켜 뒷면으로 향하게 했고,

카드를 앞장에서 뒷장으로 뒤집는 경우
카드 앞장을 Y축 기준으로 -180도만큼 회전시켜 뒷면으로 향하게 했습니다.

그리고 카드가 뒤집히는 순간
카드의 뒷면을 안 보이게 하기 위해 backface-visibility: hidden 속성을 주었습니다.

전체 카드를 최초 1회 보여주고 다시 숨기기

게임이 시작되면 전체 카드를 전부 보여 주도록 했습니다.

그리고 2초 동안 카드를 보여준 뒤, 전체 카드를 다시 뒷장으로 뒤집도록 했습니다.

(원래 5초 동안 카드를 보여주도록 했었는데 지루하다는 의견이 많아 2초로 수정하였습니다.)




3. 필요한 기능들

3-1. 오픈한 카드가 첫 번째 카드인지 두 번째 카드인지 판별하기

function getOpenCardArr(id) {
    let openCardIndexArr = [];

    // isOpen: true이고 isMatch: false인 카드의 인덱스를 openCardIndexArr에 저장
    cardDeck.forEach((element, i) => {
        if (element.isOpen === false || element.isMatch === true) {
            return;
        }

        openCardIndexArr.push(i);
    });

    return openCardIndexArr;
}

사용자가 카드를 오픈했을 때,

오픈된 상태이고 짝이 맞춰지지 않은 상태인 카드가 있다면
그 카드의 인덱스를 배열에 담도록 했습니다.

그리고 인덱스를 저장하고 있는 배열의 length가 2라면 두 번째 선택이라고 판단했습니다. (length가 2라는 건 방금 오픈한 카드 외에도 이미 오픈되어 있는 카드가 하나 더 있다는 뜻이니까요.)

첫 번째 선택인 경우

첫 번째 선택인 경우에는 별도의 설정값을 변경하지 않았습니다.

두 번째 선택인 경우

두 번째 선택인 경우 카드 일치 여부 확인을 진행했습니다.


3-2. 오픈된 카드 두 장의 일치 여부 확인

// 카드 일치 여부 확인
function checkCardMatch(indexArr) {
    let firstCard = cardDeck[indexArr[0]];
    let secondCard = cardDeck[indexArr[1]];

    if (firstCard.card === secondCard.card) {
        // 카드 일치 처리
        firstCard.isMatch = true;
        secondCard.isMatch = true;

        matchCard(indexArr);
    } else {
        // 카드 불일치 처리
        firstCard.isOpen = false;
        secondCard.isOpen = false;

        closeCard(indexArr);
    }
}

카드 일치 여부를 확인하는 함수입니다.

카드의 짝을 맞춘 경우에는 사용자가 선택한 카드들의 isMatch 값을 true로 변경하고 카드 일치 처리를 진행하는 함수를 호출했습니다.

카드의 짝을 못 맞춘 경우에는 사용자가 선택한 카드들의 isOpen 값을 false로 변경하고 카드를 다시 뒷면으로 뒤집는 함수를 호출했습니다.

짝을 맞춘 경우

function matchCard(indexArr) {
    // 카드를 전부 찾았으면 스테이지 클리어
    if (checkClear() === true) {
        clearStage();
        return;
    }

    // 바로 클릭 시 에러가 나는 경우가 있어 0.1초 후 부터 카드 뒤집기가 가능하도록 설정
    setTimeout(() => {
        isFlip = true;
    }, 100);
}

// 카드를 전부 찾았는지 확인하는 함수
function checkClear() {
    // 카드를 전부 찾았는지 확인
    let isClear = true;

    cardDeck.forEach((element) => {
        // 반복문을 돌면서 isMatch: false인 요소가 있다면 isClear에 false 값을 저장 후 반복문 탈출
        if (element.isMatch === false) {
            isClear = false;
            return;
        }
    });

    return isClear;
}

카드의 짝을 맞춘 경우에는 전체 카드 매칭에 성공했는지 확인도 진행했습니다.

모든 카드의 isMatch값이 전부 true라면 카드 전체 매칭에 성공했다고 판단했습니다.

전체 매칭에 성공했다면 다음 스테이지로 넘어갈 수 있도록 했습니다.

짝을 맞추지 못한 경우

function closeCard(indexArr) {
    // 0.8초 동안 카드 보여준 후 닫고, 카드 뒤집기가 가능하도록 설정
    setTimeout(() => {
        for (let i = 0; i < indexArr.length; i++) {
            cardBack[indexArr[i]].style.transform = "rotateY(0deg)";
            cardFront[indexArr[i]].style.transform = "rotateY(-180deg)";
        }

        isFlip = true;
    }, 800);
}

카드의 짝을 맞추지 못한 경우에는 0.8초 동안 카드를 보여준 후 다시 뒷면으로 뒤집고 다시 게임을 재개할 수 있도록 했습니다.




💗 깨알 포인트

게임 기능이랑은 1도 상관없지만 귀여운 포인트를 넣어보았습니다.
(다들 좋아할 거라고 생각하고 야심차게 넣었는데 반응이 없어 슬펐습니다)




저번에 다른 색깔 찾기 게임 구현할 때도 고민을 많이 했었는데, 이번에는 더 많은 고민의 시간이 있었습니다.

이제 계획하고 있는 게임이 두 개 남았습니다.

이번보다 더 어려워질 것 같아 걱정되지만 열심히 해보겠습니다.

profile
✈️ https://sypear.tistory.com/

7개의 댓글

comment-user-thumbnail
2022년 10월 23일

와!! 이전 프로젝트 글도 보고 왔지만 단순 구현이 아니라 완성도를 높이고자 하는 걸 찾아내고 또 구현해내고 포인트를 만드는 능력이 멋지네요! 승연님은 참 좋은 개발자가 될거라고 생각합니다!!

1개의 답글
comment-user-thumbnail
2024년 1월 21일

안녕하세요 승연님 유료로 카드게임 제작문의드리고 싶은데 카톡 hihy96으로 메세지 한번만 주실수 있나요?

1개의 답글
comment-user-thumbnail
2024년 1월 24일

미니프로 젝트 만드는데 정말 많은 도움이 되었습니다 감사합니다

1개의 답글
comment-user-thumbnail
2024년 3월 28일

설명도 매우 깔끔하고 따라하는데 별 문제가 없었네요. 감사합니다

답글 달기