diff --git a/README.md b/README.md index e078fd41f..7b4b60320 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ # javascript-racingcar-precourse + +# 기능 목록 + +## 입력 + +- [x] 게임에 참여할 두 개 이상의 자동차이름들을 쉼표(,)를 기준으로 구분해 입력받는다. + - [x] 잘못된 값 입력시, "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션 종료한다. 잘못된 값은 아래 경우에 해당한다. + - [x] 자동차이름이 6자 초과인 경우 + - [x] 구분자가 없는 경우 + = 쉼표가 아닌 구분자가 입력된 경우 + = 자동차이름을 1개만 입력한 경우 + - [x] 숫자로 시작하는 경우(eg. "123a") +- [x] 게임을 시도할 라운드 횟수를 입력받는다. + - [x] 잘못된 값 입력시, "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션 종료한다. 잘못된 값은 아래 경우에 해당한다. + - [x] 숫자가 아닌 경우 + - [x] 음수일 경우 + +## 출력 + +- [x] 매 라운드마다 모든 자동차의 전진여부가 결정된 후, 자동차이름과 전진여부(-)를 출력한다. + - 한 라운드마다 전진 시, '-' 로 표기가 하나씩 늘어난다. +- [x] 사용자가 입력한 횟수만큼의 라운드가 종료된 후, 우승자를 출력한다. + - [x] 우승자가 여러명 일 경우 쉼표(,)로 구분한다. + +## 핵심 기능 + +- [x] 사용자가 입력한 횟수만큼 라운드를 진행한다. + - [x] 매 라운드마다 모든 자동차는 각각 0~9 사이의 무작위 값을 골라주는 함수를 통해 반환된 값이 4 이상인 경우에만 한 칸 `전진`한다. + - 아닐 경우 아무 일도 일어나지 않는다. diff --git a/__tests__/FunctionTest.js b/__tests__/FunctionTest.js new file mode 100644 index 000000000..2c6f3b7a4 --- /dev/null +++ b/__tests__/FunctionTest.js @@ -0,0 +1,139 @@ +import App from "../src/App.js"; + +test("6자 이하의 자동차 이름으로만 구성된 경우 false", () => { + // given + const app = new App(); + + // when & then + expect(app.hasLongCarName(["kim", "doidoi", "young"])).toBe(true); +}); + +test("6자 이상의 자동차 이름이 있는 경우 true", () => { + // given + const app = new App(); + + // when & then + expect(app.hasLongCarName(["kim", "doidoi", "young"])).toBe(true); +}); + +test("숫자로 시작하는 자동차 이름이 없는 경우 false", () => { + // given + const app = new App(); + + // when & then + expect(app.hasCarNameStartingWithNumber(["oioi", "doi99", "kim"])).toBe( + false + ); +}); + +test("숫자로 시작하는 자동차 이름이 있는 경우 true", () => { + // given + const app = new App(); + + // when & then + expect( + app.hasCarNameStartingWithNumber(["oioi", "917", "8abd", "doi99"]) + ).toBe(true); +}); + +test("자동차 위치 속성값 추가하는 함수 테스트", () => { + // given + const app = new App(); + + // when + const result = [ + { name: "kim", position: 0 }, + { name: "bee", position: 0 }, + { name: "zee", position: 0 }, + { name: "doi", position: 0 }, + ]; + + // then + expect(app.createCarsWithPosition(["kim", "bee", "zee", "doi"])).toEqual( + result + ); +}); + +test("우승자 가려내는 함수 테스트", () => { + // given + const app = new App(); + + // when + const testMap = [ + { name: "kim", position: 5 }, + { name: "bee", position: 3 }, + { name: "zee", position: 5 }, + { name: "doi", position: 2 }, + ]; + + // then + const result = [ + { name: "kim", position: 5 }, + { name: "zee", position: 5 }, + ]; + expect(app.getWinners(testMap)).toEqual(result); +}); + +test("자동차가 랜덤으로 한 칸 움직이는 함수 테스트", () => { + // 자동차가 움직이는 경우 + // given + const app = new App(); + let car = { name: "kim", position: 3 }; + + // when + app.moveOneStepRandomly(car, 5); + + // then + expect(car.position).toEqual(4); + + // 자동차가 가만히 있는 경우 + // given + car = { name: "doi", position: 3 }; + + // when + app.moveOneStepRandomly(car, 3); + + // then + expect(car.position).toEqual(3); +}); + +test("한 라운드 진행상황 출력 함수 테스트", () => { + // given + const app = new App(); + const testMap = [ + { name: "kim", position: 2 }, + { name: "bee", position: 4 }, + ]; + console.log = jest.fn(); + + // when + app.printOneRound(testMap); + + // then + expect(console.log).toHaveBeenCalledWith("kim : --"); + expect(console.log).toHaveBeenCalledWith("bee : ----"); + expect(console.log).toHaveBeenCalledWith(""); +}); + +test("우승자 출력하는 함수 테스트", () => { + // 우승자가 한 명일 때 + // given + const app = new App(); + console.log = jest.fn(); + + // when + app.printWinners([{ name: "kim", position: 4 }]); + + // then + expect(console.log).toHaveBeenCalledWith("최종 우승자 : kim"); + + // 우승자가 여러 명일 때 + // when + app.printWinners([ + { name: "kim", position: 7 }, + { name: "doi", position: 7 }, + ]); + + // then + expect(console.log).toHaveBeenCalledWith("최종 우승자 : kim, doi"); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5d..2afc4fb26 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,115 @@ +import { MissionUtils } from "@woowacourse/mission-utils"; +const SEPARATOR = ","; +const MAXIMUM_CARNAME_THRESHOLD = 5; +const MINIMUM_MOVE_THRESHOLD = 4; + +const ERROR_MESSAGE_NO_SEPARATOR = + "[ERROR] 쉼표(,) 구분자가 없습니다. 프로그램을 종료합니다."; +const ERROR_MESSAGE_LONG_CARNAME = + "[ERROR] 구분자는 있으나, 자동차 이름은 5자 이하만 가능합니다. 프로그램을 종료합니다."; +const ERROR_MESSAGE_INVALID_CARNAME = + "[ERROR] 자동차 이름은 문자만 가능합니다. 프로그램을 종료합니다."; +const ERROR_MESSAGE_NOT_NUMBER = + "[ERROR] 숫자가 아닌 값을 입력하셨습니다. 프로그램을 종료합니다."; +const ERROR_MESSAGE_INVALID_ROUND_COUNT = + "[ERROR] 시도 횟수는 1회 이상이어야 합니다. 프로그램을 종료합니다."; + +const GUIDE_MESSAGE_ASKING_CAR_NAMES = + "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로)\n"; +const GUIDE_MESSAGE_ASKING_ROUND_COUNT = "시도할 횟수는 몇 회인가요?\n"; +const GUIDE_MESSAGE_RESULT = "\n실행 결과"; + class App { - async run() {} + async run() { + let carNamesString = await MissionUtils.Console.readLineAsync( + GUIDE_MESSAGE_ASKING_CAR_NAMES + ); + + if (!carNamesString.includes(SEPARATOR)) { + throw new Error(ERROR_MESSAGE_NO_SEPARATOR); + } + + let carNames = carNamesString.split(SEPARATOR); + if (this.hasLongCarName(carNames)) { + throw new Error(ERROR_MESSAGE_LONG_CARNAME); + } + if (this.hasCarNameStartingWithNumber(carNames)) { + throw new Error(ERROR_MESSAGE_INVALID_CARNAME); + } + + let roundCountString = await MissionUtils.Console.readLineAsync( + GUIDE_MESSAGE_ASKING_ROUND_COUNT + ); + if (isNaN(roundCountString)) { + throw new Error(ERROR_MESSAGE_NOT_NUMBER); + } + let roundCount = Number(roundCountString); + if (roundCount <= 0) { + throw new Error(ERROR_MESSAGE_INVALID_ROUND_COUNT); + } + + const cars = this.createCarsWithPosition(carNames); + + MissionUtils.Console.print(GUIDE_MESSAGE_RESULT); + for (let i = 0; i < roundCount; i++) { + for (const car of cars) { + this.moveOneStepRandomly( + car, + MissionUtils.Random.pickNumberInRange(0, 9) + ); + } + this.printOneRound(cars); + } + this.printWinners(this.getWinners(cars)); + } + + getWinners(cars) { + const winners = []; + let maxPosition = -1; + + for (const car of cars) { + if (car.position > maxPosition) { + maxPosition = car.position; + winners.length = 0; + winners.push(car); + } else if (car.position === maxPosition) winners.push(car); + } + return winners; + } + + printWinners(winners) { + if (winners.length === 1) + MissionUtils.Console.print(`최종 우승자 : ${winners[0].name}`); + else { + const winnerNames = winners.map((winner) => winner.name).join(", "); + MissionUtils.Console.print(`최종 우승자 : ${winnerNames}`); + } + } + + printOneRound(cars) { + for (const car of cars) { + const positionOutput = "-".repeat(car.position); + MissionUtils.Console.print(`${car.name} : ${positionOutput}`); + } + MissionUtils.Console.print(""); + } + + moveOneStepRandomly(car, random_number) { + const shouldMove = random_number >= MINIMUM_MOVE_THRESHOLD; + if (shouldMove) car.position++; + } + + createCarsWithPosition(carNames) { + return carNames.map((name) => ({ name, position: 0 })); + } + + hasLongCarName(carNames) { + return carNames.some((name) => name.length > MAXIMUM_CARNAME_THRESHOLD); + } + + hasCarNameStartingWithNumber(carNames) { + return carNames.some((name) => /^\d/.test(name)); + } } export default App;