diff --git a/README.md b/README.md index e078fd41..b1f49f6a 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ # javascript-racingcar-precourse +πŸ“ κΈ°λŠ₯ λͺ©λ‘ + +[V] μ‰Όν‘œ(,)둜 κ΅¬λΆ„λœ μžλ™μ°¨ 이름을 μž…λ ₯λ°›λŠ”λ‹€. + +[V] 이름은 1~5자 이내여야 ν•˜λ©°, κ³΅λ°±Β·λΉˆκ°’μ€ ν—ˆμš©ν•˜μ§€ μ•ŠλŠ”λ‹€. + +[V] μ‹œλ„ 횟수λ₯Ό μž…λ ₯λ°›κ³ , 1 μ΄μƒμ˜ 숫자만 ν—ˆμš©ν•œλ‹€. + +[V] μž…λ ₯값이 잘λͺ»λœ 경우 [ERROR] λ©”μ‹œμ§€λ₯Ό 좜λ ₯ν•˜κ³  μ’…λ£Œν•œλ‹€. + +[V] 각 μžλ™μ°¨λŠ” 이름과 μœ„μΉ˜λ₯Ό κ°€μ§„ Car μΈμŠ€ν„΄μŠ€λ‘œ κ΄€λ¦¬ν•œλ‹€. + +[V] λ§€ λΌμš΄λ“œλ§ˆλ‹€ 0~9의 λ¬΄μž‘μœ„ 값을 생성해 4 이상일 경우 μ „μ§„ν•œλ‹€. + +[V] μ‹œλ„ 횟수만큼 λͺ¨λ“  μžλ™μ°¨μ˜ 이동을 λ°˜λ³΅ν•œλ‹€. + +[V] 각 λΌμš΄λ“œ κ²°κ³Όλ₯Ό pobi : -- ν˜•μ‹μœΌλ‘œ 좜λ ₯ν•œλ‹€. + +[V] κ°€μž₯ 멀리 κ°„ μžλ™μ°¨(λ˜λŠ” 곡동 우승자)λ₯Ό 우승자둜 좜λ ₯ν•œλ‹€. + +[V] μ΅œμ’… κ²°κ³ΌλŠ” μ΅œμ’… 우승자 : pobi, jun ν˜•μ‹μœΌλ‘œ ν‘œμ‹œν•œλ‹€. diff --git a/src/App.js b/src/App.js index 091aa0a5..1cd8d0b1 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,54 @@ +import InputView from "./InputView"; +import OutputView from "./OutputView"; +import Validator from "./Validator"; +import RacingGame from "./RacingGame"; +import { PROMPT_MESSAGES,ERROR_MESSAGES } from "./Constants"; class App { - async run() {} + #game; + #attemptCount; + + async run() { + try{ + await this.setupGame(); + this.playGame(); + this.showResults(); + }catch(error){ + OutputView.printMessage(error.message); + throw error; + } + } + + async setupGame(){ + const carNames=await this.getValidCarNames(); + this.#attemptCount = await this.getValidAttemptCount(); + + this.#game = new RacingGame(carNames); + } + + playGame(){ + OutputView.printMessage(PROMPT_MESSAGES.RESULT_HEADER); + + for (let i = 0; i < this.#attemptCount; i++) { + this.#game.playRound(); + OutputView.printRoundResult(this.#game.getRoundResult()); + } + } + showResults(){ + const winners=this.#game.getWinners(); + OutputView.printWinners(winners); + } + + async getValidCarNames(){ + const carNames=await InputView.readCarNames(); + Validator.validateCarNames(carNames); + return carNames; + } + + async getValidAttemptCount(){ + const attemptCount = await InputView.readAttemptCount(); + Validator.validateAttemptCount(attemptCount); + return attemptCount; + } } export default App; diff --git a/src/Car.js b/src/Car.js new file mode 100644 index 00000000..d28a6d15 --- /dev/null +++ b/src/Car.js @@ -0,0 +1,27 @@ + +import {GAME_RULES} from './Constants.js'; + +class Car{ + #name; + #position; + + constructor(name){ + this.#name=name; + this.#position=0; + } + + move(randomNumber){ + if(randomNumber>=GAME_RULES.MOVE_FORWARD_CONDITION){ + this.#position+=1; + } + } + + getName(){ + return this.#name; + } + + getPosition(){ + return this.#position; + } +} +export default Car; \ No newline at end of file diff --git a/src/Constants.js b/src/Constants.js new file mode 100644 index 00000000..60258027 --- /dev/null +++ b/src/Constants.js @@ -0,0 +1,16 @@ +export const GAME_RULES=Object.freeze({ + MOVE_FORWARD_CONDITION:4, + MIN_RANDOM_NUMBER:0, + MAX_RANDOM_NUMBER:9, +}); + +export const ERROR_MESSAGES=Object.freeze({ + INVALID_CAR_NAME_LENGTH:'μžλ™μ°¨ 이름은 5자 μ΄ν•˜λ§Œ κ°€λŠ₯ν•©λ‹ˆλ‹€.', + INVALID_ATTEMPT_COUNT:'μ‹œλ„ νšŸμˆ˜λŠ” 1 μ΄μƒμ˜ μˆ«μžμ—¬μ•Ό ν•©λ‹ˆλ‹€.', +}); + +export const PROMPT_MESSAGES=Object.freeze({ + GET_CAR_NAMES:'κ²½μ£Όν•  μžλ™μ°¨ 이름을 μž…λ ₯ν•˜μ„Έμš”.(이름은 μ‰Όν‘œ(,) κΈ°μ€€μœΌλ‘œ ꡬ뢄)\n', + GET_ATTEMPT_COUNT:'μ‹œλ„ν•  νšŸμˆ˜λŠ” λͺ‡ νšŒμΈκ°€μš”?\n', + RESULT_HEADER:'\nμ‹€ν–‰ κ²°κ³Ό', +}); \ No newline at end of file diff --git a/src/InputView.js b/src/InputView.js new file mode 100644 index 00000000..93b56407 --- /dev/null +++ b/src/InputView.js @@ -0,0 +1,15 @@ +import { Console } from "@woowacourse/mission-utils"; +import { PROMPT_MESSAGES } from "./Constants"; + +const InputView={ + async readCarNames(){ + const input=await Console.readLineAsync(PROMPT_MESSAGES.GET_CAR_NAMES); + return input.split(','); + }, + + async readAttemptCount(){ + const input=await Console.readLineAsync(PROMPT_MESSAGES.GET_ATTEMPT_COUNT); + return Number(input); + }, +}; +export default InputView; \ No newline at end of file diff --git a/src/OutputView.js b/src/OutputView.js new file mode 100644 index 00000000..112f3197 --- /dev/null +++ b/src/OutputView.js @@ -0,0 +1,21 @@ +import { Console } from "@woowacourse/mission-utils"; + +const OutputView={ + printRoundResult(roundResult){ + roundResult.forEach(car=>{ + const hyphens='-'.repeat(car.position); + Console.print(`${car.name} : ${hyphens}`); + }); + Console.print(''); + }, + + printWinners(winners){ + const winnerString=winners.join(', '); + Console.print(`μ΅œμ’… 우승자 : ${winnerString}`); + }, + + printMessage(message){ + Console.print(message); + } +}; +export default OutputView; \ No newline at end of file diff --git a/src/RacingGame.js b/src/RacingGame.js new file mode 100644 index 00000000..0ee89a1e --- /dev/null +++ b/src/RacingGame.js @@ -0,0 +1,36 @@ +import { Random } from '@woowacourse/mission-utils'; +import Car from './Car.js'; +import { GAME_RULES } from './Constants'; + +class RacingGame { + #cars; + + constructor(carNames) { + this.#cars = carNames.map(name => new Car(name)); + } + playRound() { + this.#cars.forEach(car => { + const randomNumber = Random.pickNumberInRange( + GAME_RULES.MIN_RANDOM_NUMBER, + GAME_RULES.MAX_RANDOM_NUMBER + ); + car.move(randomNumber); + }); + } + + getRoundResult() { + return this.#cars.map(car => ({ + name: car.getName(), + position: car.getPosition(), + })); + } + + getWinners() { + const maxPosition = Math.max(...this.#cars.map(car => car.getPosition())); + + return this.#cars + .filter(car => car.getPosition() === maxPosition) + .map(car => car.getName()); + } +} +export default RacingGame; \ No newline at end of file diff --git a/src/Validator.js b/src/Validator.js new file mode 100644 index 00000000..981a8f2d --- /dev/null +++ b/src/Validator.js @@ -0,0 +1,17 @@ +import { ERROR_MESSAGES } from "./Constants"; + +const Validator={ + validateCarNames(names){ + if(names.some(name=>name.length>5||name.length===0)){ + throw new Error(`[ERROR] ${ERROR_MESSAGES.INVALID_CAR_NAME_LENGTH}`); + } + }, + + validateAttemptCount(count){ + if(isNaN(count)||count<1){ + throw new Error(`[ERROR] ${ERROR_MESSAGES.INVALID_ATTEMPT_COUNT}`); + } + }, +}; + +export default Validator; \ No newline at end of file diff --git a/test/Car.test.js b/test/Car.test.js new file mode 100644 index 00000000..278f5dd3 --- /dev/null +++ b/test/Car.test.js @@ -0,0 +1,15 @@ +import Car from '../src/Car.js'; + +describe('Car ν…ŒμŠ€νŠΈ',()=>{ + test('4 이상일 λ•Œ μ „μ§„ν•œλ‹€',()=>{ + const car=new Car('pobi'); + car.move(4); + expect(car.getPosition()).toBe(1); + }); + + test('3 μ΄ν•˜μΌ λ•Œ λ©ˆμΆ˜λ‹€',()=>{ + const car=new Car('pobi'); + car.move(3); + expect(car.getPosition()).toBe(0); + }); +}); \ No newline at end of file diff --git a/test/RacingGame.test.js b/test/RacingGame.test.js new file mode 100644 index 00000000..228f6a7a --- /dev/null +++ b/test/RacingGame.test.js @@ -0,0 +1,22 @@ +import RacingGame from "../src/RacingGame"; +import {Random} from '@woowacourse/mission-utils'; + +const mockRandom=(numbers)=>{ + Random.pickNumberInRange=jest.fn(); + numbers.reduce((acc,num)=>{ + return acc.mockReturnValueOnce(num); + + },Random.pickNumberInRange); +}; + +describe('RacingGame ν…ŒμŠ€νŠΈ',()=>{ + test('우승자 νŒλ³„ (단독 우승)',()=>{ + mockRandom([4,3,9]); + const game=new RacingGame(['pobi','woni','jun']); + + game.playRound(); + + expect(game.getWinners()).toEqual(['pobi','jun']); + }); + +}) \ No newline at end of file