Toy 1. 가위바위보 게임

min_ss·2023년 7월 9일

<Toy-Projects>

목록 보기
1/1
post-thumbnail

제작 기간: 2023.07.03 ~ 2023.07.06 (3일)


✔ 제작하게 된 계기

js를 공부하며✍ 스스로 얼마나 잘 이해하고 있는지 궁금하던 시점에
바닐라로 간단한 로직을 한번 구현해보자라는 생각이 들었습니다.
다양한 아이디어가 있지만 그 중 가위바위보는 대중성있는 게임이고 
요구사항이 명확하기 때문에 첫 토이 프로젝트로 채택 하였습니다.

✔ 목표

* Html -> 요소들의 부모, 자식간의 관계에 대해 이해하고자 id와 class는 부여하지 않고 구성하기
* JS -> 최대한 직관적이고, 간결하게 코드 구성하기


게임소개🎈

🔹 전체적인 흐름도

사용자(Me) 패 선택 -> 상대방(Com)도 랜덤하게 패 선택 -> 승패 확인 
-> 승리한 쪽에 점수 부여 -> 5번 이상 이기면 완전한 승리 -> 리셋버튼을 클릭 시 게임 초기화 

🔹 주요 기능

  • 사용자(Me)가 패를 선택 시, 상대방(Com)도 패 선택을 일시적으로 멈춥니다.

  • 승패가 확인 된 후 상대방(Com)은 다시 랜덤하게 패를 선택 합니다.

  • 사용자가(Me) 버튼을 연속적으로 클릭해도 승패는 1번으로 판단됩니다.

  • 완전한 승리가 되면 사용자는 버튼을 클릭해도 이벤트가 발생하지 않습니다.

  • 리셋 버튼을 클릭하면 게임이 초기화 됩니다.


🔹 코드 설명

❗ HTML

 <section>
    <!-- 상단  -->
    <div>
      <span>가위 바위 보! Com을 이기세요!</span>
      <input type="reset" value="reset">
    </div>
    <!-- main  -->
    <div>
      <div>
        <span>Me</span>
        <img src="./images/rock.png">
      </div>
      <div>
        <span>Com</span>
        <img src="./images/rock.png">
      </div>
    </div>
    <!--  score  -->
    <div>
      <span>0</span>
      <span>:</span>
      <span>0</span>
    </div>
    <!-- btnArea   -->
    <div>
      <input type="button" name="가위">
      <input type="button" name="바위">
      <input type="button" name="">
    </div>
  </section>

목표하는 바와 같이 id와 class는 적용하지 않고 만들었습니다.

❗ JS

const title = document.querySelector('div > span') // 상단 제목
const [meImg, comImg] = document.querySelectorAll('img'); // 화면 이미지
const bottons= document.querySelectorAll('input[type=button]'); // 하단 버튼들
// 화면 점수
const meScoreSpan= document.querySelector('section > div:nth-child(3) > span');
const comScoreSpan= document.querySelector('section > div:nth-child(3) > span:last-child');

/* 이미지 */
const rpsGameList = {
    0 : "./images/scissors.png",
    1 : "./images/rock.png",
    2 : "./images/paper.png"
 };

/* score */
const scoreList = {
    '가위' : 0,
    '바위' : 1,
    '보' : -1,
};

let meScore; // 사용자(me) 점수
let comScore; // com 점수

Com이 사용할 이미지와 가위바위보의 점수는 객체에 담았습니다.

📂 comPick() -> Com의 패를 정하는 함수

/* 컴퓨터 선택 */
function comPick(){
    // 0 ~ 2 랜덤 값 생성
    let key = Math.floor(Math.random() * 3);
    // 주소에 넣어 준다
    comImg.src = rpsGameList[key];
    // 컴퓨터 점수에 넣어 준다
    // key = 0 = 가위 / key = 1 = 바위 / key = 2 = 보
    switch (key) {
        case 0 :
            comScore = scoreList['가위'];
             break;
        case 1 :
            comScore = scoreList['바위'];
            break;
        case 2 :
            comScore = scoreList['보'];
            break;
    }
}

랜덤으로 key 값을 정해준 뒤, rpsGameList 객체를 이용 해 주소를 불러오는 형식으로
Img의 src를 변경해 주었습니다. 또한 이미지에 맞는 점수가 들어가도록 case문을 작성했습니다.

setInterval(comPick, 110);

랜덤으로 Com의 이미지를 변경하는 메소드입니다.

📂 mePick() -> Me의 패를 정하는 함수

/* 내 선택 */
function mePick(){
    // 점수 검사
    // 가위 바위 보 버튼들 순회
    [...bottons].forEach( btn => {
        // 클릭한 버튼에 따라 이미지, 점수 부여
        btn.onclick = throttle(e => {
            if (setting === -1) return;
            switch (e.target.name) {
                case '가위' :
                    meImg.src = rpsGameList[0]
                    meScore = scoreList['가위']
                    break;
                case '바위' :
                    meImg.src = rpsGameList[1]
                    meScore = scoreList['바위']
                    break;
                case '보' :
                    meImg.src = rpsGameList[2]
                    meScore = scoreList['보']
                    break;
            }
            win_check();
            start();
            game_over_check();
        }, 200)
    });
}

fotEach문을 통해 버튼을 순회하며 각 버튼을 클릭했을 때 이벤트가 발생하도록 하였습니다.
클릭한 버튼에 맞는 이미지를 화면에 보여주고, 점수를 meScore 변수에 담았습니다.


📂 win_check() -> 승패 확인 함수

function win_check(){
    let setMeScore = (+meScoreSpan.textContent) + 1;
    let setComScore = (+comScoreSpan.textContent) + 1;
    // 컴퓨터 픽 멈춤
    stop_comPick();
    // 가위 0 / 보 -1
    if(meScore > comScore && meScore === 0){
        title.textContent = '야호 이겼어요~!'
        meScoreSpan.textContent = setMeScore;
    }
    // 가위 0 / 바위 1
    else if(meScore < comScore && meScore === 0){
        title.textContent = '앗 졌어요ㅜㅜ'
        comScoreSpan.textContent = setComScore;
    }
    ... 생략

사용자가 버튼을 클릭하고 난 뒤, Com도 패 정하는 걸 멈춥니다.
승패를 확인하고 이겼다면? 점수를 1점 올리고 화면 상단에 이겼다는 text로 변경되도록 하였습니다.


📂 start() -> 지속적으로 게임을 시작하는 함수

function start(){
    setTimeout(() => {
        if( setting > 0){
            return;
        }
        //음수 값이 이면? 실행
        setting = setInterval(comPick, 110);
    }, 800);
}

win_check() 함수에서 Com이 패를 선택하는 것을 멈추었기 때문에
지속적인 게임 진행을 위해 다시 랜덤으로 Com이 이미지를 변경하고 패를 결졍하도록 하였습니다.


🔹 진행 중 발생한 문제점.. 이에 대한 해결

  1. 버튼을 클릭할 때마다 setInterval(comPick, 110);이 지속적으로 호출 되는 점
    서로 패가 정해지면 Com이 일시정지를 했다가 다시 같은 속도로 이미지가 변경 되어야 하나
    사용자가 버튼을 클릭 할수록 더 빠른 속도로 변경된다는 문제가 있었습니다.
  • 해결책
setting = setInterval(comPick, 110);
/// setting 라는 변수 정의
 function stop_comPick(){
    clearInterval(setting);
    setting = -1;
}

stop_comPick 함수에서 clearInterval을 통해 setting를 종료하고
변수 setting 에 다시 음수값(-1)을 넣습니다.

function start(){
    setTimeout(() => {
        if( setting > 0){
            return;
        }
        //음수 값이 이면? 실행
        setting = setInterval(comPick, 110);
    }, 800);
}

차후 버튼을 클릭하며 게임을 진행 시, setting가 음수 값이면
setInterval을 실행하도록 조건을 걸었습니다.


  1. 버튼 중복 클릭 방지
    버튼을 연속적으로 클릭함으로 인해 의도대로 진행 안될 가능성이 있다고 판단되었습니다.
    (한 패를 가지고 계속 이긴다거나...진다거나...하는건 재미의 요소를 반감시킨다고 생각했습니다.)

  • 해결책
function throttle(callback, delay){
    let timerID; //타이머 ID
    return (...args) => {
        if(timerID) return;
        timerID = setTimeout(() => {
            callback(...args);
            timerID = null;
        }, delay);
    };
};

콜백되는 함수 가 일정시간(delay) 뒤 실행되도록 하는 throttle 함수를 만들었습니다.

// 가위 바위 보 버튼들 순회
    [...bottons].forEach( btn => {
        // 클릭한 버튼에 따라 이미지, 점수 부여
        btn.onclick = throttle(e => {
            if (setting === -1) return;
            switch (e.target.name) {
                case '가위' :
                    meImg.src = rpsGameList[0]
                    meScore = scoreList['가위']
                    break;
                case '바위' :
                    meImg.src = rpsGameList[1]
                    meScore = scoreList['바위']
                    break;
                case '보' :
                    meImg.src = rpsGameList[2]
                    meScore = scoreList['보']
                    break;
            }
            win_check();
            start();
            game_over_check();
        }, 200)
    });

버튼들을 클릭할 때마다 throttle 함수가 실행되어 여러번 버튼을 클릭하더라도
0.2초 뒤에 클릭 이벤트가 1번 실행되도록 했습니다.


✔ 회고

이론공부를 할 때만해도 setInterval, clearTimeout, setTimeout 메소드에 대한 개념 정리가
되었다고 생각했는데 막상 사용을 하여 기능 구현을 하려하니 쉽지 않았습니다.
해당 프로젝트를 하며 이 메소드들에 대한 용도와 사용법을 확실히 알게 되었습니다.

중복 버튼 클릭의 문제점을 throttle 함수를 통해 일부분 해결하긴 하였으나,
Com이 랜덤값을 돌 때 하나의 패가 2회이상 나오지 않게 한다면 더 확실히 문제점을
막을 수 있지 않을까하는 아쉬움이 듭니다.

아직 배워야할 것이 많다고 느꼈고, 스스로 '무언가'를 구현했다는 점이 보람 차고 즐거웠습니다.
자신감도 붙고 동기부여도 되고 해당 프로젝트를 기점으로 지속적으로 프로젝트를 기획하고 제작해야겠습니다! 👀

profile
빙글빙글 개발자 취준생

0개의 댓글