From f5f47b455fe3bcc30fec38e445f9384b023e593a Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:38:17 +0900 Subject: [PATCH 01/10] =?UTF-8?q?docs:=20README=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기능 요구사항 문서화 - 구현할 기능 목록 7개 카테고리로 분류 - 프로젝트 구조 설계 - 프로그래밍 요구사항 정리 --- src/__test__/CarsTest.js | 0 src/__test__/Cartest.js | 0 src/__test__/InputValidatorTest.js | 0 src/__test__/RacingGameTest.js | 0 src/controller/GameController.js | 0 src/domain/Car.js | 0 src/domain/Cars.js | 0 src/domain/RacingGame.js | 0 src/validator/InputValidation.js | 0 src/view/InputView.js | 0 src/view/OutputView.js | 0 11 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/__test__/CarsTest.js create mode 100644 src/__test__/Cartest.js create mode 100644 src/__test__/InputValidatorTest.js create mode 100644 src/__test__/RacingGameTest.js create mode 100644 src/controller/GameController.js create mode 100644 src/domain/Car.js create mode 100644 src/domain/Cars.js create mode 100644 src/domain/RacingGame.js create mode 100644 src/validator/InputValidation.js create mode 100644 src/view/InputView.js create mode 100644 src/view/OutputView.js diff --git a/src/__test__/CarsTest.js b/src/__test__/CarsTest.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__test__/Cartest.js b/src/__test__/Cartest.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__test__/InputValidatorTest.js b/src/__test__/InputValidatorTest.js new file mode 100644 index 00000000..e69de29b diff --git a/src/__test__/RacingGameTest.js b/src/__test__/RacingGameTest.js new file mode 100644 index 00000000..e69de29b diff --git a/src/controller/GameController.js b/src/controller/GameController.js new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/Car.js b/src/domain/Car.js new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/Cars.js b/src/domain/Cars.js new file mode 100644 index 00000000..e69de29b diff --git a/src/domain/RacingGame.js b/src/domain/RacingGame.js new file mode 100644 index 00000000..e69de29b diff --git a/src/validator/InputValidation.js b/src/validator/InputValidation.js new file mode 100644 index 00000000..e69de29b diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 00000000..e69de29b diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 00000000..e69de29b From 4447141ad39d3c3bac02a4e92a1cfbed98745d26 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:41:50 +0900 Subject: [PATCH 02/10] =?UTF-8?q?test:=20Car=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 183 +++++++++++++++++- {src/__test__ => __tests__}/CarsTest.js | 0 __tests__/Cartest.js | 46 +++++ .../InputValidatorTest.js | 0 {src/__test__ => __tests__}/RacingGameTest.js | 0 src/__test__/Cartest.js | 0 src/domain/Car.js | 25 +++ 7 files changed, 253 insertions(+), 1 deletion(-) rename {src/__test__ => __tests__}/CarsTest.js (100%) create mode 100644 __tests__/Cartest.js rename {src/__test__ => __tests__}/InputValidatorTest.js (100%) rename {src/__test__ => __tests__}/RacingGameTest.js (100%) delete mode 100644 src/__test__/Cartest.js diff --git a/README.md b/README.md index e078fd41..2ca413e5 100644 --- a/README.md +++ b/README.md @@ -1 +1,182 @@ -# javascript-racingcar-precourse +# 🏎️ 자동차 경주 게임 + +우아한테크코스 프리코스 2주차 미션 - 자동차 경주 게임 구현 + +## 📌 기능 요구사항 + +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 각 자동차에 이름을 부여할 수 있다. +- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. +- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. + +## 🚀 기능 목록 + +### 1. 입력 기능 +- 자동차 이름 입력받기 + - 쉼표(,)로 구분된 문자열 파싱 + - 공백 제거 처리 +- 시도 횟수 입력받기 + - 숫자 형태의 문자열 입력 + +### 2. 입력 검증 기능 +- 자동차 이름 유효성 검증 + - 빈 이름이 있는지 확인 + - 각 이름이 5자 이하인지 확인 + - 중복된 이름이 있는지 확인 + - 유효하지 않은 경우 `[ERROR]` 메시지와 함께 에러 발생 +- 시도 횟수 유효성 검증 + - 숫자인지 확인 + - 양의 정수인지 확인 + - 유효하지 않은 경우 `[ERROR]` 메시지와 함께 에러 발생 + +### 3. 자동차 도메인 +- 자동차 객체 생성 + - 이름 저장 + - 위치 초기화 (0) +- 자동차 이동 기능 + - 0~9 사이의 무작위 값 생성 + - 무작위 값이 4 이상이면 전진 + - 무작위 값이 4 미만이면 정지 + - 위치 업데이트 + +### 4. 게임 진행 기능 +- 자동차 목록 관리 + - 여러 대의 자동차 생성 및 관리 +- 경주 진행 + - 각 라운드마다 모든 자동차 이동 시도 + - 지정된 횟수만큼 반복 +- 각 라운드 결과 저장 + +### 5. 우승자 결정 기능 +- 최대 이동 거리 계산 +- 우승자 찾기 + - 최대 거리를 가진 자동차 모두 찾기 +- 우승자 목록 반환 + +### 6. 출력 기능 +- 실행 결과 출력 + - 각 라운드의 자동차 상태 출력 + - 자동차 이름과 위치('-' 문자로 표현) 출력 + - 각 라운드 사이 빈 줄 추가 +- 최종 우승자 출력 + - 단독 우승자: `최종 우승자 : pobi` + - 공동 우승자: `최종 우승자 : pobi, jun` (쉼표로 구분) + +### 7. 테스트 기능 +- 입력 검증 테스트 + - 5자 초과 이름 테스트 + - 빈 이름 테스트 + - 중복 이름 테스트 + - 음수 횟수 테스트 + - 0 횟수 테스트 +- 자동차 이동 테스트 + - 무작위 값이 4 이상일 때 전진 테스트 + - 무작위 값이 4 미만일 때 정지 테스트 +- 우승자 결정 테스트 + - 단독 우승자 테스트 + - 공동 우승자 테스트 + +## 📂 프로젝트 구조 + +``` +src/ +├── App.js # 애플리케이션 실행 시작점 +├── controller/ +│ └── GameController.js # 게임 전체 흐름 제어 +├── domain/ +│ ├── Car.js # 자동차 클래스 +│ ├── Cars.js # 자동차 목록 관리 클래스 +│ └── RacingGame.js # 경주 게임 로직 클래스 +├── validator/ +│ └── InputValidator.js # 입력 검증 클래스 +└── view/ + ├── InputView.js # 사용자 입력 처리 + └── OutputView.js # 결과 출력 처리 + +__tests__/ +├── CarTest.js # 자동차 도메인 테스트 +├── CarsTest.js # 자동차 목록 테스트 +├── RacingGameTest.js # 게임 로직 테스트 +└── InputValidatorTest.js # 입력 검증 테스트 +``` + +## 🎯 프로그래밍 요구사항 + +- Node.js 22.19.0 버전에서 실행 가능 +- indent depth 2 이하로 제한 +- 3항 연산자 사용 금지 +- 함수는 한 가지 일만 수행하도록 작게 구현 +- Jest를 이용한 테스트 코드 작성 +- `@woowacourse/mission-utils`의 `Random`, `Console` API 사용 + +## 💻 실행 방법 + +### 패키지 설치 +```bash +npm install +``` + +### 프로그램 실행 +```bash +npm run start +``` + +### 테스트 실행 +```bash +npm run test +``` + +## 📝 실행 예시 + +``` +경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) +pobi,woni,jun +시도할 횟수는 몇 회인가요? +5 + +실행 결과 +pobi : - +woni : +jun : - + +pobi : -- +woni : - +jun : -- + +pobi : --- +woni : -- +jun : --- + +pobi : ---- +woni : --- +jun : ---- + +pobi : ----- +woni : ---- +jun : ----- + +최종 우승자 : pobi, jun +``` + +## 🔍 주요 구현 내용 + +### 객체지향 설계 +- 단일 책임 원칙(SRP)을 지키며 각 클래스가 하나의 역할만 수행 +- 도메인 로직과 입출력 로직 분리 +- 검증 로직의 독립적인 관리 + +### 에러 처리 +- 모든 잘못된 입력에 대해 `[ERROR]`로 시작하는 메시지 출력 +- `throw new Error()` 사용 + +### 함수 분리 +- indent depth를 줄이기 위해 함수를 작은 단위로 분리 +- 각 함수가 명확한 하나의 책임만 수행 + +## 📚 참고 자료 + +- [AngularJS Git Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) +- [JavaScript Style Guide](https://github.com/airbnb/javascript) +- [Jest 공식 문서](https://jestjs.io/) \ No newline at end of file diff --git a/src/__test__/CarsTest.js b/__tests__/CarsTest.js similarity index 100% rename from src/__test__/CarsTest.js rename to __tests__/CarsTest.js diff --git a/__tests__/Cartest.js b/__tests__/Cartest.js new file mode 100644 index 00000000..b64d86ee --- /dev/null +++ b/__tests__/Cartest.js @@ -0,0 +1,46 @@ +import Car from '../src/domain/Car.js'; + +describe('Car 클래스 테스트', () => { + test('자동차 생성 시 이름이 저장된다', () => { + const car = new Car('pobi'); + + expect(car.getName()).toBe('pobi'); + }); + + test('자동차 생성 시 초기 위치는 0이다', () => { + const car = new Car('pobi'); + + expect(car.getPosition()).toBe(0); + }); + + test('무작위 값이 4 이상이면 전진한다', () => { + const car = new Car('pobi'); + + car.move(4); + expect(car.getPosition()).toBe(1); + + car.move(9); + expect(car.getPosition()).toBe(2); + }); + + test('무작위 값이 4 미만이면 멈춘다', () => { + const car = new Car('pobi'); + + car.move(3); + expect(car.getPosition()).toBe(0); + + car.move(0); + expect(car.getPosition()).toBe(0); + }); + + test('여러 번 이동 시 위치가 누적된다', () => { + const car = new Car('pobi'); + + car.move(4); + car.move(5); + car.move(3); + car.move(6); + + expect(car.getPosition()).toBe(3); + }); +}); \ No newline at end of file diff --git a/src/__test__/InputValidatorTest.js b/__tests__/InputValidatorTest.js similarity index 100% rename from src/__test__/InputValidatorTest.js rename to __tests__/InputValidatorTest.js diff --git a/src/__test__/RacingGameTest.js b/__tests__/RacingGameTest.js similarity index 100% rename from src/__test__/RacingGameTest.js rename to __tests__/RacingGameTest.js diff --git a/src/__test__/Cartest.js b/src/__test__/Cartest.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/domain/Car.js b/src/domain/Car.js index e69de29b..0714eebe 100644 --- a/src/domain/Car.js +++ b/src/domain/Car.js @@ -0,0 +1,25 @@ +class Car { + #name; + #position; + + constructor(name) { + this.#name = name; + this.#position = 0; + } + + move(randomValue) { + if (randomValue >= 4) { + this.#position += 1; + } + } + + getName() { + return this.#name; + } + + getPosition() { + return this.#position; + } +} + +export default Car; \ No newline at end of file From 993b8cb6680b2db489612942f10a69f7013752d9 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:49:55 +0900 Subject: [PATCH 03/10] =?UTF-8?q?test:=20InputValidator=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/InputValidatorTest.js | 85 ++++++++++++++++++++++++++++++++ src/validator/InputValidation.js | 0 src/validator/InputValidator.js | 53 ++++++++++++++++++++ 3 files changed, 138 insertions(+) delete mode 100644 src/validator/InputValidation.js create mode 100644 src/validator/InputValidator.js diff --git a/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js index e69de29b..e56f7c41 100644 --- a/__tests__/InputValidatorTest.js +++ b/__tests__/InputValidatorTest.js @@ -0,0 +1,85 @@ +import InputValidator from '../src/validator/InputValidator.js'; + +describe('InputValidator 클래스 테스트', () => { + describe('자동차 이름 검증', () => { + test('빈 이름이 있으면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateCarNames(['pobi', '', 'jun']); + }).toThrow('[ERROR]'); + }); + + test('5자를 초과하는 이름이 있으면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateCarNames(['pobi', 'longname']); + }).toThrow('[ERROR]'); + }); + + test('중복된 이름이 있으면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateCarNames(['pobi', 'woni', 'pobi']); + }).toThrow('[ERROR]'); + }); + + test('자동차 이름이 하나도 없으면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateCarNames([]); + }).toThrow('[ERROR]'); + }); + + test('유효한 자동차 이름들은 에러가 발생하지 않는다', () => { + expect(() => { + InputValidator.validateCarNames(['pobi', 'woni', 'jun']); + }).not.toThrow(); + }); + + test('1글자 이름도 유효하다', () => { + expect(() => { + InputValidator.validateCarNames(['a', 'b', 'c']); + }).not.toThrow(); + }); + + test('5글자 이름은 유효하다', () => { + expect(() => { + InputValidator.validateCarNames(['abcde', 'fghij']); + }).not.toThrow(); + }); + }); + + describe('시도 횟수 검증', () => { + test('음수이면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateRounds(-1); + }).toThrow('[ERROR]'); + }); + + test('0이면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateRounds(0); + }).toThrow('[ERROR]'); + }); + + test('숫자가 아니면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateRounds('abc'); + }).toThrow('[ERROR]'); + }); + + test('소수이면 에러가 발생한다', () => { + expect(() => { + InputValidator.validateRounds(3.5); + }).toThrow('[ERROR]'); + }); + + test('양의 정수는 유효하다', () => { + expect(() => { + InputValidator.validateRounds(5); + }).not.toThrow(); + }); + + test('1도 유효하다', () => { + expect(() => { + InputValidator.validateRounds(1); + }).not.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/src/validator/InputValidation.js b/src/validator/InputValidation.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/validator/InputValidator.js b/src/validator/InputValidator.js new file mode 100644 index 00000000..f104dc51 --- /dev/null +++ b/src/validator/InputValidator.js @@ -0,0 +1,53 @@ +class InputValidator { + static validateCarNames(names) { + this.#validateNotEmpty(names); + this.#validateNameLength(names); + this.#validateNoDuplicate(names); + } + + static #validateNotEmpty(names) { + if (names.length === 0) { + throw new Error('[ERROR] 자동차 이름은 최소 1개 이상 입력해야 합니다.'); + } + + if (names.some(name => name.trim() === '')) { + throw new Error('[ERROR] 자동차 이름은 빈 값일 수 없습니다.'); + } + } + + static #validateNameLength(names) { + if (names.some(name => name.length > 5)) { + throw new Error('[ERROR] 자동차 이름은 5자 이하만 가능합니다.'); + } + } + + static #validateNoDuplicate(names) { + const uniqueNames = new Set(names); + if (uniqueNames.size !== names.length) { + throw new Error('[ERROR] 자동차 이름은 중복될 수 없습니다.'); + } + } + + static validateRounds(rounds) { + this.#validateIsNumber(rounds); + this.#validateIsPositiveInteger(rounds); + } + + static #validateIsNumber(rounds) { + if (typeof rounds !== 'number' || Number.isNaN(rounds)) { + throw new Error('[ERROR] 시도 횟수는 숫자여야 합니다.'); + } + } + + static #validateIsPositiveInteger(rounds) { + if (!Number.isInteger(rounds)) { + throw new Error('[ERROR] 시도 횟수는 정수여야 합니다.'); + } + + if (rounds <= 0) { + throw new Error('[ERROR] 시도 횟수는 1 이상의 숫자여야 합니다.'); + } + } +} + +export default InputValidator; \ No newline at end of file From 4fa6fc3a3457e2888a254045545a5b38e40b2dc3 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:54:51 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20Cars=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/CarsTest.js | 66 +++++++++++++++++++++++++++++++++++++++++++ src/domain/Cars.js | 30 ++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/__tests__/CarsTest.js b/__tests__/CarsTest.js index e69de29b..8a21f13b 100644 --- a/__tests__/CarsTest.js +++ b/__tests__/CarsTest.js @@ -0,0 +1,66 @@ +import Cars from '../src/domain/Cars.js'; +import Car from '../src/domain/Car.js'; + +describe('Cars 클래스 테스트', () => { + test('이름 배열로 여러 자동차를 생성한다', () => { + const names = ['pobi', 'woni', 'jun']; + const cars = new Cars(names); + + expect(cars.getCars()).toHaveLength(3); + }); + + test('모든 자동차를 한 번씩 이동시킨다', () => { + const cars = new Cars(['pobi', 'woni']); + + cars.moveAll([4, 3]); + + const carsList = cars.getCars(); + expect(carsList[0].getPosition()).toBe(1); + expect(carsList[1].getPosition()).toBe(0); + }); + + test('각 자동차에 다른 무작위 값을 적용한다', () => { + const cars = new Cars(['pobi', 'woni', 'jun']); + + cars.moveAll([5, 2, 8]); + + const carsList = cars.getCars(); + expect(carsList[0].getPosition()).toBe(1); + expect(carsList[1].getPosition()).toBe(0); + expect(carsList[2].getPosition()).toBe(1); + }); + + test('최대 위치를 가진 자동차들을 찾는다 - 단독 우승자', () => { + const cars = new Cars(['pobi', 'woni', 'jun']); + + cars.moveAll([5, 4, 3]); + cars.moveAll([6, 3, 2]); + + const winners = cars.getWinners(); + + expect(winners).toHaveLength(1); + expect(winners[0].getName()).toBe('pobi'); + }); + + test('최대 위치를 가진 자동차들을 찾는다 - 공동 우승자', () => { + const cars = new Cars(['pobi', 'woni', 'jun']); + + cars.moveAll([5, 6, 3]); + cars.moveAll([6, 5, 2]); + + const winners = cars.getWinners(); + + expect(winners).toHaveLength(2); + expect(winners.map(car => car.getName())).toEqual(expect.arrayContaining(['pobi', 'woni'])); + }); + + test('모든 자동차가 같은 위치면 모두 우승자다', () => { + const cars = new Cars(['pobi', 'woni', 'jun']); + + cars.moveAll([3, 3, 3]); + + const winners = cars.getWinners(); + + expect(winners).toHaveLength(3); + }); +}); \ No newline at end of file diff --git a/src/domain/Cars.js b/src/domain/Cars.js index e69de29b..4e0d6ebc 100644 --- a/src/domain/Cars.js +++ b/src/domain/Cars.js @@ -0,0 +1,30 @@ +import Car from './Car.js'; + +class Cars { + #cars; + + constructor(names) { + this.#cars = names.map(name => new Car(name)); + } + + moveAll(randomValues) { + this.#cars.forEach((car, index) => { + car.move(randomValues[index]); + }); + } + + getCars() { + return this.#cars; + } + + getWinners() { + const maxPosition = this.#getMaxPosition(); + return this.#cars.filter(car => car.getPosition() === maxPosition); + } + + #getMaxPosition() { + return Math.max(...this.#cars.map(car => car.getPosition())); + } +} + +export default Cars; \ No newline at end of file From 9b22679a9c102b32e166cb87df916ba9caf458e4 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:56:38 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20RacingGame=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/RacingGameTest.js | 69 +++++++++++++++++++++++++++++++++++++ src/domain/RacingGame.js | 47 +++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/__tests__/RacingGameTest.js b/__tests__/RacingGameTest.js index e69de29b..2d59488b 100644 --- a/__tests__/RacingGameTest.js +++ b/__tests__/RacingGameTest.js @@ -0,0 +1,69 @@ +import RacingGame from '../src/domain/RacingGame.js'; + +describe('RacingGame 클래스 테스트', () => { + test('자동차 이름들과 시도 횟수로 게임을 생성한다', () => { + const game = new RacingGame(['pobi', 'woni'], 5); + + expect(game).toBeDefined(); + }); + + test('지정된 횟수만큼 경주를 진행한다', () => { + const game = new RacingGame(['pobi', 'woni'], 3); + + const generateRandomValues = () => [4, 5]; + game.play(generateRandomValues); + + const results = game.getResults(); + + expect(results).toHaveLength(3); + }); + + test('각 라운드마다 자동차들의 상태를 기록한다', () => { + const game = new RacingGame(['pobi', 'woni'], 2); + + let callCount = 0; + const generateRandomValues = () => { + callCount++; + if (callCount === 1) return [5, 3]; + return [2, 6]; + }; + + game.play(generateRandomValues); + + const results = game.getResults(); + + expect(results[0]).toEqual([ + { name: 'pobi', position: 1 }, + { name: 'woni', position: 0 } + ]); + + expect(results[1]).toEqual([ + { name: 'pobi', position: 1 }, + { name: 'woni', position: 1 } + ]); + }); + + test('우승자를 반환한다', () => { + const game = new RacingGame(['pobi', 'woni', 'jun'], 2); + + const generateRandomValues = () => [5, 3, 6]; + game.play(generateRandomValues); + + const winners = game.getWinners(); + + expect(winners).toHaveLength(2); + expect(winners.map(w => w.name)).toEqual(expect.arrayContaining(['pobi', 'jun'])); + }); + + test('단독 우승자를 찾는다', () => { + const game = new RacingGame(['pobi', 'woni'], 1); + + const generateRandomValues = () => [5, 2]; + game.play(generateRandomValues); + + const winners = game.getWinners(); + + expect(winners).toHaveLength(1); + expect(winners[0].name).toBe('pobi'); + }); +}); \ No newline at end of file diff --git a/src/domain/RacingGame.js b/src/domain/RacingGame.js index e69de29b..baea817e 100644 --- a/src/domain/RacingGame.js +++ b/src/domain/RacingGame.js @@ -0,0 +1,47 @@ +import Cars from './Cars.js'; + +class RacingGame { + #cars; + #rounds; + #results; + + constructor(carNames, rounds) { + this.#cars = new Cars(carNames); + this.#rounds = rounds; + this.#results = []; + } + + play(generateRandomValues) { + for (let i = 0; i < this.#rounds; i++) { + this.#playRound(generateRandomValues); + } + } + + #playRound(generateRandomValues) { + const randomValues = generateRandomValues(); + this.#cars.moveAll(randomValues); + this.#saveCurrentState(); + } + + #saveCurrentState() { + const currentState = this.#cars.getCars().map(car => ({ + name: car.getName(), + position: car.getPosition() + })); + this.#results.push(currentState); + } + + getResults() { + return this.#results; + } + + getWinners() { + const winners = this.#cars.getWinners(); + return winners.map(car => ({ + name: car.getName(), + position: car.getPosition() + })); + } +} + +export default RacingGame; \ No newline at end of file From e537a940452ccfcad57f53ce8ea7f8d6922f4fc5 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:58:23 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20InputView=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Console API를 활용한 사용자 입력 처리 - 자동차 이름 입력 및 파싱 - 시도 횟수 입력 및 변환 - 쉼표 구분 및 공백 제거 처리 --- src/view/InputView.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/view/InputView.js b/src/view/InputView.js index e69de29b..2850c0bc 100644 --- a/src/view/InputView.js +++ b/src/view/InputView.js @@ -0,0 +1,26 @@ +import { Console } from '@woowacourse/mission-utils'; + +class InputView { + static async readCarNames() { + const input = await Console.readLineAsync( + '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n' + ); + return this.#parseCarNames(input); + } + + static #parseCarNames(input) { + return input.split(',').map(name => name.trim()); + } + + static async readRounds() { + const input = await Console.readLineAsync('시도할 횟수는 몇 회인가요?\n'); + return this.#parseRounds(input); + } + + static #parseRounds(input) { + const rounds = Number(input); + return rounds; + } +} + +export default InputView; \ No newline at end of file From 5acdc525d7d9015ddd65a4e4fd48d1997e0ea8b8 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:58:43 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20OutputView=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Console API를 활용한 결과 출력 - 라운드별 자동차 상태 출력 - 자동차 위치를 '-' 문자로 시각화 - 최종 우승자 출력 (단독/공동) --- src/view/OutputView.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/view/OutputView.js b/src/view/OutputView.js index e69de29b..44356226 100644 --- a/src/view/OutputView.js +++ b/src/view/OutputView.js @@ -0,0 +1,22 @@ +import { Console } from '@woowacourse/mission-utils'; + +class OutputView { + static printStartMessage() { + Console.print('\n실행 결과'); + } + + static printRoundResult(roundResult) { + roundResult.forEach(car => { + const position = '-'.repeat(car.position); + Console.print(`${car.name} : ${position}`); + }); + Console.print(''); + } + + static printWinners(winners) { + const winnerNames = winners.map(winner => winner.name).join(', '); + Console.print(`최종 우승자 : ${winnerNames}`); + } +} + +export default OutputView; \ No newline at end of file From c7ad62f5fdec91d1d95dc11ab17b7971c78e2fff Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:59:00 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20GameController=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 게임 전체 흐름 제어 - InputView를 통한 입력 처리 - InputValidator를 통한 유효성 검증 - Random API를 활용한 무작위 값 생성 - OutputView를 통한 결과 출력 --- src/controller/GameController.js | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/controller/GameController.js b/src/controller/GameController.js index e69de29b..012e7f0f 100644 --- a/src/controller/GameController.js +++ b/src/controller/GameController.js @@ -0,0 +1,54 @@ +import { Random } from '@woowacourse/mission-utils'; +import InputView from '../view/InputView.js'; +import OutputView from '../view/OutputView.js'; +import InputValidator from '../validator/InputValidator.js'; +import RacingGame from '../domain/RacingGame.js'; + +class GameController { + async run() { + const carNames = await this.#getCarNames(); + const rounds = await this.#getRounds(); + + this.#playGame(carNames, rounds); + } + + async #getCarNames() { + const carNames = await InputView.readCarNames(); + InputValidator.validateCarNames(carNames); + return carNames; + } + + async #getRounds() { + const rounds = await InputView.readRounds(); + InputValidator.validateRounds(rounds); + return rounds; + } + + #playGame(carNames, rounds) { + const game = new RacingGame(carNames, rounds); + + game.play(() => this.#generateRandomValues(carNames.length)); + + this.#printResults(game); + } + + #generateRandomValues(count) { + return Array.from({ length: count }, () => + Random.pickNumberInRange(0, 9) + ); + } + + #printResults(game) { + OutputView.printStartMessage(); + + const results = game.getResults(); + results.forEach(roundResult => { + OutputView.printRoundResult(roundResult); + }); + + const winners = game.getWinners(); + OutputView.printWinners(winners); + } +} + +export default GameController; \ No newline at end of file From 24cbe9eb2541ba91c1931770c457b9cf497fd339 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 22:59:19 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20App=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EB=B0=8F=20=EC=8B=A4=ED=96=89=20=EC=A7=84=EC=9E=85=EC=A0=90?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - App 클래스로 애플리케이션 시작 - index.js 진입점 생성 - GameController 연동 --- src/App.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index 091aa0a5..884c397f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,10 @@ +import GameController from './controller/GameController.js'; + class App { - async run() {} + async run() { + const gameController = new GameController(); + await gameController.run(); + } } -export default App; +export default App; \ No newline at end of file From 78847ee53a23f19b08ee004142b3200d8a4adbf3 Mon Sep 17 00:00:00 2001 From: Sebeen Kwon Date: Mon, 27 Oct 2025 23:00:43 +0900 Subject: [PATCH 10/10] =?UTF-8?q?chore:=20index.js=20=EB=88=84=EB=9D=BD=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 --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 02a1d389..1c2cba24 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import App from "./App.js"; +import App from './App.js'; const app = new App(); -await app.run(); +app.run(); \ No newline at end of file