diff --git a/README.md b/README.md index e078fd41f..c1b0a8127 100644 --- a/README.md +++ b/README.md @@ -1 +1,23 @@ # javascript-racingcar-precourse +# 자동차 경주 + +## 과제 진행 요구 사항 +미션은 자동차 경주 저장소를 포크하고 클론하는 것으로 시작한다. +기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다. +Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다. +AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다. +자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다. + +# 기능 요구 사항 + +- [x] 자동차 이름, 횟수 입력 받기 + - [x] ',' 기준으로 이름 입력 (5자 이하) + - [x] 자연수 숫자 입력받기 + - [x] 잘못된 값 입력시 에러처리 + - [x] 유효한 숫자 아닌 경우 에러처리 +- [x] 자동차 경주 + - [x] 전진 기능 구현 + - [x] 결과 출력 기능 구현 +- [x] 최종 우승자 출력 + - [x] 공동 우승 처리 +- [x] ERROR 출력 diff --git a/__tests__/Test.js b/__tests__/Test.js new file mode 100644 index 000000000..2c5591e99 --- /dev/null +++ b/__tests__/Test.js @@ -0,0 +1,60 @@ +import App from "../src/App.js"; +import { MissionUtils } from "@woowacourse/mission-utils"; + +const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); + + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); + return Promise.resolve(input); + }); +}; + +const mockRandoms = (numbers) => { + MissionUtils.Random.pickNumberInRange = jest.fn(); + + numbers.reduce((acc, number) => { + return acc.mockReturnValueOnce(number); + }, MissionUtils.Random.pickNumberInRange); +}; + +const getLogSpy = () => { + const logSpy = jest.spyOn(MissionUtils.Console, "print"); + logSpy.mockClear(); + return logSpy; +}; + +describe("자동차 경주", () => { + test("기능 테스트", async () => { + // given + const MOVING_FORWARD = 4; + const STOP = 3; + const inputs = ["dao,bazzi", "1"]; + const logs = ["dao : -", "bazzi : ", "최종 우승자 : dao"]; + const logSpy = getLogSpy(); + + mockQuestions(inputs); + mockRandoms([MOVING_FORWARD, STOP]); + + // when + const app = new App(); + await app.run(); + + // then + logs.forEach((log) => { + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log)); + }); + }); + + test("예외 테스트", async () => { + // given + const inputs = ["bazzi,dao,,json"]; + mockQuestions(inputs); + + // when + const app = new App(); + + // then + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5d..310590fc7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,36 @@ +import View from "./View.js"; +import Car from "./Car.js"; +import Race from "./Race.js"; +import OutView from "./OutView.js"; + class App { - async run() {} + #input = new View(); + #race = new Race(); + #output = new OutView(); + + async run() { + const carNames = await this.#input.readInputCar(); + const raceCount = await this.#input.readInputRaceCount(); + + this.#race.setRaceCount(raceCount); + + // Race 객체에 Car객체 추가 + carNames.forEach(carName => { + this.#race.addRacingCar(new Car(carName)); + }); + + for (let count = 0; count < this.#race.getRaceCount(); count++) { + this.#race.racing(); + this.#race.showRacingResult(); + + } + + this.#race.determineWinner(); + this.#race.showWinner(); + const winners = this.#race.getWinner(); + + this.#output.showWinner(winners); + } } export default App; diff --git a/src/Car.js b/src/Car.js new file mode 100644 index 000000000..330311cb7 --- /dev/null +++ b/src/Car.js @@ -0,0 +1,24 @@ +class Car { + #forwardCount=0; + #name; + + constructor(name) { + this.#name = name; + } + + forward(random) { + if (random >= 4) { + this.#forwardCount += 1; + } + } + + getName() { + return this.#name; + } + + getForwardCount() { + return this.#forwardCount; + } +} + +export default Car; \ No newline at end of file diff --git a/src/OutView.js b/src/OutView.js new file mode 100644 index 000000000..fda73a101 --- /dev/null +++ b/src/OutView.js @@ -0,0 +1,12 @@ +import { Console } from "@woowacourse/mission-utils"; + +class OutView { + + showWinner(winners) { + Console.print(`최종 우승자 : ${winners.join(',')}`); + } + + +} + +export default OutView \ No newline at end of file diff --git a/src/Race.js b/src/Race.js new file mode 100644 index 000000000..74e68819a --- /dev/null +++ b/src/Race.js @@ -0,0 +1,53 @@ +import { Random, Console } from "@woowacourse/mission-utils"; + +class Race { + #cars = []; + #winners = []; + #raceCount; + + + addRacingCar(car) { + this.#cars.push(car); + } + + setRaceCount(raceCount) { + this.#raceCount = raceCount; + } + + getRaceCount() { + return this.#raceCount; + } + + racing() { + this.#cars.forEach(car => { + const random = Random.pickNumberInRange(0, 9); + // 4 이상일 경우 전진 + car.forward(random); + }) + } + + showRacingResult() { + // name : --- 형식으로 출력 + this.#cars.forEach( car => { + Console.print(car.getName() + ' : ' + '-'.repeat(car.getForwardCount())); + }) + Console.print(''); + } + + determineWinner() { + const max = Math.max(...this.#cars.map(car => car.getForwardCount())); + + this.#cars.forEach(car => { + if (car.getForwardCount() === max){ + this.#winners.push(car.getName()); + } + }) + } + + getWinner() { + return this.#winners; + } + +} + +export default Race; \ No newline at end of file diff --git a/src/View.js b/src/View.js new file mode 100644 index 000000000..9cd505a22 --- /dev/null +++ b/src/View.js @@ -0,0 +1,31 @@ +import { Console } from "@woowacourse/mission-utils"; + +class View { + #inputCarNameRegex = /^([a-zA-z]{1,5})(,[a-zA-Z]{1,5})*$/ + + #carInputMessage = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n"; + #raceCount = "시도할 횟수는 몇 회인가요?\n"; + + async readInputCar() { + const carNamesStr = await Console.readLineAsync(this.#carInputMessage); + + if (!this.#inputCarNameRegex.test(carNamesStr)){ + throw new Error("[ERROR] : 잘못된 이름 입력\n"); + } + + const carNames = carNamesStr.split(','); + return carNames; + } + + async readInputRaceCount() { + const raceCountStr = await Console.readLineAsync(this.#raceCount); + const raceCount = Number(raceCountStr); + if (raceCount.isNaN() || raceCount <= 0){ + throw new Error("[ERROR] : 잘못된 숫자 입력\n"); + } + + return raceCount; + } +} + +export default View; \ No newline at end of file