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/__tests__/CarsTest.js b/__tests__/CarsTest.js new file mode 100644 index 00000000..8a21f13b --- /dev/null +++ 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/__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/__tests__/InputValidatorTest.js b/__tests__/InputValidatorTest.js new file mode 100644 index 00000000..e56f7c41 --- /dev/null +++ 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/__tests__/RacingGameTest.js b/__tests__/RacingGameTest.js new file mode 100644 index 00000000..2d59488b --- /dev/null +++ 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/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 diff --git a/src/controller/GameController.js b/src/controller/GameController.js new file mode 100644 index 00000000..012e7f0f --- /dev/null +++ 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 diff --git a/src/domain/Car.js b/src/domain/Car.js new file mode 100644 index 00000000..0714eebe --- /dev/null +++ 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 diff --git a/src/domain/Cars.js b/src/domain/Cars.js new file mode 100644 index 00000000..4e0d6ebc --- /dev/null +++ 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 diff --git a/src/domain/RacingGame.js b/src/domain/RacingGame.js new file mode 100644 index 00000000..baea817e --- /dev/null +++ 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 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 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 diff --git a/src/view/InputView.js b/src/view/InputView.js new file mode 100644 index 00000000..2850c0bc --- /dev/null +++ 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 diff --git a/src/view/OutputView.js b/src/view/OutputView.js new file mode 100644 index 00000000..44356226 --- /dev/null +++ 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