From d7344cfb542169ea61e3fac3e20a53c724d277c8 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:26:57 +0900 Subject: [PATCH 01/20] =?UTF-8?q?docs(readme):=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e078fd41..668fadb2 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# javascript-racingcar-precourse +# 자동차 경주 +자동차 이름과 시도 횟수를 입력 받아 자동차 게임을 진행한다. + +## 기능 +### 입력 +- [ ] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 +- [ ] 시도할 횟수 입력 + +### 유효성 검사 +- [ ] 쉼표를 기준으로 구분 가능 +- [ ] 이름 5자 이하 +- [ ] 이름 공백 불가능 +- [ ] 이동 횟수는 0보다 큰 정수 + +### 게임 진행 +- [ ] 자동차 생성 +- [ ] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 +- [ ] 시도 횟수만큼 게임 진행 후 우승자 선출 + +### 출력 +- [ ] 차수별 실행 결과 출력 +- [ ] 게임 종료 후 우승자 안내 문구 출력 + +### 테스트 +- [ ] 정상 작동 확인 \ No newline at end of file From 92496401154ae39534589ca89ba1df3a29eaf44c Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:30:08 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat(input):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=9E=85=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/App.js | 6 +++++- src/input.js | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/input.js diff --git a/README.md b/README.md index 668fadb2..bea82c89 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## 기능 ### 입력 -- [ ] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 +- [x] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 - [ ] 시도할 횟수 입력 ### 유효성 검사 diff --git a/src/App.js b/src/App.js index 091aa0a5..6fb7d839 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,9 @@ +import { readCarNames } from "./input.js"; + class App { - async run() {} + async run() { + const carNames = await readCarNames(); + } } export default App; diff --git a/src/input.js b/src/input.js new file mode 100644 index 00000000..e903600b --- /dev/null +++ b/src/input.js @@ -0,0 +1,8 @@ +import { Console } from "@woowacourse/mission-utils"; + +export async function readCarNames() { + const input = await Console.readLineAsync( + "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n" + ); + return input; +} From f09e3d6677aa893afe994673bc4ce198f7a43a95 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:31:16 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat(input):=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=ED=95=A0=20=ED=9A=9F=EC=88=98=20=EC=9E=85=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/App.js | 3 ++- src/input.js | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bea82c89..81442303 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 기능 ### 입력 - [x] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 -- [ ] 시도할 횟수 입력 +- [x] 시도할 횟수 입력 ### 유효성 검사 - [ ] 쉼표를 기준으로 구분 가능 diff --git a/src/App.js b/src/App.js index 6fb7d839..713058c3 100644 --- a/src/App.js +++ b/src/App.js @@ -1,8 +1,9 @@ -import { readCarNames } from "./input.js"; +import { readCarNames, readTryCount } from "./input.js"; class App { async run() { const carNames = await readCarNames(); + const tryCount = await readTryCount(); } } diff --git a/src/input.js b/src/input.js index e903600b..26880620 100644 --- a/src/input.js +++ b/src/input.js @@ -6,3 +6,10 @@ export async function readCarNames() { ); return input; } + +export async function readTryCount() { + const input = await Console.readLineAsync( + "시도할 횟수는 몇 회인가요?\n" + ); + return input; +} \ No newline at end of file From 7d53620327ee4447d4470a8972e7ab31f216b9bc Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:38:54 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat(app):=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- src/App.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 81442303..abfc7876 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,11 @@ - [x] 경주할 자동차 이름(이름은 쉼표(,) 기준으로 구분) 입력 - [x] 시도할 횟수 입력 +### 파싱 +- [x] 자동차 이름 입력값을 쉼표(,) 기준으로 나누어 배열로 변환 +- [x] 각 이름의 공백 제거 + ### 유효성 검사 -- [ ] 쉼표를 기준으로 구분 가능 - [ ] 이름 5자 이하 - [ ] 이름 공백 불가능 - [ ] 이동 횟수는 0보다 큰 정수 diff --git a/src/App.js b/src/App.js index 713058c3..153d8b77 100644 --- a/src/App.js +++ b/src/App.js @@ -2,7 +2,8 @@ import { readCarNames, readTryCount } from "./input.js"; class App { async run() { - const carNames = await readCarNames(); + const carNamesRaw = await readCarNames(); + const carNames = carNamesRaw.split(",").map((name) => name.trim()); const tryCount = await readTryCount(); } } From b082a210d72ada8bc2be91577429e7e03c8afc54 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:48:47 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat(validator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- src/App.js | 2 ++ src/validator.js | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/validator.js diff --git a/README.md b/README.md index abfc7876..c8d78546 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ - [x] 각 이름의 공백 제거 ### 유효성 검사 -- [ ] 이름 5자 이하 -- [ ] 이름 공백 불가능 +- [x] 이름 5자 이하 +- [x] 이름 공백 불가능 - [ ] 이동 횟수는 0보다 큰 정수 ### 게임 진행 diff --git a/src/App.js b/src/App.js index 153d8b77..846d6862 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,11 @@ import { readCarNames, readTryCount } from "./input.js"; +import { validateCarNames } from "./validator.js"; class App { async run() { const carNamesRaw = await readCarNames(); const carNames = carNamesRaw.split(",").map((name) => name.trim()); + validateCarNames(carNames); const tryCount = await readTryCount(); } } diff --git a/src/validator.js b/src/validator.js new file mode 100644 index 00000000..5150db34 --- /dev/null +++ b/src/validator.js @@ -0,0 +1,9 @@ +export function validateCarNames(names) { + if (names.some((name) => name.length === 0)) { + throw new Error("[ERROR] 이름에 공백이 포함되어 있습니다."); + } + + if (names.some((name) => name.trim().length > 5)) { + throw new Error("[ERROR] 자동차 이름은 5자 이하만 가능합니다."); + } +} From 17c20d59d91a801e424d6501a2fd02436c2dd25e Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 19:53:34 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat(validator):=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/App.js | 3 ++- src/validator.js | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8d78546..cf0c61f3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ ### 유효성 검사 - [x] 이름 5자 이하 - [x] 이름 공백 불가능 -- [ ] 이동 횟수는 0보다 큰 정수 +- [x] 시도 횟수는 0보다 큰 정수 ### 게임 진행 - [ ] 자동차 생성 diff --git a/src/App.js b/src/App.js index 846d6862..35181ec5 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,5 @@ import { readCarNames, readTryCount } from "./input.js"; -import { validateCarNames } from "./validator.js"; +import { validateCarNames, validateTryCount } from "./validator.js"; class App { async run() { @@ -7,6 +7,7 @@ class App { const carNames = carNamesRaw.split(",").map((name) => name.trim()); validateCarNames(carNames); const tryCount = await readTryCount(); + validateTryCount(tryCount); } } diff --git a/src/validator.js b/src/validator.js index 5150db34..a2e687bc 100644 --- a/src/validator.js +++ b/src/validator.js @@ -7,3 +7,19 @@ export function validateCarNames(names) { throw new Error("[ERROR] 자동차 이름은 5자 이하만 가능합니다."); } } + +export function validateTryCount(count) { + const num = Number(count); + + if (Number.isNaN(num)) { + throw new Error("[ERROR] 시도 횟수는 숫자여야 합니다."); + } + + if (!Number.isInteger(num)) { + throw new Error("[ERROR] 시도 횟수는 정수여야 합니다."); + } + + if (num<=0) { + throw new Error("[ERROR] 시도 횟수는 0보다 커야 합니다."); + } +} From 61dfbf66662cbf2a45f941c07c68d6e647a944c0 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 20:48:10 +0900 Subject: [PATCH 07/20] =?UTF-8?q?refactor(validator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=B5=9C=EB=8C=80=20=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=83=81=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validator.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/validator.js b/src/validator.js index a2e687bc..005a2767 100644 --- a/src/validator.js +++ b/src/validator.js @@ -1,10 +1,12 @@ +const MAX_CAR_NAME_LENGTH = 5; + export function validateCarNames(names) { if (names.some((name) => name.length === 0)) { throw new Error("[ERROR] 이름에 공백이 포함되어 있습니다."); } - if (names.some((name) => name.trim().length > 5)) { - throw new Error("[ERROR] 자동차 이름은 5자 이하만 가능합니다."); + if (names.some((name) => name.trim().length > MAX_CAR_NAME_LENGTH)) { + throw new Error(`[ERROR] 자동차 이름은 ${MAX_CAR_NAME_LENGTH}자 이하만 가능합니다.`); } } From 30e959d3334727136d5df5dfaa95eed3e134cef3 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 20:59:44 +0900 Subject: [PATCH 08/20] =?UTF-8?q?test(validator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- __tests__/validator.test.js | 47 +++++++++++++++++++++++++++++++++++++ src/validator.js | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 __tests__/validator.test.js diff --git a/README.md b/README.md index cf0c61f3..317a5c38 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,4 @@ - [ ] 게임 종료 후 우승자 안내 문구 출력 ### 테스트 -- [ ] 정상 작동 확인 \ No newline at end of file +- [x] validator \ No newline at end of file diff --git a/__tests__/validator.test.js b/__tests__/validator.test.js new file mode 100644 index 00000000..f79adfb7 --- /dev/null +++ b/__tests__/validator.test.js @@ -0,0 +1,47 @@ +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", "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(); + }); +}); \ No newline at end of file diff --git a/src/validator.js b/src/validator.js index 005a2767..0efaf3f8 100644 --- a/src/validator.js +++ b/src/validator.js @@ -1,4 +1,4 @@ -const MAX_CAR_NAME_LENGTH = 5; +export const MAX_CAR_NAME_LENGTH = 5; export function validateCarNames(names) { if (names.some((name) => name.length === 0)) { From 584c82c477532594e263df4fa0b228e95b961c52 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 21:08:06 +0900 Subject: [PATCH 09/20] =?UTF-8?q?feat(car,=20racinggame):=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/Car.js | 10 ++++++++++ src/RacingGame.js | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/Car.js create mode 100644 src/RacingGame.js diff --git a/README.md b/README.md index 317a5c38..3e69d961 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - [x] 시도 횟수는 0보다 큰 정수 ### 게임 진행 -- [ ] 자동차 생성 +- [x] 자동차 생성 - [ ] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 - [ ] 시도 횟수만큼 게임 진행 후 우승자 선출 diff --git a/src/Car.js b/src/Car.js new file mode 100644 index 00000000..934cb5fa --- /dev/null +++ b/src/Car.js @@ -0,0 +1,10 @@ +export class Car { + constructor(name) { + this.name = name; + this.position = 0; + } + + move() { + this.position += 1; + } +} \ No newline at end of file diff --git a/src/RacingGame.js b/src/RacingGame.js new file mode 100644 index 00000000..b59cd34e --- /dev/null +++ b/src/RacingGame.js @@ -0,0 +1,7 @@ +import { Car } from "./Car.js"; + +export class RacintGame { + constructor(carNames) { + this.cars = carNames.map((name) => new Car(name)); + } +} \ No newline at end of file From 82784faabc93767c9d0af72f2526433d071b31e5 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 21:08:59 +0900 Subject: [PATCH 10/20] =?UTF-8?q?test(car):=20=EC=9E=90=EB=8F=99=EC=B0=A8?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/car.test.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 __tests__/car.test.js diff --git a/__tests__/car.test.js b/__tests__/car.test.js new file mode 100644 index 00000000..de621ebf --- /dev/null +++ b/__tests__/car.test.js @@ -0,0 +1,26 @@ +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("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); + }); +}); \ No newline at end of file From c96c685f3bffe0889bea2868be040e0970b3d97b Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 21:39:57 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat(racinggame):=20=EB=9E=9C=EB=8D=A4=20?= =?UTF-8?q?=EA=B0=92=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/Car.js | 4 ++++ src/RacingGame.js | 12 +++++++++++- src/utils/randomUtils.js | 5 +++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/utils/randomUtils.js diff --git a/README.md b/README.md index 3e69d961..8c006d21 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ### 게임 진행 - [x] 자동차 생성 -- [ ] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 +- [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 - [ ] 시도 횟수만큼 게임 진행 후 우승자 선출 ### 출력 diff --git a/src/Car.js b/src/Car.js index 934cb5fa..c81f0b7f 100644 --- a/src/Car.js +++ b/src/Car.js @@ -4,6 +4,10 @@ export class Car { this.position = 0; } + canMove(randomValue){ + return randomValue >= 4; + } + move() { this.position += 1; } diff --git a/src/RacingGame.js b/src/RacingGame.js index b59cd34e..2fd59838 100644 --- a/src/RacingGame.js +++ b/src/RacingGame.js @@ -1,7 +1,17 @@ import { Car } from "./Car.js"; +import { getRandomNumber } from "./utils/randomUtils.js"; -export class RacintGame { +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(); + } + }) + } } \ No newline at end of file diff --git a/src/utils/randomUtils.js b/src/utils/randomUtils.js new file mode 100644 index 00000000..b168b734 --- /dev/null +++ b/src/utils/randomUtils.js @@ -0,0 +1,5 @@ +import { Random } from "@woowacourse/mission-utils"; + +export function getRandomNumber() { + return Random.pickNumberInRange(0,9); +} \ No newline at end of file From 44a88ddf03c96b87735134afdcb23322b0f88a98 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 21:41:35 +0900 Subject: [PATCH 12/20] =?UTF-8?q?test(car,=20racinggame):=20=EB=9E=9C?= =?UTF-8?q?=EB=8D=A4=20=EA=B0=92=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=A0=84?= =?UTF-8?q?=EC=A7=84=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/RacingGame.test.js | 32 ++++++++++++++++++++++++++++++++ __tests__/car.test.js | 10 ++++++++++ 2 files changed, 42 insertions(+) create mode 100644 __tests__/RacingGame.test.js diff --git a/__tests__/RacingGame.test.js b/__tests__/RacingGame.test.js new file mode 100644 index 00000000..c382a08a --- /dev/null +++ b/__tests__/RacingGame.test.js @@ -0,0 +1,32 @@ +import { RacingGame } from "../src/RacingGame.js"; +import * as randomUtils from "../src/utils/randomUtils.js"; // ← 모듈 전체를 가져와서 spyOn 가능하게 + +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); + }); + }); +}); \ No newline at end of file diff --git a/__tests__/car.test.js b/__tests__/car.test.js index de621ebf..810ab1b6 100644 --- a/__tests__/car.test.js +++ b/__tests__/car.test.js @@ -8,6 +8,16 @@ describe("Car 클래스", () => { 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(); From 769e710e4e3debd03231392bd3a4487fc71fb977 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:10:46 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat(racinggame):=20=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=EB=A7=8C=ED=81=BC=20=EA=B2=8C=EC=9E=84=20?= =?UTF-8?q?=EC=A7=84=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- src/App.js | 5 +++++ src/RacingGame.js | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c006d21..14696192 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ ### 게임 진행 - [x] 자동차 생성 - [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 -- [ ] 시도 횟수만큼 게임 진행 후 우승자 선출 +- [x] 시도 횟수만큼 게임 진행 +- [ ] 우승자 선출 ### 출력 - [ ] 차수별 실행 결과 출력 diff --git a/src/App.js b/src/App.js index 35181ec5..127d1986 100644 --- a/src/App.js +++ b/src/App.js @@ -1,13 +1,18 @@ import { readCarNames, readTryCount } from "./input.js"; import { validateCarNames, validateTryCount } from "./validator.js"; +import { RacingGame } from "./RacingGame.js"; class App { 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); } } diff --git a/src/RacingGame.js b/src/RacingGame.js index 2fd59838..110a297b 100644 --- a/src/RacingGame.js +++ b/src/RacingGame.js @@ -14,4 +14,10 @@ export class RacingGame { } }) } + + play(tryCount) { + for (let i = 0; i Date: Mon, 27 Oct 2025 22:11:39 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat(output):=20=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=EB=B3=84=20=EC=8B=A4=ED=96=89=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/RacingGame.js | 4 +++- src/output.js | 9 +++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/output.js diff --git a/README.md b/README.md index 14696192..a4e1e1a4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ - [ ] 우승자 선출 ### 출력 -- [ ] 차수별 실행 결과 출력 +- [x] 라운드별 실행 결과 출력 - [ ] 게임 종료 후 우승자 안내 문구 출력 ### 테스트 diff --git a/src/RacingGame.js b/src/RacingGame.js index 110a297b..cf639bf1 100644 --- a/src/RacingGame.js +++ b/src/RacingGame.js @@ -1,5 +1,6 @@ import { Car } from "./Car.js"; import { getRandomNumber } from "./utils/randomUtils.js"; +import { printRoundResult } from "./output.js"; export class RacingGame { constructor(carNames) { @@ -12,7 +13,8 @@ export class RacingGame { if (car.canMove(randomValue)) { car.move(); } - }) + }); + printRoundResult(this.cars); } play(tryCount) { diff --git a/src/output.js b/src/output.js new file mode 100644 index 00000000..f8689c3e --- /dev/null +++ b/src/output.js @@ -0,0 +1,9 @@ +import { Console } from "@woowacourse/mission-utils"; + +export function printRoundResult(cars) { + cars.forEach((car) => { + const positionBar = "-".repeat(car.position); + Console.print(`${car.name} : ${positionBar}`); + }); + Console.print(""); +} \ No newline at end of file From 2c78b950bfdb14e2883c8c9916a68c96813cbba8 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:12:10 +0900 Subject: [PATCH 15/20] =?UTF-8?q?test(output):=20=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=EB=B3=84=20=EC=8B=A4=ED=96=89=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/output.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 __tests__/output.test.js diff --git a/__tests__/output.test.js b/__tests__/output.test.js new file mode 100644 index 00000000..939d12a7 --- /dev/null +++ b/__tests__/output.test.js @@ -0,0 +1,19 @@ +import { printRoundResult } from "../src/output.js"; +import { Console } from "@woowacourse/mission-utils"; + +describe("출력", () => { + test("자동차의 현재 위치를 '-'로 출력한다.", () => { + const spy = jest.spyOn(Console, "print").mockImplementation(() => {}); + const cars = [ + { name: "pobi", position: 2 }, + { name: "crong", position: 3 }, + ]; + + printRoundResult(cars); + + expect(spy).toHaveBeenCalledWith("pobi : --"); + expect(spy).toHaveBeenCalledWith("crong : ---"); + expect(spy).toHaveBeenCalledWith(""); + spy.mockRestore(); + }); +}); \ No newline at end of file From 8c38bc2f26936bdaa4e5121c2f9197f334f92bcd Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:21:24 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat(racinggame):=20=EC=9A=B0=EC=8A=B9?= =?UTF-8?q?=EC=9E=90=20=EC=84=A0=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/App.js | 2 ++ src/RacingGame.js | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4e1e1a4..4291f312 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - [x] 자동차 생성 - [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상인 경우 자동차 전진 - [x] 시도 횟수만큼 게임 진행 -- [ ] 우승자 선출 +- [x] 우승자 선출 ### 출력 - [x] 라운드별 실행 결과 출력 diff --git a/src/App.js b/src/App.js index 127d1986..d7372f21 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,8 @@ class App { const game = new RacingGame(carNames); game.play(tryCount); + + const winners = game.getWinners(); } } diff --git a/src/RacingGame.js b/src/RacingGame.js index cf639bf1..4e081f3a 100644 --- a/src/RacingGame.js +++ b/src/RacingGame.js @@ -22,4 +22,11 @@ export class RacingGame { 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); + } } \ No newline at end of file From 9ce69326154612fcb87ab8cba317e5cd7f1c5d06 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:38:19 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat(ouput):=20=EC=B5=9C=EC=A2=85=20?= =?UTF-8?q?=EC=9A=B0=EC=8A=B9=EC=9E=90=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/App.js | 2 ++ src/output.js | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4291f312..e4025cb2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ### 출력 - [x] 라운드별 실행 결과 출력 -- [ ] 게임 종료 후 우승자 안내 문구 출력 +- [x] 게임 종료 후 우승자 안내 문구 출력 ### 테스트 - [x] validator \ No newline at end of file diff --git a/src/App.js b/src/App.js index d7372f21..57c40bde 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,7 @@ 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() { @@ -15,6 +16,7 @@ class App { game.play(tryCount); const winners = game.getWinners(); + printWinners(winners); } } diff --git a/src/output.js b/src/output.js index f8689c3e..f087de28 100644 --- a/src/output.js +++ b/src/output.js @@ -6,4 +6,8 @@ export function printRoundResult(cars) { Console.print(`${car.name} : ${positionBar}`); }); Console.print(""); +} + +export function printWinners(winners) { + Console.print(`최종 우승자 : ${winners.join(", ")}`); } \ No newline at end of file From 61525563587f8a2e8c22b0a3efd37cb4db8ae845 Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:38:33 +0900 Subject: [PATCH 18/20] =?UTF-8?q?test(racinggame,=20output):=20=EC=9A=B0?= =?UTF-8?q?=EC=8A=B9=EC=9E=90=20=EC=84=A0=EC=B6=9C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/RacingGame.test.js | 37 ++++++++++++++++++++++++++++++- __tests__/output.test.js | 42 ++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/__tests__/RacingGame.test.js b/__tests__/RacingGame.test.js index c382a08a..ce34b0ba 100644 --- a/__tests__/RacingGame.test.js +++ b/__tests__/RacingGame.test.js @@ -1,5 +1,6 @@ import { RacingGame } from "../src/RacingGame.js"; -import * as randomUtils from "../src/utils/randomUtils.js"; // ← 모듈 전체를 가져와서 spyOn 가능하게 +import * as randomUtils from "../src/utils/randomUtils.js"; +import { Car } from "../src/Car.js"; describe("RacingGame 클래스", () => { afterEach(() => { @@ -29,4 +30,38 @@ describe("RacingGame 클래스", () => { 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"]); + }); + }); }); \ No newline at end of file diff --git a/__tests__/output.test.js b/__tests__/output.test.js index 939d12a7..6e606e5c 100644 --- a/__tests__/output.test.js +++ b/__tests__/output.test.js @@ -1,9 +1,16 @@ -import { printRoundResult } from "../src/output.js"; +import { printRoundResult, printWinners } from "../src/output.js"; import { Console } from "@woowacourse/mission-utils"; -describe("출력", () => { +describe("printRoundResult", () => { + beforeEach(() => { + jest.spyOn(Console, "print").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + test("자동차의 현재 위치를 '-'로 출력한다.", () => { - const spy = jest.spyOn(Console, "print").mockImplementation(() => {}); const cars = [ { name: "pobi", position: 2 }, { name: "crong", position: 3 }, @@ -11,9 +18,30 @@ describe("출력", () => { printRoundResult(cars); - expect(spy).toHaveBeenCalledWith("pobi : --"); - expect(spy).toHaveBeenCalledWith("crong : ---"); - expect(spy).toHaveBeenCalledWith(""); - spy.mockRestore(); + 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"); }); }); \ No newline at end of file From d6885b999a45c733c56d09ec8d565ec76a6c12db Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:50:36 +0900 Subject: [PATCH 19/20] =?UTF-8?q?feat(validator):=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=A4=91=EB=B3=B5=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + __tests__/validator.test.js | 6 ++++++ src/validator.js | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/README.md b/README.md index e4025cb2..175a83c5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ### 유효성 검사 - [x] 이름 5자 이하 - [x] 이름 공백 불가능 +- [x] 이름 중복 불가능 - [x] 시도 횟수는 0보다 큰 정수 ### 게임 진행 diff --git a/__tests__/validator.test.js b/__tests__/validator.test.js index f79adfb7..f5e5d7f0 100644 --- a/__tests__/validator.test.js +++ b/__tests__/validator.test.js @@ -14,6 +14,12 @@ describe("자동차 이름 유효성 검사", () => { ); }); + test("이름이 중복되면 에러를 발생시킨다", () => { + expect(() => validateCarNames(["pobi", "woni", "pobi"])).toThrow( + "[ERROR] 자동차 이름은 중복될 수 없습니다." + ); + }); + test("모든 이름이 조건을 만족하면 통과한다", () => { expect(() => validateCarNames(["pobi", "jun", "woni"])).not.toThrow(); }); diff --git a/src/validator.js b/src/validator.js index 0efaf3f8..95ab3c30 100644 --- a/src/validator.js +++ b/src/validator.js @@ -8,6 +8,11 @@ export function validateCarNames(names) { if (names.some((name) => name.trim().length > MAX_CAR_NAME_LENGTH)) { throw new Error(`[ERROR] 자동차 이름은 ${MAX_CAR_NAME_LENGTH}자 이하만 가능합니다.`); } + + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error("[ERROR] 자동차 이름은 중복될 수 없습니다."); + } } export function validateTryCount(count) { From c069b29f4f519a73f30b908aa933e96579f2febd Mon Sep 17 00:00:00 2001 From: nunomi0 Date: Mon, 27 Oct 2025 22:53:13 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs(readme):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=AA=A9=EB=A1=9D,=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0,=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 175a83c5..f88ff3f1 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,43 @@ - [x] 게임 종료 후 우승자 안내 문구 출력 ### 테스트 -- [x] validator \ No newline at end of file +- [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 +``` \ No newline at end of file