-
Notifications
You must be signed in to change notification settings - Fork 212
[자동차 경주] 김용주 미션 제출합니다. #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b168d71
f2b17ca
cec3017
d828264
b7e96df
867b7f7
985493a
0bf1270
a9b29bd
dc2ed9d
67f2372
2f7d85a
d2c381f
3b2e389
dcf5304
04dc010
c4a8101
55b2d12
1cb89a1
d48f7ac
95089da
9701a15
fd9c1ee
63793cd
d4fcfbe
a4232a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,96 @@ | ||
| # javascript-racingcar-precourse | ||
|
|
||
| # 자동차 경주 🚗 | ||
|
|
||
| ## 과제 개요 및 정리 | ||
|
|
||
| 사용자가 `,` 로 구분된 자동차 이름들과 5이하의 시도 횟수를 입력하여 자동차 경주 게임을 진행한다.<br> | ||
| 각 시도에서 자동차 마다 `0에서 9사이의 랜덤값`을 생성하여 각 자동차의 전진 여부를 결정한다.<br> | ||
| 랜덤 값이 `4이상일 경우에 전진`한다.<br> | ||
| 모든 시도가 종료될 때 가장 많이 전진한 자동차가 우승한다. | ||
|
|
||
| ### 🏃🏻➡️ 기능 흐름 | ||
|
|
||
| 1. `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)` 라는 입력문을 출력한다. | ||
| 2. `,` 로 구분된 경주할 자동차 이름 문자열을 입력한다. | ||
| 3. `시도할 횟수는 몇 회인가요?` 라는 입력문을 출력한다. | ||
| 4. 시도할 횟수를 5이하의 숫자로 입력한다. | ||
| 5. `실행 결과` 를 출력한다. | ||
| 6. 한번의 시도에서 각 자동차의 전진도를 한줄씩 `[자동차 이름] :` 의 형식으로 출력하고 전진도만큼 `-` 기호를 붙인다. | ||
| 7. 한번의 시도에서 각 자동차에 배정될 0에서 9사이의 랜덤 값을 구한다. 4이상일 경우 전진한다. | ||
| 8. 사용자 입력만큼 시도하고, 모두 종료되면 우승자를 `최종 우승자 : [우승자 이름]` 의 형식으로 출력한다. | ||
| 9. 공동 우승자가 있을 경우, `최종 우승자 : [우승자 이름], [우승자 이름]` 의 형식으로 출력한다. | ||
|
|
||
| ### 🤩 기능 요구사항 | ||
|
|
||
| > #### 사용자 입력 | ||
|
|
||
| - [x] `경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)` 출력과 함께 사용자가 문자열을 입력한다. | ||
| - [x] `시도할 횟수는 몇 회인가요?` 출력과 함께 사용자가 숫자를 입력한다. | ||
| > #### 입력값 검증 | ||
|
|
||
| ##### 자동차 입력값 | ||
|
|
||
| - [x] 공란이면 에러 | ||
| - [x] 분리된 이름이 공란이거나 스페이스 문자라면 에러 | ||
| - [x] 이름에 특수문자가 포함되면 에러 | ||
| - [x] 양쪽 끝에 `,` 문자가 있을 경우, 입력 형식 에러 | ||
|
|
||
| ##### 시도 횟수값 | ||
|
|
||
| - [x] 시도 횟수값이 없으면 에러 | ||
| - [x] 시도 횟수값이 숫자가 아니면 에러 | ||
| - [x] 시도 횟수값이 6이상, 0이하이면 에러 | ||
| > #### 입력 처리 | ||
| - [x] 자동차 입력값 양쪽 공백을 제거한다. | ||
| - [x] 자동차 입력값을 `,`를 기준으로 분리한다. | ||
| - [x] 분리된 이름과 0으로 초기화된 전진도가 저장된 자동차 객체를 각 자동차만큼 생성하고 배열에 저장한다. | ||
| > #### 게임 진행 | ||
| - [x] `실행 결과` 를 출력한다. | ||
| - [x] 각 시도 마다 자동차 객체 배열을 순회하면서, 0에서 9사이의 랜덤값을 추출하고 4이상일 경우 해당 자동차 객체의 전진도를 증가시킨다. | ||
| - [x] 자동차 객체 배열을 순회하면서 자동차의 전진도를 `[자동차 이름] :`와 함께 전진도만큼 `-`를 출력하여 해당 시도의 결과를 출력한다. | ||
| > #### 게임 결과 | ||
| - [x] 자동차의 전진도 최대값을 구하여, 우승자 자동차를 배열에 수집한다. | ||
| - [x] 우승자 자동차 배열을 `,`로 구분된 문자열로 만든다. | ||
| - [x] 최종 우승자를 `최종 우승자 : [우승자 이름]` 혹은 `최종 우승자 : [우승자 이름], [우승자 이름]` 와 같은 형식으로 출력한다. | ||
|
|
||
| --- | ||
|
|
||
| ## 구현 | ||
|
|
||
| ### 폴더 구조 | ||
|
|
||
| ``` | ||
| src | ||
| │ App.js | ||
| │ index.js | ||
| │ | ||
| ├─constants // 상수값 | ||
| │ gameSettings.js | ||
| │ message.js | ||
| │ regex.js | ||
| │ | ||
| ├─domain // 자동차 경주 도메인 | ||
| │ Car.js // 자동차 클래스 정의 | ||
| │ RacingGame.js // 자동차 경주 클래스 정의 | ||
| │ | ||
| └─view // 입출력 처리 | ||
| InputView.js | ||
| OutputView.js | ||
| Validation.js // 입력값 유효성 검사 | ||
| ``` | ||
|
|
||
| ### 구현 전 아이디어 | ||
|
|
||
| 객체 지향 프로그래밍의 MVC 패턴을 참고하여 입출력 관련 기능과 주요 도메인 관련 기능이 분리되도록 폴더 구조를 형성하였다.<br> | ||
| 자바스크립트는 객체 지향이 아닌 함수 지향이라고 할 수 있지만 주어진 과제의 전체적인 구현 목표는 입력과 출력을 포함하는 기능을 구현하는 것이 목표이기 때문에 부작용이 발생할 수 있는 입출력 관련 기능을 따로 배치하는 것이 필요하다고 생각하였다.<br><br> | ||
| 따라서 다음과 같이 폴더 구조를 생각하여 리팩토링을 진행했다.<br> | ||
|
|
||
| - App (컨트롤러) : 전체적인 기능들의 파이프라인으로써 자동차 경주의 기능 흐름을 정의한다. | ||
| - view (뷰) : 자동차 경주의 입력과 출력 관련 기능 집합 | ||
| - domain (모델) : 자동차 경주의 진행 관련 기능의 집합 | ||
|
|
||
| ### 추가 아이디어 | ||
|
|
||
| 유효성 검사를 초기엔 입력 부분에 대해서만 진행했다. 이 부분에 관해서 chatGPT에게 물어본 결과 자동차 이름 관련한 형식의 유효성 검사는 입력 부분에서 진행하는것이 옳고, 자동차 이름의 규칙을 정하는 것은 도메인 내부의 유효성 검사로 넣는 것이 옳다는 결과를 얻었다.<br><br> | ||
| 그 이유는 현재는 콘솔을 통한 입력으로 자동차 이름을 받고 있지만 이후 확장으로 파일 입력으로 자동차 이름을 받을 때, 기존 입력에서 이름 규칙 유효성 검사를 진행하면 OCP 규칙에 위반된다. 파일입력 관련 기능을 추가 했지만, 코드 중복을 피하기 위해 기존 입력에서 이름 규칙 유효성 검사를 따로 빼야하는 수정이 불가피하기 때문이다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,31 @@ | ||
| import * as InputView from "./view/InputView.js"; | ||
| import * as OutputView from "./view/OutputView.js"; | ||
| import RacingGame from "./domain/RacingGame.js"; | ||
|
|
||
| class App { | ||
| async run() {} | ||
| async run() { | ||
| try { | ||
| await this.#playSingleGame(); | ||
| } catch (error) { | ||
| OutputView.printErrorMessage(error); | ||
| throw error; | ||
| } | ||
| } | ||
| async #playSingleGame() { | ||
| const carList = await InputView.readCarNames(); | ||
| const attemptCount = await InputView.readAttemptCount(); | ||
|
|
||
| const raceGame = new RacingGame(carList); | ||
|
|
||
| OutputView.printRaceHeader(); | ||
| for (let i = 0; i < attemptCount; i++) { | ||
| raceGame.runSingleAttempt(); | ||
| OutputView.printSingleAttemptResult(raceGame.getCarStatusList()); | ||
| } | ||
| const raceWinner = raceGame.judgeWinner(); | ||
|
|
||
| OutputView.printWinners(raceWinner); | ||
| } | ||
| } | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export const GameSettings = Object.freeze({ | ||
| MAX_INPUT: 1000, | ||
|
|
||
| MAX_NAME: 100, | ||
|
|
||
| MAX_ATTEMPT: 5, | ||
| MIN_ATTEMPT: 1, | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 게임 진행 횟수와 관련해서 최솟값 제한을 둔 부분이 인상 깊습니다! |
||
| PROGRESS_VISUAL: "-", | ||
| VISUAL_SPACE: "", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 VISUAL_SPACE는 무슨 역할을 하는 상수인가요? 필요한 상수로는 보이지 않는데, 선언하신 이유가 궁금합니다! |
||
|
|
||
| CAR_NAME_DELIMITER: ",", | ||
| STRINGIZE_DELIMITER: ", ", | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { GameSettings } from "./gameSettings.js"; | ||
|
|
||
| export const Message = Object.freeze({ | ||
| INPUT_CAR: "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n", | ||
| INPUT_ATTEMPT: "시도할 횟수는 몇 회인가요?\n", | ||
| RACE_HEADER: "실행 결과", | ||
| PREFIX_WINNER: "최종 우승자 : ", | ||
| }); | ||
|
|
||
| export const ErrorMessage = Object.freeze({ | ||
| PREFIX: "[ERROR]", | ||
|
|
||
| BIG_INPUT: `너무 큰 입력값이 들어왔습니다.(${GameSettings.MAX_INPUT}자 제한)`, | ||
|
|
||
| CAR_INPUT_NON: "자동차 이름 입력값이 없습니다.", | ||
| CAR_INPUT_SPECIAL_CHARACTER: "자동차 이름엔 특수문자가 포함될 수 없습니다.", | ||
| CAR_INPUT_FORM: "입력값의 입력 형식을 확인해주세요.", | ||
| CAR_NAME_DUPLICATION: "중복된 자동차 이름이 존재합니다.", | ||
| CAR_NAME_LIMIT: `너무 긴 이름입니다. (${GameSettings.MAX_NAME}자 제한)`, | ||
|
|
||
| ATTEMPT_INPUT_NON: "시도 횟수를 입력하지 않음", | ||
| ATTEMPT_INPUT_NAN: "시도 횟수 입력값이 숫자가 아닙니다.", | ||
| ATTEMPT_INPUT_OVER_LIMIT: | ||
| "입력된 시도 횟수값이 제한된 범위를 초과하였습니다. (1 ~ 5)", | ||
| ATTEMPT_INPUT_ISINTEGER: "시도 횟수 값이 소수입니다.", | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export const Regex = Object.freeze({ | ||
| MATCH_CAR_SPECIAL_CHARACTER: /[!@#$%^&*\(\)_+~\`;:\"\'\{\}\[\]<>.\/?\\\-=|]/g, | ||
| CHECK_CAR_FORMAT: /,\s*,|^,|,$/g, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| export default class Car { | ||
| #carName; | ||
| #progress; | ||
|
|
||
| constructor({ carName }) { | ||
| this.#carName = carName; | ||
| this.#progress = 0; | ||
| } | ||
|
|
||
| get carName() { | ||
| return this.#carName; | ||
| } | ||
| get progress() { | ||
| return this.#progress; | ||
| } | ||
| get carStatus() { | ||
| return { carName: this.#carName, progress: this.#progress }; | ||
| } | ||
|
|
||
| move(randomNumber) { | ||
| if (randomNumber >= 4) this.#progress += 1; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { MissionUtils, Console } from "@woowacourse/mission-utils"; | ||
| import Car from "./Car.js"; | ||
| import { ErrorMessage } from "../constants/message.js"; | ||
| import { GameSettings } from "../constants/gameSettings.js"; | ||
|
|
||
| export default class RacingGame { | ||
| #carObjects; | ||
|
|
||
| constructor(carNames) { | ||
| this.#validateCarNames(carNames); | ||
| this.#carObjects = carNames.map((name) => new Car({ carName: name })); | ||
| } | ||
|
|
||
| getCarStatusList() { | ||
| return this.#carObjects.map((car) => car.carStatus); | ||
| } | ||
|
|
||
| runSingleAttempt() { | ||
| this.#carObjects.forEach((car) => | ||
| car.move(MissionUtils.Random.pickNumberInRange(0, 9)) | ||
| ); | ||
| } | ||
|
|
||
| judgeWinner() { | ||
| const maxProgress = this.#getMaxCarProgress(); | ||
| const winnerList = this.#getRaceWinnerList(maxProgress); | ||
| return winnerList; | ||
| } | ||
| #validateCarNames(carNames) { | ||
| // 자동차 이름이 중복됐을 경우 | ||
| const uniqueNames = new Set(carNames); | ||
|
|
||
| if (uniqueNames.size !== carNames.length) | ||
| throw new Error( | ||
| `${ErrorMessage.PREFIX} ${ErrorMessage.CAR_NAME_DUPLICATION}` | ||
| ); | ||
| // 자동차 이름이 너무 길 경우 | ||
| const hasInvalidLength = carNames.some( | ||
| (car) => car.length > GameSettings.MAX_NAME | ||
| ); | ||
| if (hasInvalidLength) | ||
| throw new Error(`${ErrorMessage.PREFIX} ${ErrorMessage.CAR_NAME_LIMIT}`); | ||
| } | ||
| #getMaxCarProgress() { | ||
| const maxProgress = this.#carObjects.reduce((currMax, car) => { | ||
| return Math.max(currMax, car.progress); | ||
| }, -Infinity); | ||
| return maxProgress; | ||
| } | ||
| #getRaceWinnerList(maxProgress) { | ||
| const raceWinners = this.#carObjects.reduce((winner, car) => { | ||
| if (car.progress === maxProgress) { | ||
| winner.push(car.carName); | ||
| } | ||
| return winner; | ||
| }, []); | ||
| return raceWinners; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Console } from "@woowacourse/mission-utils"; | ||
| import * as Validation from "./Validation.js"; | ||
| import { Message } from "../constants/message.js"; | ||
| import { GameSettings } from "../constants/gameSettings.js"; | ||
|
|
||
| export async function readCarNames() { | ||
| const carNames = await Console.readLineAsync(Message.INPUT_CAR); | ||
| Validation.validateCarInput(carNames); | ||
| return inputCarProcess(carNames); | ||
| } | ||
| function inputCarProcess(rawCarString) { | ||
| const strippedString = rawCarString.trim(); | ||
| const splittedArray = strippedString.split(GameSettings.CAR_NAME_DELIMITER); | ||
| return splittedArray; | ||
| } | ||
| export async function readAttemptCount() { | ||
| const attemptCount = await Console.readLineAsync(Message.INPUT_ATTEMPT); | ||
| Validation.validateAttemptInput(attemptCount); | ||
| return inputAttemptProcess(attemptCount); | ||
| } | ||
| function inputAttemptProcess(attempt) { | ||
| return Number(attempt); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { Console } from "@woowacourse/mission-utils"; | ||
| import { Message } from "../constants/message.js"; | ||
| import { GameSettings } from "../constants/gameSettings.js"; | ||
|
|
||
| export function printRaceHeader() { | ||
| Console.print(Message.RACE_HEADER); | ||
| } | ||
| export function printWinners(winnerList) { | ||
| Console.print(`${Message.PREFIX_WINNER}${stringizeList(winnerList)}`); | ||
| } | ||
| function stringizeList(rawlist) { | ||
| return rawlist.join(GameSettings.STRINGIZE_DELIMITER); | ||
| } | ||
| export function printSingleAttemptResult(carList) { | ||
| carList.forEach((car) => { | ||
| Console.print(`${car.carName} : ${visualizeProgress(car.progress)}`); | ||
| }); | ||
| } | ||
| function visualizeProgress(progress) { | ||
| return Array.from( | ||
| { length: progress }, | ||
| (v, k) => GameSettings.PROGRESS_VISUAL | ||
| ).join(GameSettings.VISUAL_SPACE); | ||
| } | ||
| export function printErrorMessage(error) { | ||
| Console.print(error.message); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { Regex } from "../constants/regex.js"; | ||
| import { ErrorMessage } from "../constants/message.js"; | ||
| import { GameSettings } from "../constants/gameSettings.js"; | ||
|
|
||
| export function validateCarInput(carInput) { | ||
| // 자동차 입력값이 없을 경우 | ||
| if (!carInput) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.CAR_INPUT_NON}`); | ||
| // 자동차 입력값이 너무 큰 경우 | ||
| if (carInput.length > GameSettings.MAX_INPUT) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.BIG_INPUT}`); | ||
| // 자동차 입력값에 특수 문자가 포함됐을 경우 | ||
| if (Regex.MATCH_CAR_SPECIAL_CHARACTER.test(carInput)) | ||
| throw Error( | ||
| `${ErrorMessage.PREFIX} ${ErrorMessage.CAR_INPUT_SPECIAL_CHARACTER}` | ||
| ); | ||
| // 자동차 입력값에 이름이 공백인 경우나 양쪽 끝에 쉼표가 포함됐을 경우 | ||
| if (Regex.CHECK_CAR_FORMAT.test(carInput)) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.CAR_INPUT_FORM}`); | ||
| } | ||
|
|
||
| export function validateAttemptInput(attemptInput) { | ||
| // 시도 입력값이 없을 경우 | ||
| if (!attemptInput) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.ATTEMPT_INPUT_NON}`); | ||
| // 시도 입력값이 너무 큰 경우 | ||
| if (attemptInput.length > GameSettings.MAX_INPUT) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.BIG_INPUT}`); | ||
|
|
||
| const numberAttempt = Number(attemptInput); | ||
| // 시도 입력값이 숫자가 아닌 경우 | ||
| if (!Number.isFinite(numberAttempt)) | ||
| throw Error(`${ErrorMessage.PREFIX} ${ErrorMessage.ATTEMPT_INPUT_NAN}`); | ||
| // 시도 입력값이 소수인 경우 | ||
| if (!Number.isInteger(numberAttempt)) | ||
| throw Error( | ||
| `${ErrorMessage.PREFIX} ${ErrorMessage.ATTEMPT_INPUT_ISINTEGER}` | ||
| ); | ||
| // 시도 입력값이 정해진 범위를 넘은 경우 | ||
| if ( | ||
| numberAttempt < GameSettings.MIN_ATTEMPT || | ||
| numberAttempt > GameSettings.MAX_ATTEMPT | ||
| ) | ||
| throw Error( | ||
| `${ErrorMessage.PREFIX} ${ErrorMessage.ATTEMPT_INPUT_OVER_LIMIT}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력된 횟수만큼 라운드를 진행하는 로직을 한 번에 처리하지 않으시고 따로 왜runSingleAttempt로 별도의 메소드를 두시고 여러번 호출하셨는지 궁금합니다! 걸리는 시간은 비슷할 것 같은데 특별한 이유가 있으셨는지 궁금합니다.