-
Notifications
You must be signed in to change notification settings - Fork 212
[자동차 경주] 이유경 미션 제출합니다. #189
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
d7344cf
9249640
f09e3d6
7d53620
b082a21
17c20d5
61dfbf6
30e959d
584c82c
82784fa
c96c685
44a88dd
769e710
b84f3d0
2c78b95
8c38bc2
9ce6932
6152556
d6885b9
c069b29
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,69 @@ | ||
| # javascript-racingcar-precourse | ||
| # 자동차 경주 | ||
| 자동차 이름과 시도 횟수를 입력 받아 자동차 게임을 진행한다. | ||
|
|
||
| ## 기능 | ||
| ### 입력 | ||
| - [x] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 | ||
| - [x] 시도할 횟수 입력 | ||
|
|
||
| ### 파싱 | ||
| - [x] 자동차 이름 입력값을 쉼표(,) 기준으로 나누어 배열로 변환 | ||
| - [x] 각 이름의 공백 제거 | ||
|
|
||
| ### 유효성 검사 | ||
| - [x] 이름 5자 이하 | ||
| - [x] 이름 공백 불가능 | ||
| - [x] 이름 중복 불가능 | ||
| - [x] 시도 횟수는 0보다 큰 정수 | ||
|
|
||
| ### 게임 진행 | ||
| - [x] 자동차 생성 | ||
| - [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 | ||
| - [x] 시도 횟수만큼 게임 진행 | ||
| - [x] 우승자 선출 | ||
|
|
||
| ### 출력 | ||
| - [x] 라운드별 실행 결과 출력 | ||
| - [x] 게임 종료 후 우승자 안내 문구 출력 | ||
|
|
||
| ### 테스트 | ||
| - [x] validator | ||
| - [x] car | ||
| - [x] RacingGame | ||
| - [x] output | ||
|
|
||
| ## 프로젝트 구조 | ||
| ``` | ||
| src/ | ||
| ├── App.js # 프로그램 실행 흐름 관리 (입력 → 검증 → 게임 → 출력) | ||
| ├── index.js # 프로그램 실행 진입점 | ||
| ├── Car.js # 자동차 클래스 (이름, 위치, 이동 로직) | ||
| ├── RacingGame.js # 게임 진행 및 우승자 계산 | ||
| ├── input.js # 사용자 입력 처리 | ||
| ├── output.js # 게임 결과 및 우승자 출력 | ||
| ├── validator.js # 입력값 유효성 검사 | ||
| └── utils/ | ||
| └── randomUtils.js # 랜덤 숫자 생성 (0~9) | ||
| __tests__/ # 단위 테스트 모음 | ||
| ``` | ||
|
|
||
| ## 실행 결과 | ||
| ```bash | ||
| 경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) | ||
| pobi,woni,jun | ||
| 시도할 횟수는 몇 회인가요? | ||
| 3 | ||
| pobi : | ||
| woni : - | ||
| jun : - | ||
|
|
||
| pobi : - | ||
| woni : -- | ||
| jun : - | ||
|
|
||
| pobi : - | ||
| woni : --- | ||
| jun : - | ||
|
|
||
| 최종 우승자 : woni | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { RacingGame } from "../src/RacingGame.js"; | ||
| import * as randomUtils from "../src/utils/randomUtils.js"; | ||
| import { Car } from "../src/Car.js"; | ||
|
|
||
| describe("RacingGame 클래스", () => { | ||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| test("랜덤 값이 4 이상이면 자동차가 전진한다.", () => { | ||
| const carNames = ["pobi", "woni"]; | ||
| const game = new RacingGame(carNames); | ||
|
|
||
| jest.spyOn(randomUtils, "getRandomNumber").mockReturnValue(7); | ||
| game.playRound(); | ||
|
|
||
| game.cars.forEach((car) => { | ||
| expect(car.position).toBe(1); | ||
| }); | ||
| }); | ||
|
|
||
| test("랜덤 값이 4 미만이면 자동차가 전진하지 않는다.", () => { | ||
| const carNames = ["pobi", "woni"]; | ||
| const game = new RacingGame(carNames); | ||
|
|
||
| jest.spyOn(randomUtils, "getRandomNumber").mockReturnValue(2); | ||
|
|
||
| game.playRound(); | ||
| game.cars.forEach((car) => { | ||
| expect(car.position).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe("우승자 선출", () => { | ||
| test("가장 멀리 간 자동차가 단독 우승자가 된다.", () => { | ||
| const game = new RacingGame(["pobi", "woni", "jun"]); | ||
| game.cars = [ | ||
| new Car("pobi"), | ||
| new Car("woni"), | ||
| new Car("jun"), | ||
| ]; | ||
|
|
||
| game.cars[0].position = 2; | ||
| game.cars[1].position = 5; | ||
| game.cars[2].position = 3; | ||
|
|
||
| const winners = game.getWinners(); | ||
| expect(winners).toEqual(["woni"]); | ||
| }); | ||
|
|
||
| test("가장 멀리 간 자동차가 여러 대면 공동 우승자가 된다.", () => { | ||
| const game = new RacingGame(["pobi", "woni", "jun"]); | ||
| game.cars = [ | ||
| new Car("pobi"), | ||
| new Car("woni"), | ||
| new Car("jun"), | ||
| ]; | ||
|
|
||
| game.cars[0].position = 4; | ||
| game.cars[1].position = 4; | ||
| game.cars[2].position = 2; | ||
|
|
||
| const winners = game.getWinners(); | ||
| expect(winners).toEqual(["pobi", "woni"]); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { Car } from "../src/Car.js"; | ||
|
|
||
| describe("Car 클래스", () => { | ||
| test("자동차는 이름과 초기 위치를 가진다", () => { | ||
| const car = new Car("pobi"); | ||
|
|
||
| expect(car.name).toBe("pobi"); | ||
| expect(car.position).toBe(0); | ||
| }); | ||
|
|
||
| test("randomValue가 4인 경우 canMove는 true를 반환한다.", () => { | ||
| const car = new Car("pobi"); | ||
| expect(car.canMove(4)).toBe(true); | ||
| }); | ||
|
|
||
| test("randomValue가 3인 경우 canMove는 false를 반환한다.", () => { | ||
| const car = new Car("pobi"); | ||
| expect(car.canMove(3)).toBe(false); | ||
| }); | ||
|
|
||
| test("move()를 호출하면 position이 1 증가한다", () => { | ||
| const car = new Car("pobi"); | ||
| car.move(); | ||
|
|
||
| expect(car.position).toBe(1); | ||
| }); | ||
|
|
||
| test("move()를 여러 번 호출하면 그 횟수만큼 position이 증가한다", () => { | ||
| const car = new Car("pobi"); | ||
| car.move(); | ||
| car.move(); | ||
| car.move(); | ||
|
|
||
| expect(car.position).toBe(3); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { printRoundResult, printWinners } from "../src/output.js"; | ||
| import { Console } from "@woowacourse/mission-utils"; | ||
|
|
||
| describe("printRoundResult", () => { | ||
| beforeEach(() => { | ||
| jest.spyOn(Console, "print").mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| test("자동차의 현재 위치를 '-'로 출력한다.", () => { | ||
| const cars = [ | ||
| { name: "pobi", position: 2 }, | ||
| { name: "crong", position: 3 }, | ||
| ]; | ||
|
|
||
| printRoundResult(cars); | ||
|
|
||
| expect(Console.print).toHaveBeenCalledWith("pobi : --"); | ||
| expect(Console.print).toHaveBeenCalledWith("crong : ---"); | ||
| expect(Console.print).toHaveBeenCalledWith(""); | ||
| }); | ||
| }); | ||
|
|
||
| describe("printWinners", () => { | ||
| beforeEach(() => { | ||
| jest.spyOn(Console, "print").mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| test("우승자가 한 명일 경우 이름을 출력한다.", () => { | ||
| const winners = ["pobi"]; | ||
| printWinners(winners); | ||
| expect(Console.print).toHaveBeenCalledWith("최종 우승자 : pobi"); | ||
| }); | ||
|
|
||
| test("우승자가 여러 명일 경우 쉼표로 구분해 출력한다.", () => { | ||
| const winners = ["pobi", "woni", "jun"]; | ||
| printWinners(winners); | ||
| expect(Console.print).toHaveBeenCalledWith("최종 우승자 : pobi, woni, jun"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { validateCarNames, validateTryCount, MAX_CAR_NAME_LENGTH } from "../src/validator.js"; | ||
|
|
||
| describe("자동차 이름 유효성 검사", () => { | ||
| test("이름이 비어있으면 에러를 발생시킨다", () => { | ||
| expect(() => validateCarNames(["", "pobi"])).toThrow( | ||
| "[ERROR] 이름에 공백이 포함되어 있습니다." | ||
| ); | ||
| }); | ||
|
|
||
| test(`이름이 ${MAX_CAR_NAME_LENGTH}자를 초과하면 에러를 발생시킨다`, () => { | ||
| const overLengthName = "a".repeat(MAX_CAR_NAME_LENGTH + 1); | ||
| expect(() => validateCarNames([overLengthName, "jun"])).toThrow( | ||
| `[ERROR] 자동차 이름은 ${MAX_CAR_NAME_LENGTH}자 이하만 가능합니다.` | ||
| ); | ||
| }); | ||
|
|
||
| test("이름이 중복되면 에러를 발생시킨다", () => { | ||
| expect(() => validateCarNames(["pobi", "woni", "pobi"])).toThrow( | ||
| "[ERROR] 자동차 이름은 중복될 수 없습니다." | ||
| ); | ||
| }); | ||
|
|
||
| test("모든 이름이 조건을 만족하면 통과한다", () => { | ||
| expect(() => validateCarNames(["pobi", "jun", "woni"])).not.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("시도 횟수 유효성 검사", () => { | ||
| test("숫자가 아닌 값을 입력하면 에러를 발생시킨다", () => { | ||
| expect(() => validateTryCount("abc")).toThrow( | ||
| "[ERROR] 시도 횟수는 숫자여야 합니다." | ||
| ); | ||
| }); | ||
|
|
||
| test("소수 값을 입력하면 에러를 발생시킨다", () => { | ||
| expect(() => validateTryCount("3.5")).toThrow( | ||
| "[ERROR] 시도 횟수는 정수여야 합니다." | ||
| ); | ||
| }); | ||
|
|
||
| test(`0 이하의 값을 입력하면 에러를 발생시킨다`, () => { | ||
| expect(() => validateTryCount("0")).toThrow( | ||
| `[ERROR] 시도 횟수는 0보다 커야 합니다.` | ||
| ); | ||
| expect(() => validateTryCount("-2")).toThrow( | ||
| `[ERROR] 시도 횟수는 0보다 커야 합니다.` | ||
| ); | ||
| }); | ||
|
|
||
| test(`0보다 큰 양의 정수를 입력하면 통과한다`, () => { | ||
| expect(() => validateTryCount("5")).not.toThrow(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,23 @@ | ||
| import { readCarNames, readTryCount } from "./input.js"; | ||
| import { validateCarNames, validateTryCount } from "./validator.js"; | ||
| import { RacingGame } from "./RacingGame.js"; | ||
| import { printWinners } from "./output.js"; | ||
|
|
||
| class App { | ||
| async run() {} | ||
| async run() { | ||
| const carNamesRaw = await readCarNames(); | ||
| const carNames = carNamesRaw.split(",").map((name) => name.trim()); | ||
| validateCarNames(carNames); | ||
|
|
||
| const tryCount = await readTryCount(); | ||
| validateTryCount(tryCount); | ||
|
|
||
| const game = new RacingGame(carNames); | ||
| game.play(tryCount); | ||
|
|
||
| const winners = game.getWinners(); | ||
| printWinners(winners); | ||
| } | ||
| } | ||
|
|
||
| export default App; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export class Car { | ||
| constructor(name) { | ||
| this.name = name; | ||
| this.position = 0; | ||
| } | ||
|
|
||
| canMove(randomValue){ | ||
|
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.
|
||
| return randomValue >= 4; | ||
| } | ||
|
|
||
| move() { | ||
| this.position += 1; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,32 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { Car } from "./Car.js"; | ||||||||||||||||||||||||||||||||||||||||
| import { getRandomNumber } from "./utils/randomUtils.js"; | ||||||||||||||||||||||||||||||||||||||||
| import { printRoundResult } from "./output.js"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export class RacingGame { | ||||||||||||||||||||||||||||||||||||||||
| constructor(carNames) { | ||||||||||||||||||||||||||||||||||||||||
| this.cars = carNames.map((name) => new Car(name)); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| playRound() { | ||||||||||||||||||||||||||||||||||||||||
| this.cars.forEach((car) => { | ||||||||||||||||||||||||||||||||||||||||
| const randomValue = getRandomNumber(); | ||||||||||||||||||||||||||||||||||||||||
| if (car.canMove(randomValue)) { | ||||||||||||||||||||||||||||||||||||||||
| car.move(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+16
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. 자동차 이동을 판별하는 로직을 car.move로 옮기는 건 어떨까요?
Suggested change
Author
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.
리뷰 감사드립니다! 다음부터는 커밋 바디도 작성해봐야겠네요 :) 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. 아하 그런 이유가 있었군요. 저는 자동차가 움직이는 조건을 자동차 특성으로 봤어요. |
||||||||||||||||||||||||||||||||||||||||
| printRoundResult(this.cars); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| play(tryCount) { | ||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i<tryCount; i++){ | ||||||||||||||||||||||||||||||||||||||||
| this.playRound(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| getWinners() { | ||||||||||||||||||||||||||||||||||||||||
| const max = Math.max(...this.cars.map((car) => car.position)); | ||||||||||||||||||||||||||||||||||||||||
| return this.cars | ||||||||||||||||||||||||||||||||||||||||
| .filter((car) => car.position === max) | ||||||||||||||||||||||||||||||||||||||||
| .map((car) => car.name); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { Console } from "@woowacourse/mission-utils"; | ||
|
|
||
| export async function readCarNames() { | ||
| const input = await Console.readLineAsync( | ||
| "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n" | ||
| ); | ||
| return input; | ||
| } | ||
|
|
||
| export async function readTryCount() { | ||
| const input = await Console.readLineAsync( | ||
| "시도할 횟수는 몇 회인가요?\n" | ||
| ); | ||
| return input; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { Console } from "@woowacourse/mission-utils"; | ||
|
|
||
| export function printRoundResult(cars) { | ||
| cars.forEach((car) => { | ||
| const positionBar = "-".repeat(car.position); | ||
|
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. car.position으로 바로 가져오는 것도 좋은데, Car 클래스의 인스턴스를 직접 가져오는 것 보다는 private으로 선언한 뒤, getPosition()등의 함수를 통해 값을 받아보는 건 어떨까요? |
||
| Console.print(`${car.name} : ${positionBar}`); | ||
| }); | ||
| Console.print(""); | ||
| } | ||
|
|
||
| export function printWinners(winners) { | ||
| Console.print(`최종 우승자 : ${winners.join(", ")}`); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { Random } from "@woowacourse/mission-utils"; | ||
|
|
||
| export function getRandomNumber() { | ||
| return Random.pickNumberInRange(0,9); | ||
| } |
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.
메인 로직을 읽는 부분이어서 세부적인 로직을 따로 추상화하면 더 깔끔해질 거 같아요.