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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
# javascript-racingcar-precourse

# 2주차 구현 체크리스트

상태: 시작 전
마감일: 2025년 10월 21일

## **기능 요구 사항**

초간단 자동차 경주 게임을 구현한다.

- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
- 이름 부여, 출략도 같이 나올 수 있게 객체 활용? 객체화 시켜서 정렬시키면 될듯!
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
- 쉼표로 나눠서 구분 입력은 5자 아래로
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- 총 몇번 반복할지 코드 베이스를 하나의 큰 틀에 담아 반복
- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
- 랜덤 메서드를 통해서 무작위 값을 0~9 까지 구현
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.
- 우승자 구분 함수 필요
- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.
- 우승자와 점수가 같다면 ,를 통해 같이 출력
- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션은 종료되어야 한다.

- [ ] 먼저 이름을 입력받고
- [ ] 진행할 횟수 입력
- [ ] 랜덤으로 숫자가 돌아감
- [ ] 숫자 판별시 카운트
- [ ] 숫자와 이름을 객체화
- [ ] 숫자에 맞게 짝대기 추가
- [ ] 짝대기와 이름에 맞게 새롭게 객체화
- [ ] 우승자 판별 함수
- [ ] 우승자 배열에 담음
- [ ] 우승자 배열 출력
- [ ] 예외처리 추가
- [ ] 우승자 이름 정렬
73 changes: 73 additions & 0 deletions __tests__/car.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import CarInfo from "../src/carInfo.js";

describe("CarInfo 클래스 테스트", () => {
describe("생성자 테스트", () => {
test("자동차 이름이 5자 이하인 경우 정상적으로 생성된다", () => {
expect(() => new CarInfo("pobi")).not.toThrow();
expect(() => new CarInfo("12345")).not.toThrow();
});

test("자동차 이름이 5자를 초과하면 예외가 발생한다", () => {
const car = new CarInfo("123456");
expect(async () => await car.carName("123456")).rejects.toThrow("[ERROR]");
});

test("자동차 이름이 빈 문자열이면 예외가 발생한다", () => {
const car = new CarInfo("");
expect(async () => await car.carName("")).rejects.toThrow("[ERROR]");
expect(async () => await car.carName(" ")).rejects.toThrow("[ERROR]");
});
});

describe("move 메서드 테스트", () => {
test("랜덤 값이 4 이상이면 전진한다", () => {
const CAR = new CarInfo("pobi");
CAR.move(4);
expect(CAR.getPosition()).toBe(1);

CAR.move(5);
expect(CAR.getPosition()).toBe(2);

CAR.move(3);
expect(CAR.getPosition()).toBe(2);
});

test("랜덤 값이 4 미만이면 멈춘다", () => {
const CAR = new CarInfo("pobi");
CAR.move(3);
expect(CAR.getPosition()).toBe(0);

CAR.move(0);
expect(CAR.getPosition()).toBe(0);
});

test("계속 전진하면 위치가 증가한다", () => {
const CAR = new CarInfo("test");
CAR.move(4);
CAR.move(5);
CAR.move(9);
expect(CAR.getPosition()).toBe(3);
});
});

describe("getter 메서드 테스트", () => {
test("자동차 이름을 반환한다", () => {
const CAR = new CarInfo("pobi");
expect(CAR.getName()).toBe("pobi");
});

test("자동차 위치를 시각적으로 표현한다", () => {
const CAR = new CarInfo("pobi");
expect(CAR.getDisplayedPosition()).toBe("");

CAR.move(4);
expect(CAR.getDisplayedPosition()).toBe("-");

CAR.move(5);
expect(CAR.getDisplayedPosition()).toBe("--");

CAR.move(3);
expect(CAR.getDisplayedPosition()).toBe("--");
});
});
});
150 changes: 150 additions & 0 deletions __tests__/raceGame.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import RaceGame from "../src/raceGame.js";
import { Console } from "@woowacourse/mission-utils";
import makeRandomNumber from "../src/randomNumber.js";

jest.mock("@woowacourse/mission-utils");
jest.mock("../src/randomNumber.js", () => ({
__esModule: true,
default: jest.fn(),
}));

describe("RaceGame 클래스 테스트", () => {
beforeEach(() => {
jest.clearAllMocks();
Console.print = jest.fn();
});

describe("생성자 테스트", () => {
test("이름과 시행회수를 정상적으로 입력받으면 게임을 실행한다", () => {
expect(() => new RaceGame("pobi,woni,jun", 5)).not.toThrow();
});

test("여러 자동차를 정상적으로 만든다", () => {
const game = new RaceGame("pobi,woni,jun", 5);

expect(game.cars.length).toBe(3);
expect(game.cars[0].getName()).toBe("pobi");
expect(game.cars[1].getName()).toBe("woni");
expect(game.cars[2].getName()).toBe("jun");
});

test("이름에 있는 공백을 자동으로 제거한다", () => {
const game = new RaceGame("pobi , woni , jun", 5);

expect(game.cars.length).toBe(3);
expect(game.cars[0].getName()).toBe("pobi");
expect(game.cars[1].getName()).toBe("woni");
expect(game.cars[2].getName()).toBe("jun");
});
});

describe("playRound 테스트", () => {
test("모든 자동차가 한 라운드를 진행한다", () => {
const game = new RaceGame("pobi,woni", 1);
makeRandomNumber.mockReturnValue(4);

game.playRound();

expect(makeRandomNumber).toHaveBeenCalledTimes(2);
});

test("랜덤값이 4 이상이면 자동차가 전진한다", () => {
const game = new RaceGame("pobi", 1);
makeRandomNumber.mockReturnValue(4);

game.playRound();

expect(game.cars[0].getPosition()).toBe(1);
});

test("랜덤값이 3 이하면 자동차가 멈춘다", () => {
const game = new RaceGame("pobi", 1);
makeRandomNumber.mockReturnValue(3);

game.playRound();

expect(game.cars[0].getPosition()).toBe(0);
});
});

describe("displayRoundResult 테스트", () => {
test("각 자동차의 상태를 출력한다", () => {
const game = new RaceGame("pobi,woni", 1);
makeRandomNumber.mockReturnValueOnce(4).mockReturnValueOnce(3);

game.playRound();
game.displayRoundResult();

expect(Console.print).toHaveBeenCalledWith("pobi : -");
expect(Console.print).toHaveBeenCalledWith("woni : ");
expect(Console.print).toHaveBeenCalledWith("");
});
});

describe("getWinners 테스트", () => {
test("단일 우승자를 반환한다", () => {
const game = new RaceGame("pobi,woni", 1);
makeRandomNumber.mockReturnValueOnce(4).mockReturnValueOnce(3);

game.playRound();

const winners = game.getWinners();

expect(winners).toEqual(["pobi"]);
});

test("여러 우승자를 반환한다", () => {
const game = new RaceGame("pobi,woni,jun", 2);

makeRandomNumber
.mockReturnValueOnce(4)
.mockReturnValueOnce(4)
.mockReturnValueOnce(3)
.mockReturnValueOnce(4)
.mockReturnValueOnce(4)
.mockReturnValueOnce(3);

game.play();
const winners = game.getWinners();

expect(winners).toContain("pobi");
expect(winners).toContain("woni");
expect(winners.length).toBe(2);
});

test("모든 자동차가 같은 위치면 모두 우승자다", () => {
const game = new RaceGame("pobi,woni,jun", 1);
makeRandomNumber.mockReturnValue(3);

game.playRound();

const winners = game.getWinners();

expect(winners.length).toBe(3);
expect(winners).toContain("pobi");
expect(winners).toContain("woni");
expect(winners).toContain("jun");
});
});

describe("play 테스트", () => {
test("지정된 라운드 수만큼 게임을 진행한다", () => {
const game = new RaceGame("pobi,woni", 3);
makeRandomNumber.mockReturnValue(4);

game.play();

expect(Console.print).toHaveBeenCalledWith("실행 결과");
expect(Console.print).toHaveBeenCalledTimes(10);
});

test("각 라운드마다 자동차들이 이동한다", () => {
const game = new RaceGame("pobi", 2);
makeRandomNumber.mockReturnValue(4);

game.play();

expect(game.cars[0].getPosition()).toBe(2);
});
});
});
20 changes: 19 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { Console } from "@woowacourse/mission-utils";
import RaceGame from "./raceGame.js";
import GameInput from "./gameInput.js";

class App {
async run() {}
async run() {
try {
const CAR_NAMES = await GameInput.getCarNames();
const ROUND_COUNT = await GameInput.getRoundCount();

Comment on lines +8 to +10

Choose a reason for hiding this comment

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

입력 받는 부분을 하나의 함수로 분리해서, 자동차 이름 입력과 라운드 입력을 받는 하나의 입력 단계로 App.js에 구성하는 것은 어떨까요?

const [carNames, roundCount] = await initRacingGame();
async function initRacingGame() {
  const carNames = await GameInput.getCarNames();
  const roundCount = await GameInput.getRoundCount();

  return [carNames, roundCount];
}

const RACE_GAME = new RaceGame(CAR_NAMES, ROUND_COUNT);
RACE_GAME.play();

const WINNERS = RACE_GAME.getWinners();
Console.print(`최종 우승자 : ${WINNERS.join(", ")}`);
Comment on lines +14 to +15

Choose a reason for hiding this comment

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

이후 로직에서 배열을 다시 사용하지 않기에, join하는 부분을 getWinners()와 함께 사용하는 것은 어떨까요?

1안

Console.print(`최종 우승자 : ${RACE_GAME.getWinners().join(", ")}`);

2안

const WINNERS = RACE_GAME.getWinners().join(", ");
Console.print(`최종 우승자 : ${WINNERS}`);

} catch (error) {
Console.print(error.message);
throw error;
}
}
}

export default App;
36 changes: 36 additions & 0 deletions src/carInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export default class CarInfo {
static MAX_NAME_LENGTH = 5;
static MOVE_THRESHOLD = 4;

constructor(name) {
this.name = name;
this.position = 0;
}

async carName(name) {
if (!name || name.trim().length === 0) {
throw new Error("[ERROR] 자동차 이름은 공백으로 자을수 없습니다.");
}
if (name.length > CarInfo.MAX_NAME_LENGTH) {
throw new Error("[ERROR] 자동차 이름은 5자 이하로 만들어야 합니다.");
}
}

move(randomValue) {
if (randomValue >= CarInfo.MOVE_THRESHOLD) {
this.position++;
}
}

getName() {
return this.name;
}

getPosition() {
return this.position;
}

getDisplayedPosition() {
return "-".repeat(this.position);
}
}
21 changes: 21 additions & 0 deletions src/gameInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Console } from "@woowacourse/mission-utils";

export default class GameInput {
static async getCarNames() {
const INPUT = await Console.readLineAsync("자동차 이름을 입력하세요.");
const TRIMMED_INPUT = INPUT.replaceAll(" ", "");

return TRIMMED_INPUT;
}

static async getRoundCount() {
const INPUT = await Console.readLineAsync("실행 횟수를 입력하세요. : ");
const COUNT = Number(INPUT);

if (isNaN(COUNT) || COUNT <= 0 || !Number.isInteger(COUNT)) {
throw new Error("[ERROR] 1 이상의 정수를 입력해 주세요.");
}

return COUNT;
}
}
45 changes: 45 additions & 0 deletions src/raceGame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import CarInfo from "./carInfo.js";
import makeRandomNumber from "./randomNumber.js";
import { Console } from "@woowacourse/mission-utils";

export default class RaceGame {
constructor(carNames, roundCount) {
this.cars = this.createCars(carNames);
this.roundCount = roundCount;
}

createCars(carNames) {
const NAME_ARRAY = carNames.split(",");

return NAME_ARRAY.map((name) => new CarInfo(name.trim()));
Comment on lines +12 to +14

Choose a reason for hiding this comment

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

대문자 네이밍은 상수(불변, 전역 수준)에 사용하는 것이 원칙으로 알고 있습니다!

여기서는 지역 변수이므로, 소문자로 변경하는 것이 좋을듯 합니다!

}

playRound(RandomNumber) {
this.cars.forEach((car) => {
const randomNumber = makeRandomNumber();
car.move(randomNumber);
});
}
Comment on lines +17 to +22

Choose a reason for hiding this comment

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

인자를 제거하는 것은 어떨까요?

현재 RandomNumber 매개변수를 받지만, 실제로 사용하고 있지 않는 코드로 보입니다.

이것은 혼동을 줄 수 있는 죽은 파라미터와 같아서, 제거하는 것이 깔끔해 보입니다!

혹시 인자를 추가하신 이유가 있으실까요?


displayRoundResult() {
this.cars.forEach((car) => {
Console.print(`${car.getName()} : ${car.getDisplayedPosition()}`);
});
Console.print("");
}

play() {
Console.print("실행 결과");
for (let i = 0; i < this.roundCount; i++) {
this.playRound();
this.displayRoundResult();
}
}

getWinners() {
const MAX_POSITION = Math.max(...this.cars.map((car) => car.getPosition()));
return this.cars
.filter((car) => car.getPosition() === MAX_POSITION)
.map((car) => car.getName());
}
}
Loading