Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[자동차 경주] 김도이 미션 제출합니다. #443

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
# javascript-racingcar-precourse

# 기능 목록

## 입력

- [x] 게임에 참여할 두 개 이상의 자동차이름들을 쉼표(,)를 기준으로 구분해 입력받는다.
- [x] 잘못된 값 입력시, "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션 종료한다. 잘못된 값은 아래 경우에 해당한다.
- [x] 자동차이름이 6자 초과인 경우
- [x] 구분자가 없는 경우
= 쉼표가 아닌 구분자가 입력된 경우
= 자동차이름을 1개만 입력한 경우
- [x] 숫자로 시작하는 경우(eg. "123a")
- [x] 게임을 시도할 라운드 횟수를 입력받는다.
- [x] 잘못된 값 입력시, "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션 종료한다. 잘못된 값은 아래 경우에 해당한다.
- [x] 숫자가 아닌 경우
- [x] 음수일 경우

## 출력

- [x] 매 라운드마다 모든 자동차의 전진여부가 결정된 후, 자동차이름과 전진여부(-)를 출력한다.
- 한 라운드마다 전진 시, '-' 로 표기가 하나씩 늘어난다.
- [x] 사용자가 입력한 횟수만큼의 라운드가 종료된 후, 우승자를 출력한다.
- [x] 우승자가 여러명 일 경우 쉼표(,)로 구분한다.

## 핵심 기능

- [x] 사용자가 입력한 횟수만큼 라운드를 진행한다.
- [x] 매 라운드마다 모든 자동차는 각각 0~9 사이의 무작위 값을 골라주는 함수를 통해 반환된 값이 4 이상인 경우에만 한 칸 `전진`한다.
- 아닐 경우 아무 일도 일어나지 않는다.
139 changes: 139 additions & 0 deletions __tests__/FunctionTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import App from "../src/App.js";

test("6자 이하의 자동차 이름으로만 구성된 경우 false", () => {
// given
const app = new App();

// when & then
expect(app.hasLongCarName(["kim", "doidoi", "young"])).toBe(true);
});

test("6자 이상의 자동차 이름이 있는 경우 true", () => {
// given
const app = new App();

// when & then
expect(app.hasLongCarName(["kim", "doidoi", "young"])).toBe(true);
});

test("숫자로 시작하는 자동차 이름이 없는 경우 false", () => {
// given
const app = new App();

// when & then
expect(app.hasCarNameStartingWithNumber(["oioi", "doi99", "kim"])).toBe(
false
);
});

test("숫자로 시작하는 자동차 이름이 있는 경우 true", () => {
// given
const app = new App();

// when & then
expect(
app.hasCarNameStartingWithNumber(["oioi", "917", "8abd", "doi99"])
).toBe(true);
});

test("자동차 위치 속성값 추가하는 함수 테스트", () => {
// given
const app = new App();

// when
const result = [
{ name: "kim", position: 0 },
{ name: "bee", position: 0 },
{ name: "zee", position: 0 },
{ name: "doi", position: 0 },
];

// then
expect(app.createCarsWithPosition(["kim", "bee", "zee", "doi"])).toEqual(
result
);
});

test("우승자 가려내는 함수 테스트", () => {
// given
const app = new App();

// when
const testMap = [
{ name: "kim", position: 5 },
{ name: "bee", position: 3 },
{ name: "zee", position: 5 },
{ name: "doi", position: 2 },
];

// then
const result = [
{ name: "kim", position: 5 },
{ name: "zee", position: 5 },
];
expect(app.getWinners(testMap)).toEqual(result);
});

test("자동차가 랜덤으로 한 칸 움직이는 함수 테스트", () => {
// 자동차가 움직이는 경우
// given
const app = new App();
let car = { name: "kim", position: 3 };

// when
app.moveOneStepRandomly(car, 5);

// then
expect(car.position).toEqual(4);

// 자동차가 가만히 있는 경우
// given
car = { name: "doi", position: 3 };

// when
app.moveOneStepRandomly(car, 3);

// then
expect(car.position).toEqual(3);
});

test("한 라운드 진행상황 출력 함수 테스트", () => {
// given
const app = new App();
const testMap = [
{ name: "kim", position: 2 },
{ name: "bee", position: 4 },
];
console.log = jest.fn();

// when
app.printOneRound(testMap);

// then
expect(console.log).toHaveBeenCalledWith("kim : --");
expect(console.log).toHaveBeenCalledWith("bee : ----");
expect(console.log).toHaveBeenCalledWith("");
});

test("우승자 출력하는 함수 테스트", () => {
// 우승자가 한 명일 때
// given
const app = new App();
console.log = jest.fn();

// when
app.printWinners([{ name: "kim", position: 4 }]);

// then
expect(console.log).toHaveBeenCalledWith("최종 우승자 : kim");

// 우승자가 여러 명일 때
// when
app.printWinners([
{ name: "kim", position: 7 },
{ name: "doi", position: 7 },
]);

// then
expect(console.log).toHaveBeenCalledWith("최종 우승자 : kim, doi");
});
112 changes: 111 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,115 @@
import { MissionUtils } from "@woowacourse/mission-utils";
const SEPARATOR = ",";
const MAXIMUM_CARNAME_THRESHOLD = 5;
const MINIMUM_MOVE_THRESHOLD = 4;

const ERROR_MESSAGE_NO_SEPARATOR =
"[ERROR] 쉼표(,) 구분자가 없습니다. 프로그램을 종료합니다.";
const ERROR_MESSAGE_LONG_CARNAME =
"[ERROR] 구분자는 있으나, 자동차 이름은 5자 이하만 가능합니다. 프로그램을 종료합니다.";
const ERROR_MESSAGE_INVALID_CARNAME =
"[ERROR] 자동차 이름은 문자만 가능합니다. 프로그램을 종료합니다.";
const ERROR_MESSAGE_NOT_NUMBER =
"[ERROR] 숫자가 아닌 값을 입력하셨습니다. 프로그램을 종료합니다.";
const ERROR_MESSAGE_INVALID_ROUND_COUNT =
"[ERROR] 시도 횟수는 1회 이상이어야 합니다. 프로그램을 종료합니다.";

const GUIDE_MESSAGE_ASKING_CAR_NAMES =
"경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로)\n";
const GUIDE_MESSAGE_ASKING_ROUND_COUNT = "시도할 횟수는 몇 회인가요?\n";
const GUIDE_MESSAGE_RESULT = "\n실행 결과";

class App {
async run() {}
async run() {
let carNamesString = await MissionUtils.Console.readLineAsync(
GUIDE_MESSAGE_ASKING_CAR_NAMES
);

if (!carNamesString.includes(SEPARATOR)) {
throw new Error(ERROR_MESSAGE_NO_SEPARATOR);
}

let carNames = carNamesString.split(SEPARATOR);
if (this.hasLongCarName(carNames)) {
throw new Error(ERROR_MESSAGE_LONG_CARNAME);
}
if (this.hasCarNameStartingWithNumber(carNames)) {
throw new Error(ERROR_MESSAGE_INVALID_CARNAME);
}

let roundCountString = await MissionUtils.Console.readLineAsync(
GUIDE_MESSAGE_ASKING_ROUND_COUNT
);
if (isNaN(roundCountString)) {
throw new Error(ERROR_MESSAGE_NOT_NUMBER);
}
let roundCount = Number(roundCountString);
if (roundCount <= 0) {
throw new Error(ERROR_MESSAGE_INVALID_ROUND_COUNT);
}

const cars = this.createCarsWithPosition(carNames);

MissionUtils.Console.print(GUIDE_MESSAGE_RESULT);
for (let i = 0; i < roundCount; i++) {
for (const car of cars) {
this.moveOneStepRandomly(
car,
MissionUtils.Random.pickNumberInRange(0, 9)
);
}
this.printOneRound(cars);
}
this.printWinners(this.getWinners(cars));
}

getWinners(cars) {
const winners = [];
let maxPosition = -1;

for (const car of cars) {
if (car.position > maxPosition) {
maxPosition = car.position;
winners.length = 0;
winners.push(car);
} else if (car.position === maxPosition) winners.push(car);
}
return winners;
}

printWinners(winners) {
if (winners.length === 1)
MissionUtils.Console.print(`최종 우승자 : ${winners[0].name}`);
else {
const winnerNames = winners.map((winner) => winner.name).join(", ");
MissionUtils.Console.print(`최종 우승자 : ${winnerNames}`);
}
}

printOneRound(cars) {
for (const car of cars) {
const positionOutput = "-".repeat(car.position);
MissionUtils.Console.print(`${car.name} : ${positionOutput}`);
}
MissionUtils.Console.print("");
}

moveOneStepRandomly(car, random_number) {
const shouldMove = random_number >= MINIMUM_MOVE_THRESHOLD;
if (shouldMove) car.position++;
}

createCarsWithPosition(carNames) {
return carNames.map((name) => ({ name, position: 0 }));
}

hasLongCarName(carNames) {
return carNames.some((name) => name.length > MAXIMUM_CARNAME_THRESHOLD);
}

hasCarNameStartingWithNumber(carNames) {
return carNames.some((name) => /^\d/.test(name));
}
}

export default App;