바닐라 자바스크립트로 숫자야구 만들기 (feat. 코딩자율학습 제로초)
인프런에서 "코딩자율학습 제로초의 자바스크립트 입문"을 따라가고 있다.
섹션4에서는 계산기를 만들었었고, 이번 섹션5에서는 숫자야구다! 강의에서 구현과정 보여주기 전에 먼저 해보고 강의를 빠르게 듣는 게 더 효율적인 것 같다. 그래서 혼자서 구현한 과정 -> 강의 후 수정 과정 순서로 작성하려고 한다.
숫자야구 규칙 정리 & 순서도 작성
숫자야구 많이 했었는데 규칙을 잊어서 간단하게 나무위키와 몇몇 티스토리를 확인하고, 바로 순서도를 그렸다.
핵심은 1) 적절한 형태의 입력값을 받는 것 2) Strike, Ball, Out을 판단하는 것이라고 생각했다.
"자리"기준으로 비교를 편리하게 하기 위해 숫자가 아닌 문자열 형태로 정답과 입력값을 비교했다.
저번에 계산기 구현 때와 마찬가지로 여러 가지 예외사항이나 고려해야 하는 케이스들을 떠올려봤다. 1번, 2번은 이미 순서도 작성 하고 처음으로 구현할 때 반영했다. 3,4번은 나중에 떠올라서 이후에 추가적으로 반영했다.
HTML 구조
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>숫자야구</title>
<style>
#start {
display: block;
}
#trial,
#submit {
position: relative;
top: 20px;
}
table {
position: relative;
top: 30px;
}
#submitNum {
margin-top: 10px;
width: 150px;
height: 80px;
border: 5px solid black;
text-align: center;
line-height: 80px;
font-size: 40px;
}
</style>
</head>
<body>
<h3>⚾️ 숫자야구 ⚾️</h3>
<button id="start">게임시작</button>
<div id="submitNum"></div>
<input id="trial" />
<button id="submit">제출</button>
<div>
<table border="1">
<th>STRIKE</th>
<th>BALL</th>
<th>OUT</th>
<tr>
<td id="strike">0</td>
<td id="ball">0</td>
<td id="out">0</td>
</tr>
</table>
</div>
</body>
</html>
이렇게 생겼다. 최소한의 구조와 스타일링만 했다.
내가 의도한 방식은 아래와 같다.
- 게임시작을 눌러야, 컴퓨터가 결정한 랜덤 정답값이 생성된다.
- 입력창에 예측값을 쓰고 제출버튼을 클릭해야한다.
- 제출한 예측값은 검정 테두리의 네모 칸에 표시된다.
- 제출한 예측값에 대한 strike, ball, out정보가 표에 표시된다.
구현 과정
필요한 변수를 초기화한다.
let answer;
let strike;
let ball;
let out;
난수생성함수 makeNum
0-9범위의 한 자리 숫자를 반환하는 함수를 따로 정의해주었다. 정답값과 예측값 모두 문자열 형태로 비교하고자, 반환 형태도 String으로 처리했다.
/* 난수생성 */
const makeNum = () => {
return String(Math.floor(Math.random() * 9));
};
게임시작함수 makeNum
게임시작 버튼을 클릭하면 실행되는 함수다. 위의 makeNum함수를 활용하여 나온 String형태의 숫자 3개를 더하여 세 자리 정답값을 만든다.
이미 게임을 진행하고 있는 와중에 게임시작 버튼을 누를 수도 있기에 if문을 추가했다. answer값이 생성된 적이 있다면, 기존 정답을 알려주고 다시 새로운 정답값이 세팅됨을 알려준다. 제출한 예측값이 있을 수도 있기 때문에, 예측값을 보여주는 칸도 비워준다.
/* 게임시작 */
const onStart = () => {
if (answer) {
alert(`정답: ${answer}, 정답값이 다시 초기화됩니다.`);
$submitNum.textContent = "";
}
answer = makeNum() + makeNum() + makeNum();
console.log(answer);
strike = 0;
ball = 0;
out = 0;
};
예측값 제출함수 onSubmit
사용자가 예측값을 입력하고 제출 버튼을 클릭했을 때 실행되는 함수다.
1) 입력값 형태가 적절한지를 판단하고
2) strike / ball / out 정보를 알려주고
3) 해당 정보를 화면에 그려줘야 한다
일단 answer가 있어야 위의 모든 기능이 작동하도록 했다. answer가 없다면, 게임시작 버튼을 통해 정답값을 먼저 생성하도록 유도한다.
step1에서는 크게 2가지 조건을 고려했다. 숫자가 아닌 다른값을 입력했거나 길이가 3이 아니라면 다시 입력하도록 한다.
step2는 step1에서 제대로된 예측값으로 판단되었을 때 실행된다. String형태의 answer와 String형태의 예측값trialNum을 한 자리씩 비교한다. 완전히 일치하면 strike, 그렇지 않으면 포함된 숫자인지 판단하여 ball 또는 out으로 판단한다.
step3은 간단하게 화면에 위의 정보를 그려준다. 다만 Strike가 3인 경우, 정답을 알려주고 게임시작 버튼을 다시 누르도록 유도한다.
😢 지금보니 조건문이 너무 중첩되어 가독성이 떨어지는 것 같다... 이 부분도 보완할 수 있으면 좋을 듯하다.
/* 제출 시도 */
const onSubmit = () => {
if (answer) {
trialNum = $trial.value;
strike = 0;
ball = 0;
out = 0;
if (!Number(trialNum) || trialNum.length !== 3) {
//숫자 외의 잘못된 값을 입력할 경우
alert("잘못 입력하였습니다. 다시 입력하세요.");
$trial.value = "";
} else {
//제대로 입력한경우
$submitNum.textContent = trialNum;
$trial.value = "";
/* 정답 근접도 평가 */
for (let i = 0; i < 3; i++) {
if (trialNum[i] === answer[i]) {
strike += 1;
} else {
if (answer.indexOf(trialNum[i]) !== -1) {
ball += 1;
} else {
out += 1;
}
}
}
$strike.textContent = String(strike);
$ball.textContent = String(ball);
$out.textContent = String(out);
}
if (strike == 3) {
alert("정답입니다🥰 게임을 다시 시작해주세요!");
}
} else {
alert("게임을 시작해주세요.");
$trial.value = "";
}
};
엘리먼트는 아래와 같이 저장하여 사용했다.
const $start = document.querySelector("#start");
$start.addEventListener("click", onStart);
const $trial = document.querySelector("#trial");
const $submit = document.querySelector("#submit");
$submit.addEventListener("click", onSubmit);
const $strike = document.querySelector("#strike");
const $ball = document.querySelector("#ball");
const $out = document.querySelector("#out");
const $submitNum = document.querySelector("#submitNum");
일단 작동은 되는데 아직까지 뭔가 고려 못한 상황이 있는 것 같아서 강의내용도 참고해야겠다.
리뷰
🍊 태도
지금처럼 순서도 차근차근 그려가면서 해보자. 그리고 중간에 모르는 사소한 개념이 있다면 반드시 무엇인지 알고 넘어가자.
🍊 지식
(질문🕵️) 위의 onSubmit함수에서 strike가 3일 때, alert해주는 코드를 네모칸에 예측값 페인팅해주는 코드보다 분명히 더 아래에 작성했는데 왜 먼저 실행되는지 모르겠다. 그니까 정답일 경우, 정답으로 작성한 값이 네모칸에 그려지기 전에 알림창이 뜬다. alert함수는 다른 방식으로 작동되는 것인가?
저번 섹션의 실습에서도 함수선언이나 변수선언 위치에 따라 어떻게 실행되는지가 의문이었는데, 이건 그때와는 또다른 영역인 것 같다. (TBD)
강의 수강 후, 잘못되었던 부분이나 고려하지 못한 부분들을 돌아보는 노트다.
기능 구상 회고
잘한 점 🥰
- 입력값 검증을 강의 내용보다는 더 꼼꼼히 했다는 생각이 든다. 강의 예제의 경우, 문자 입력 시의 예외 처리를 하지 않았다.
- 새로고침 기준 외에도 '게임시작'을 통해 사용자가 직접 게임을 초기화할 수 있는 옵션을 주었다.
못한 점 🥹
- 정답값 생성 시, 숫자가 중복되는 것을 고려하지 못했다. 숫자 중복을 고려했다면, 문자열이 아닌 배열로 변수를 다뤘을 것 같은데... 배열을 사용하지 않으면 중복 체크 시 중복되지 않을 때까지 다시 새로운 숫자 한 자리를 생성해야하는 핸디캡이 있기 때문이다.
- 게임 종료 조건을 세분화해서 생각하지 못했다. 내가 혼자 구현했을 때는 그저 정답을 맞혀야 종료되는데, 강의에서는 시도가능한 횟수를 정의하였다. '종료' 조건을 명확히 하는 것이 구현하기에도 깔끔한 것 같다.
- 시도한 값들의 히스토리를 보여주지 않았다. 사용자는 이전 시도 이력들을 통해 새로운 값을 유추할텐데 해당 부분을 고려하지 못했던 것 같다. 히스토리가 있으면 이미 시도한 값을 다시 제출하지 못하도록 방지할 수도 있다.
수정한 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>숫자야구</title>
<style>
#start {
display: block;
}
#trial,
#submit {
position: relative;
top: 20px;
}
table {
position: relative;
top: 30px;
}
#submitNum {
margin-top: 10px;
width: 150px;
height: 80px;
border: 5px solid black;
text-align: center;
line-height: 80px;
font-size: 40px;
}
</style>
</head>
<body>
<h3>⚾️ 숫자야구 ⚾️</h3>
<button id="start">게임시작</button>
<div id="submitNum"></div>
<div id="msg"></div>
<input id="trial" />
<button id="submit">제출</button>
<div>
<table border="1">
<th>Your Num</th>
<th>STRIKE</th>
<th>BALL</th>
<th>OUT</th>
<tbody id="records"></tbody>
</table>
</div>
<body>
</html>
수정1 - 정답값 중복 삭제
일단 범위를 1-9까지로 하고, 세자리 숫자 중 중복값이 없도록 수정하였다. 강의 예제와는 다르게 연습겸 문자열 형태를 그대로 가져가면서 수정하는 방향으로 했다.
//정답값 생성
answer = makeNum();
while (answer.length < 4) {
let tempNum = makeNum();
if (!answer.includes(tempNum)) {
answer += tempNum;
}
}
수정2 - 히스토리 추가 / 시도한 값인지 검증 / 종료 시 입력 방지
HTML구조를 조금 수정하여, table에서 시도한 값들을 담을 공간을 분리하였다. 적절한 값을 입력할 경우, 히스토리를 보여주는 표에 [시도한 숫자 / strike / ball / out]의 값이 하나의 행으로 추가되어 화면에 그려지도록 했다.
/* 제출 시도 */
const onSubmit = () => {
if (answer) {
trialNum = $trial.value;
strike = 0;
ball = 0;
out = 0;
if (!Number(trialNum) || trialNum.length !== 3 || $msg.textContent) {
//숫자 외의 잘못된 값을 입력할 경우
const errMsg = $msg.textContent
? "종료되었습니다. 재시작해주세요"
: "잘못 입력하였습니다. 다시 입력하세요.";
alert(errMsg);
} else if (tries.includes(trialNum)) {
alert("이미 시도한 값입니다. 다시 시도해주세요");
} else {
//제대로 입력한경우
$submitNum.textContent = trialNum;
/* 정답 근접도 평가 */
for (let i = 0; i < 3; i++) {
if (trialNum[i] === answer[i]) {
strike += 1;
} else {
if (answer.indexOf(trialNum[i]) !== -1) {
ball += 1;
} else {
out += 1;
}
}
}
// 새로운 시도 기록
tries.push(trialNum);
const newRow = document.createElement("tr");
newRow.innerHTML = `<td>${trialNum}</td><td>${String(
strike
)}</td><td>${String(ball)}</td><td>${String(out)}</td>`;
document.querySelector("#records").appendChild(newRow);
}
$trial.value = "";
if (strike == 3) {
const message = `정답입니다🥰 게임을 다시 시작해주세요!`;
alert(message);
$msg.textContent = message;
}
} else {
alert("게임을 시작해주세요.");
$trial.value = "";
}
};
아래의 사진처럼 추가된다.
그런데 해당 방식으로 구현하다보니, 종료 때 매우 애매한 상황이 나온다. 정답을 맞히면 맞혔다는 팝업창만 뜰 뿐, 팝업창을 없애고 나서는 종료되었는지 무엇을 해야하는지 명확하지 않은 화면이 그려지는 것이다.
따라서 다음처럼, 정답을 맞힐 경우 축하문구와 함께 게임시작을 누르라는 멘트가 화면에도 그려지도록 수정했다.
또한, 축하문구($msg.textContent)의 값이 있으면 숫자 제출이 안되도록 조건을 추가하여, 게임 종료 시에는 더이상 숫자를 추가할 수 없게 하였다.
숫자를 제출할 때, 시도한 값들의 이력인 tries에 포함되었는지도 검증하여 기존에 제출한 값이면 제출되지 않도록 제한하였다.
일단 수정 방향은 횟수 상관없이 맞힐 때까지 시도할 수 있도록 해둔 상태다. 역시 완벽한 구현이란 어렵고, 아직 다양한 상황들을 고려하면서 깔끔하게 코드를 짜기엔 정말x100 부족하구나. 이래서 복잡한 프로그램을 처음 구상할 때, 다들 설계에 대한 방법론까지 고민하나보다...
'프로그래밍 언어 > JavaScript' 카테고리의 다른 글
[프로그래머스 Lv.1] 개인정보 수집 유효기간 (자바스크립트) (1) | 2024.05.25 |
---|---|
[프로그래머스 Lv.0] 자바스크립트로 레벨0 모두 풀이하기 (0) | 2024.05.23 |
[자바스크립트 토픽 정리] 변수 선언위치에 대해 (1) | 2024.04.06 |
[자바스크립트 토픽 정리] 옵셔널 체이닝 ?. (0) | 2024.04.06 |
바닐라 자바스크립트로 계산기 만들기 (feat. 코딩자율학습 제로초) (1) | 2024.03.17 |