diff --git a/README.md b/README.md index 8b2833c..9349030 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# javascript-baseball-precourse \ No newline at end of file + +

+KAKAO +

+ +# 1회차 미니 과제 - ⚾️ 숫자 야구 게임 + +> 카카오 테크 캠퍼스 2기 FE 1회차 미니 과제, 숫자 야구 게임 레포입니다. + +
+ + +## 기능 목록 + +### UI +* 게임 타이틀과 규칙 및 예시를 표시한다. +* 입력을 받을 텍스트 필드를 배치한다. +* 답 제출 버튼을 배치한다. +* 결과 섹션을 배치한다. + +
+ +### 문제 출제 +* 1부터 9까지 서로 다른 임의의 수로 이루어진 3자리의 수가 생성된다. + +
+ +### 문제 풀기 +* 사용자로부터 서로 다른 3자리의 수를 입력 받는다. +* 정상 입력된 경우 입력에 따른 결과 메시지를 표시한다. + * 같은 수가 같은 자리에 있으면 스트라이크 + * 다른 자리에 있으면 볼 + * 같은 수가 전혀 없으면 낫싱 +* 게임을 종료한 후 게임을 다시 시작하거나 완전히 종료할 수 있다. +* 잘못 입력된 경우 alert()을 이용한 에러 메시지 출력 후 그 부분부터 다시 입력받는다. + +
+ +### 로그 +* 사용자가 입력한 값과 그 결과값을 표시한다. + +
+ +### 정답 이후 처리 +* 3개의 숫자를 모두 맞히면 게임이 종료되고 '🎉정답을 맞추셨습니다🎉' 메시지 출력과 함께 재시작 문구 및 버튼이 표시된다. + +
diff --git a/images/favicon.png b/images/favicon.png new file mode 100644 index 0000000..a74328e Binary files /dev/null and b/images/favicon.png differ diff --git a/index.html b/index.html index b021b5c..dcaf87e 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,56 @@ - - + + - + 숫자 야구 + + + + -
- +
+
+
+
+ +
+ +

⚾️ 숫자 야구 게임

+ +
+ 1~9까지의 수를 중복없이 3개 입력해주세요. + +
올바른 예) 139
+
틀린 예) 122
+
+ +
+ +
+ + +
+ +
+

📄 결과

+
+
+ + +
+ +

📋 기록

+
+ +
+ + +
diff --git a/src/components/domManipulation.js b/src/components/domManipulation.js new file mode 100644 index 0000000..58a03fd --- /dev/null +++ b/src/components/domManipulation.js @@ -0,0 +1,48 @@ +import { + inputTextFieldEl, + inputBtnEl, + resultTxtEl, + restartMsgEl, + logsEl, + restartBtnEl, + finishBtnEl, +} from "./elements.js"; + +export function showElement(element) { + element.classList.remove("hidden"); +} + +export function hideElement(element) { + element.classList.add("hidden"); +} + +export function clearElements() { + resultTxtEl.innerText = ""; + restartMsgEl.innerText = ""; + logsEl.innerHTML = ""; + restartBtnEl.type = "hidden"; + finishBtnEl.type = "hidden"; + enableBtn(inputBtnEl); +} + +export function enableInputTextField() { + inputTextFieldEl.disabled = false; +} + +export function disableInputTextField() { + inputTextFieldEl.disabled = true; +} + +export function enableBtn(btnEl) { + btnEl.disabled = false; +} + +export function disableBtn(btnEl) { + btnEl.disabled = true; +} + +export function appendLog(log) { + const li = document.createElement("li"); + li.textContent = log; + logsEl.appendChild(li); +} diff --git a/src/components/elements.js b/src/components/elements.js new file mode 100644 index 0000000..506cebc --- /dev/null +++ b/src/components/elements.js @@ -0,0 +1,10 @@ +export const inputTextFieldEl = document.querySelector(".input #input__textfield"); +export const inputBtnEl = document.querySelector(".input #input__btn"); +export const resultTxtEl = document.querySelector(".result .result__text"); +export const restartBtnEl = document.querySelector(".result .result__btn--restart"); +export const finishBtnEl = document.querySelector(".result .result__btn--finish"); +export const restartMsgEl = document.querySelector(".result .result__restartMessage"); +export const titleStartBtnEl = document.querySelector(".titleScreen .titleScreen__startBtn"); +export const titleScreenEl = document.querySelector(".titleScreen"); +export const titleContainersEl = document.querySelector(".titleScreen .container"); +export const logsEl = document.querySelector(".loglist"); diff --git a/src/games/eventHandlers.js b/src/games/eventHandlers.js new file mode 100644 index 0000000..dea81d4 --- /dev/null +++ b/src/games/eventHandlers.js @@ -0,0 +1,79 @@ +import { + inputTextFieldEl, + inputBtnEl, + restartBtnEl, + resultTxtEl, + restartMsgEl, + finishBtnEl, + titleScreenEl, + titleContainersEl, +} from "../components/elements.js"; + +import { + showElement, + hideElement, + clearElements, + enableInputTextField, + disableInputTextField, + enableBtn, + disableBtn, + appendLog, +} from "../components/domManipulation.js"; + +import { RESTART_MESSAGE } from "../utils/constants.js"; + +let game; + +export function initGameInstance(gameInstance) { + game = gameInstance; +} + +export function handleInputTextFieldKeyPress(event) { + if (event.key === "Enter") { + inputBtnEl.click(); + } +} + +export function handleInputBtnClick() { + if (game.compareResult(inputTextFieldEl)) { + resultTxtEl.innerText = game.getResult(); + inputTextFieldEl.value = ""; + inputTextFieldEl.focus(); + + if (game.isGameSet) { + disableInputTextField(); + disableBtn(inputBtnEl); + restartMsgEl.innerText = RESTART_MESSAGE; + restartBtnEl.type = "button"; + finishBtnEl.type = "button"; + } + + appendLog(game.getLastLog()); + } +} + +export function handleRestartBtnClick() { + game.restart(() => { + enableInputTextField(); + enableBtn(inputBtnEl); + clearElements(); + inputTextFieldEl.focus(); + }); +} + +export function handleTitleStartBtnClick() { + enableInputTextField(); + hideElement(titleScreenEl); + inputTextFieldEl.focus(); + game.generateRandomNumber(); +} + +export function handleDOMContentLoaded() { + showElement(titleContainersEl); +} + +export function handleFinishBtnClick() { + showElement(titleScreenEl); + clearElements(); + game.isGameSet = false; +} diff --git a/src/games/game.js b/src/games/game.js new file mode 100644 index 0000000..c6f586c --- /dev/null +++ b/src/games/game.js @@ -0,0 +1,126 @@ +import "../utils/isNotIncludes.js"; +import { ERROR_MESSAGE, RESULT_NOTHING, RESULT_CORRECT } from "../utils/constants.js"; + +export class Game { + #answer; + + constructor() { + this.#answer = undefined; + this.result = ""; + this.isGameSet = false; + this.logs = []; + } + + generateRandomNumber() { + let number = ""; + + while (number.length < 3) { + const randomNumber = Math.floor(Math.random() * 9) + 1; + if (number.isNotIncludes(randomNumber)) { + number += randomNumber; + } + } + console.log(number); + this.#answer = number; + } + + compareResult(inputEl) { + let inputValue = inputEl.value; + if (!this.#isValid(inputValue)) { + this.#alertHandler(inputEl); + return false; + } + const { strikeCount, ballCount, nothingCount } = this.#countHits(inputValue); + this.#setResult(strikeCount, ballCount, nothingCount, () => { + this.logs.push(`${inputValue}: ${this.result}`); + }); + + return true; + } + + #countHits(inputValue) { + let nothingCount = 0; + let strikeCount = 0; + let ballCount = 0; + for (let i = 0; i < this.#answer.length; i++) { + if (inputValue[i] === this.#answer[i]) { + strikeCount += 1; + continue; + } + + if (inputValue.isNotIncludes(this.#answer[i])) { + nothingCount += 1; + } else { + ballCount += 1; + } + } + + return { strikeCount, ballCount, nothingCount }; + } + + #alertHandler(inputEl) { + alert(ERROR_MESSAGE); + this.#resetInputEl(inputEl); + } + + #resetInputEl(inputEl) { + inputEl.value = ""; + inputEl.focus(); + } + + #setResult(strikeCount, ballCount, nothingCount, callback) { + if (nothingCount === 3) { + this.result = RESULT_NOTHING; + } else if (strikeCount === 3) { + this.result = RESULT_CORRECT; + this.isGameSet = true; + } else { + if (strikeCount === 0) { + this.result = `${ballCount}볼`; + } else if (ballCount === 0) { + this.result = `${strikeCount}스트라이크`; + } else { + this.result = `${ballCount}볼 ${strikeCount}스트라이크`; + } + } + + callback(); + } + + getResult() { + return this.result; + } + + getLastLog() { + return this.logs[this.logs.length - 1]; + } + + addLog(logsEl) { + const li = document.createElement("li"); + li.textContent = game.getLastLog(); + logsEl.appendChild(li); + } + + #isValid(input) { + let numPattern = /^[1-9]+$/; + + if (!numPattern.test(input)) { + return false; + } + if (input.length !== 3) { + return false; + } + if (new Set(input).size !== input.length) { + return false; + } + + return true; + } + + restart(complete) { + this.generateRandomNumber(); + this.result = ""; + this.isGameSet = false; + complete(); + } +} diff --git a/src/main.css b/src/main.css new file mode 100644 index 0000000..e24cc95 --- /dev/null +++ b/src/main.css @@ -0,0 +1,114 @@ +body { + margin: 0; + padding: 0; + overflow: hidden; +} + +#app { + margin: 50px; +} + +.titleScreen { + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: #edeae1; + transition: 0.4s; + visibility: visible; + opacity: 1; + user-select: none; + z-index: 5; +} + +.titleScreen.hidden { + visibility: hidden; + opacity: 0; +} + +.titleScreen .triangle-red { + top: 0; + left: 0; + border-bottom: 40vh solid transparent; + border-left: 60vw solid #f1535a; + border-right: 360px solid transparent; +} + +.titleScreen .triangle-offblack { + position: absolute; + bottom: 0; + right: 0; + border-top: 30vh solid transparent; + border-left: 50vw solid transparent; + border-right: 70vw solid #2f2f2f; +} + +.titleScreen .container { + position: fixed; + top: 50%; + left: 50%; + text-align: center; + transform: translate(-50%, -50%); + opacity: 1; + transition: 2s; +} + +.titleScreen .container.hidden { + opacity: 0; +} + +.titleScreen .titleScreen__image { + font-size: 300px; +} + +.titleScreen .titleScreen__title { + color: #2f2f2f; + font-weight: 700; + font-size: 120px; + margin-bottom: 100px; +} + +.titleScreen .titleScreen__startBtn { + font-size: 40px; + margin-bottom: 120px; + color: #2f2f2f; + cursor: pointer; + transition: 0.2s; + font-weight: 700; + display: inline-block; +} + +.titleScreen .titleScreen__startBtn:hover { + color: #f1535a; +} + +.result { + min-height: 150px; +} + +.result .result__text { + font-weight: 700; + margin-bottom: 24px; +} + +.result .result__restartMessage { + margin-bottom: 24px; +} + +.log { + width: 500px; + height: 500px; + position: absolute; + overflow: auto; + margin: 20px; + background-color: rgba(95, 95, 95, 0.15); + border-radius: 20px; +} + +.log .loglist { +} + +.log .loglist li { + margin: 30px; +} diff --git a/src/main.js b/src/main.js index e69de29..50bc186 100644 --- a/src/main.js +++ b/src/main.js @@ -0,0 +1,21 @@ +import { Game } from "./games/game.js"; +import { inputTextFieldEl, inputBtnEl, restartBtnEl, finishBtnEl, titleStartBtnEl } from "./components/elements.js"; +import { + initGameInstance, + handleInputTextFieldKeyPress, + handleInputBtnClick, + handleRestartBtnClick, + handleTitleStartBtnClick, + handleDOMContentLoaded, + handleFinishBtnClick, +} from "./games/eventHandlers.js"; + +const game = new Game(); + +initGameInstance(game); +inputTextFieldEl.addEventListener("keypress", handleInputTextFieldKeyPress); +inputBtnEl.addEventListener("click", handleInputBtnClick); +restartBtnEl.addEventListener("click", handleRestartBtnClick); +titleStartBtnEl.addEventListener("click", handleTitleStartBtnClick); +document.addEventListener("DOMContentLoaded", handleDOMContentLoaded); +finishBtnEl.addEventListener("click", handleFinishBtnClick); diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 0000000..2eec6b2 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,4 @@ +export const RESTART_MESSAGE = "게임을 새로 시작하시겠습니까?"; +export const ERROR_MESSAGE = "1~9까지의 수를 중복없이 3개 입력해주세요."; +export const RESULT_NOTHING = "낫싱"; +export const RESULT_CORRECT = "🎉정답을 맞추셨습니다🎉"; diff --git a/src/utils/isNotIncludes.js b/src/utils/isNotIncludes.js new file mode 100644 index 0000000..7f42372 --- /dev/null +++ b/src/utils/isNotIncludes.js @@ -0,0 +1,3 @@ +String.prototype.isNotIncludes = function (char) { + return !this.includes(char); +};