Skip to content
183 changes: 182 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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/)
66 changes: 66 additions & 0 deletions __tests__/CarsTest.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Comment on lines +33 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 3개의 테스트의 경우에는 test.each로 한 번에 처리가 가능할 것 같습니다!
우테코에서 알려준 블로그

46 changes: 46 additions & 0 deletions __tests__/Cartest.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
85 changes: 85 additions & 0 deletions __tests__/InputValidatorTest.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
Loading