Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# javascript-racingcar-precourse
# 자동차 경주

## 과제 진행 요구 사항
미션은 자동차 경주 저장소를 포크하고 클론하는 것으로 시작한다.
기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.
자세한 과제 진행 방법은 프리코스 진행 가이드 문서를 참고한다.

# 기능 요구 사항

- [x] 자동차 이름, 횟수 입력 받기
- [x] ',' 기준으로 이름 입력 (5자 이하)
- [x] 자연수 숫자 입력받기
- [x] 잘못된 값 입력시 에러처리
- [x] 유효한 숫자 아닌 경우 에러처리
- [x] 자동차 경주
- [x] 전진 기능 구현
- [x] 결과 출력 기능 구현
- [x] 최종 우승자 출력
- [x] 공동 우승 처리
- [x] ERROR 출력
60 changes: 60 additions & 0 deletions __tests__/Test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import App from "../src/App.js";
import { MissionUtils } from "@woowacourse/mission-utils";

const mockQuestions = (inputs) => {
MissionUtils.Console.readLineAsync = jest.fn();

MissionUtils.Console.readLineAsync.mockImplementation(() => {
const input = inputs.shift();
return Promise.resolve(input);
});
};

const mockRandoms = (numbers) => {
MissionUtils.Random.pickNumberInRange = jest.fn();

numbers.reduce((acc, number) => {
return acc.mockReturnValueOnce(number);
}, MissionUtils.Random.pickNumberInRange);
};

const getLogSpy = () => {
const logSpy = jest.spyOn(MissionUtils.Console, "print");
logSpy.mockClear();
return logSpy;
};

describe("자동차 경주", () => {
test("기능 테스트", async () => {
// given
const MOVING_FORWARD = 4;
const STOP = 3;
const inputs = ["dao,bazzi", "1"];
const logs = ["dao : -", "bazzi : ", "최종 우승자 : dao"];
const logSpy = getLogSpy();

mockQuestions(inputs);
mockRandoms([MOVING_FORWARD, STOP]);

// when
const app = new App();
await app.run();

// then
logs.forEach((log) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(log));
});
});

test("예외 테스트", async () => {
// given
const inputs = ["bazzi,dao,,json"];
mockQuestions(inputs);

// when
const app = new App();

// then
await expect(app.run()).rejects.toThrow("[ERROR]");
});
});
33 changes: 32 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import View from "./View.js";
import Car from "./Car.js";
import Race from "./Race.js";
import OutView from "./OutView.js";

class App {
async run() {}
#input = new View();
#race = new Race();
#output = new OutView();

async run() {
const carNames = await this.#input.readInputCar();
const raceCount = await this.#input.readInputRaceCount();

this.#race.setRaceCount(raceCount);

// Race 객체에 Car객체 추가
carNames.forEach(carName => {
this.#race.addRacingCar(new Car(carName));
});

for (let count = 0; count < this.#race.getRaceCount(); count++) {
this.#race.racing();
this.#race.showRacingResult();

}

this.#race.determineWinner();
this.#race.showWinner();
const winners = this.#race.getWinner();

this.#output.showWinner(winners);
}
}

export default App;
24 changes: 24 additions & 0 deletions src/Car.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Car {
#forwardCount=0;
#name;

constructor(name) {
this.#name = name;
}

forward(random) {
if (random >= 4) {
this.#forwardCount += 1;
}
}

getName() {
return this.#name;
}

getForwardCount() {
return this.#forwardCount;
}
}

export default Car;
12 changes: 12 additions & 0 deletions src/OutView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Console } from "@woowacourse/mission-utils";

class OutView {

showWinner(winners) {
Console.print(`최종 우승자 : ${winners.join(',')}`);
}


}

export default OutView
53 changes: 53 additions & 0 deletions src/Race.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Random, Console } from "@woowacourse/mission-utils";

class Race {
#cars = [];
#winners = [];
#raceCount;


addRacingCar(car) {
this.#cars.push(car);
}

setRaceCount(raceCount) {
this.#raceCount = raceCount;
}

getRaceCount() {
return this.#raceCount;
}

racing() {
Copy link

Choose a reason for hiding this comment

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

메서드나 함수의 이름은 동사로 시작하도록 짓는 것이 가독성이 좋을 것 같습니다!

https://velog.io/@pjc0247/%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%98%81%EC%96%B4

this.#cars.forEach(car => {
const random = Random.pickNumberInRange(0, 9);
// 4 이상일 경우 전진
car.forward(random);
})
}

showRacingResult() {
// name : --- 형식으로 출력
this.#cars.forEach( car => {
Console.print(car.getName() + ' : ' + '-'.repeat(car.getForwardCount()));
Copy link

Choose a reason for hiding this comment

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

Airbnb JS 스타일 가이드에서는 문자열을 연결할 때 template string 사용을 권장하는 것 같습니다!

https://github.com/airbnb/javascript?tab=readme-ov-file#es6-template-literals

추가적으로 사용자에게 입력을 받는 역할을 View 클래스에 위임하신 것처럼 결과를 출력하는 역할도 View 클래스에 위임하면 좋을 것 같아요!

현재는 결과 문자열을 만들어서 출력해주다보니 UI와 결합성이 강해 추후 UI가 변경되면 Race 클래스까지 변경해야 될 수도 있을 것 같습니다!

})
Console.print('');
}

determineWinner() {
const max = Math.max(...this.#cars.map(car => car.getForwardCount()));

this.#cars.forEach(car => {
if (car.getForwardCount() === max){
this.#winners.push(car.getName());
}
})
}

getWinner() {
return this.#winners;
}

}

export default Race;
31 changes: 31 additions & 0 deletions src/View.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Console } from "@woowacourse/mission-utils";

class View {
Copy link

Choose a reason for hiding this comment

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

View 클래스로 UI 로직을 분리한게 좋네요👍

#inputCarNameRegex = /^([a-zA-z]{1,5})(,[a-zA-Z]{1,5})*$/

#carInputMessage = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n";
#raceCount = "시도할 횟수는 몇 회인가요?\n";

async readInputCar() {
const carNamesStr = await Console.readLineAsync(this.#carInputMessage);

if (!this.#inputCarNameRegex.test(carNamesStr)){
throw new Error("[ERROR] : 잘못된 이름 입력\n");
}

const carNames = carNamesStr.split(',');
return carNames;
}

async readInputRaceCount() {
const raceCountStr = await Console.readLineAsync(this.#raceCount);
const raceCount = Number(raceCountStr);
if (raceCount.isNaN() || raceCount <= 0){
throw new Error("[ERROR] : 잘못된 숫자 입력\n");
}

return raceCount;
}
}

export default View;