Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,54 @@
# javascript-racingcar-precourse
- 프로그램 입출력 처리
- 입력
- 경주할 자동차 이름을 입력받는다
- 시도할 횟수를 입력받는다
- 출력
- 실행결과를 출력한다
- 최종 우승자를 출력한다
- 예외처리

- 공통
- 입력/출력에 사용할 상수 변수들을 따로 관리한다
- 출력용 문자열을 뽑아낼 함수를 따로 만들어 작성한다

- 자동차 경주 계산
- 경주할 자동차를 파싱한다

- 경기 결과
- 경주 1회를 진행한다
- 한번 시도시 경주할 자동차가 얼마나 전진했는지 랜덤으로 얻는다
- 랜덤 숫자을 얻는다
- 전진 여부 기준으로 계산한다
- 시도할 횟수만큼 경주를 진행한다
- 전진 조건을 변수로 따로 관리한다
- 랜덤으로 뽑아낼 범위 조건을 변수로 따로 관리한다
- 랜덤으로 숫자를 뽑아내는 함수를 따로 관리한다

- 최종 우승자 계산

- 에러 예외 케이스
- 에러 처리: [ERROR] 형식을 안정적으로 처리하도록 공통화

- 입력
- 자동차 이름을 사용자가 입력하지 않으면 에러 처리
- 이름이 중복된 경우 에러 처리한다
- 이름을 하나만 입력한 경우 에러 처리한다
- 이름을 5자이상 입력시 에러 처리한다

- 계산
- 우승자가 없는 경우

- 출력
- 시도할 횟수를 사용자가 입력하지 않으면 에러 처리
- 시도할 횟수를 숫자가 아닌걸로 입력하면 에러 처리

- 자동차 경주 게임에서 변경될 만한 내용은 따로 추려서
변경에 유연하게 대처할수 있도록 코드를 개션한다
- 입력
- 5글자 이상
- 구분자 ,
- 계산
- 전진 조건: 4 이상
- 출력
- 최종 우승자 , 로 구분
176 changes: 176 additions & 0 deletions __tests__/CustomTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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;
};

function getDummyInputs(cars, attemps) {
return [cars.join(','), attemps];
}

function getDummyLogs(races, finalWinner) {
return [
...races.flatMap((v) => v).map((race) => `${race.name} : ${'-'.repeat(race.score)}`),
`최종 우승자 : ${finalWinner.join(',')}`,
];
}

function getDummyMockRandoms(races) {
return races.flatMap((v) => v).map((race) => race.number);
}

describe('자동차 경주', () => {
describe('정상', () => {
test('최종 우승자 얻기 계산(하드코딩)', async () => {
// given
const cars = ['pobi', 'woni'];
const attemps = 3;

const finalWinner = 'woni';
const races = [
[
{ name: 'pobi', score: 0, number: 1 },
{ name: 'woni', score: 6, number: 9 },
],
[
{ name: 'pobi', score: 5, number: 8 },
{ name: 'woni', score: 0, number: 2 },
],
[
{ name: 'pobi', score: 0, number: 2 },
{ name: 'woni', score: 2, number: 5 },
],
];

const inputs = ['pobi,woni', '3'];
const logs = [
'pobi : ',
'woni : ------',

'pobi : -----',
'woni : ',

'pobi : ',
'woni : --',

'최종 우승자 : woni',
];
const logSpy = getLogSpy();

mockQuestions(inputs);
mockRandoms([1, 9, 8, 2, 2, 5]);

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

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

test('최종 우승자 얻기 계산(함수)', async () => {
// given
const cars = ['pobi', 'woni'];
const attemps = 3;

const finalWinner = ['woni'];
const races = [
[
{ name: 'pobi', score: 0, number: 1 },
{ name: 'woni', score: 6, number: 9 },
],
[
{ name: 'pobi', score: 5, number: 8 },
{ name: 'woni', score: 0, number: 2 },
],
[
{ name: 'pobi', score: 0, number: 2 },
{ name: 'woni', score: 2, number: 5 },
],
];

const inputs = getDummyInputs(cars, attemps);
const logs = getDummyLogs(races, finalWinner);
const logSpy = getLogSpy();

mockQuestions(inputs);
mockRandoms(getDummyMockRandoms(races));

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

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

test('최종 우승자가 여러명인 경우', async () => {
// given
const cars = ['pobi', 'woni', 'java'];
const attemps = 1;

const finalWinner = ['woni', 'java'];
const races = [
[
{ name: 'pobi', score: 0, number: 1 },
{ name: 'woni', score: 6, number: 9 },
{ name: 'java', score: 6, number: 9 },
],
];

const inputs = getDummyInputs(cars, attemps);
const logs = getDummyLogs(races, finalWinner);
const logSpy = getLogSpy();

mockQuestions(inputs);
mockRandoms(getDummyMockRandoms(races));

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

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

describe('예외', () => {
test("자동차 이름이 5자 이하인 경우", async () => {
// given
const inputs = ["pobi,javaji", 1];
mockQuestions(inputs);

// when
const app = new App();

// then
await expect(app.run()).rejects.toThrow("[ERROR]");
});
});
});
45 changes: 44 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,48 @@
import { Console } from '@woowacourse/mission-utils';

import CarRacing from './CarRacing.js';

import { INPUT, OUTPUT } from './constants.js';

class App {
async run() {}
async run() {
try {
const { inputs, attemps } = await this.input();

const carRacing = new CarRacing({ inputs, attemps });

const races = carRacing.getRaces();
const finalWinner = carRacing.getFinalWinner();

this.output({ races, finalWinner });
} catch (err) {
throw Error(`[ERROR] ${err}`);
}
}

async input() {
const inputs = await Console.readLineAsync(`${INPUT.INPUT_CAR_NAME}\n`);

const attemps = await Console.readLineAsync(`${INPUT.INPUT_ATTEMPS}\n`);

if (!attemps) throw new Error('[ERROR]');

return { inputs, attemps };
}

async output({ races, finalWinner }) {
Console.print(`\n${OUTPUT.EXECUTION_RESULT}`);

races.forEach((race) => {
const raceResult = race.map((v) => `${v.name} : ${'-'.repeat(v.score)}`);

raceResult.forEach((v) => {
Console.print(`${v}\n`);
});
});

Console.print(`\n${OUTPUT.FINAL_RESULT} : ${finalWinner.join(',')}`);
}
}

export default App;
109 changes: 109 additions & 0 deletions src/CarRacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Random } from '@woowacourse/mission-utils';

class CarRacing {
constructor({ inputs, attemps }) {
this.inputs = inputs;
this.attemps = attemps;
}

checkValidationCars(cars) {
cars.forEach((car) => {
if (car.length > 5) {
throw '자동차 이름을 5자이하만 가능하다';
}
});

return true;
}

getCars() {
const DELIMETER = ',';
const result = this.inputs.split(DELIMETER);

if (!this.checkValidationCars(result)) return;

return result;
}

getRandom() {
return Random.pickNumberInRange(0, 9);
}

getScore(number) {
const THREDSHOLD = 4;
const calculatedScore = number - THREDSHOLD + 1;

return calculatedScore <= 0 ? 0 : calculatedScore;
}

getRace() {
const cars = this.getCars();

return cars.map((car) => {
const randomNumber = this.getRandom();
const score = this.getScore(randomNumber);

return { name: car, score };
});
}

getRaces() {
const { attemps } = this;

const result = Array.from({ length: attemps }).map(() => this.getRace());

this.races = result;

return result;
}

calculateWinner(race) {
const highScore = race.reduce((acc, r) => (r.score > acc ? r.score : acc), 0);

return race.filter((r) => r.score === highScore).map((r) => r.name);
}

calculateRaceWinners(races) {
return races.map((race) => this.calculateWinner(race));
}

calculateWinCount(raceWinners) {
const cars = this.getCars();

const carCount = cars.reduce((acc, car) => {
acc[car] = 0;
return acc;
}, {});

return raceWinners.reduce((acc, winners) => {
winners.forEach((winner) => {
acc[winner] += 1;
});

return acc;
}, carCount);
}

calculateFinalWinner(raceWinners) {
// { pobi: 2, woni: 1 }
const winCount = this.calculateWinCount(raceWinners);

const highTimes = Object.values(winCount).reduce((acc, v) => (v > acc ? v : acc), 0);

return Object.entries(winCount)
.filter(([_, times]) => times === highTimes)
.map((([name]) => name));
}

getFinalWinner() {
// [['pobi', 'woni'], ['pobi']]
const raceWinners = this.calculateRaceWinners(this.races);

// ['pobi'] || ['pobi', 'woni']
const finalWinner = this.calculateFinalWinner(raceWinners);

return finalWinner;
}
}

export default CarRacing;
Loading