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

[자동차 경주] 조영후 미션 제출합니다. #739

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
925cbc0
docs: 기능 목록 작성
choekko Nov 1, 2023
856d07e
feat: string을 입력받고 해당 string이 '경주할 자동차 이름 형식'에 알맞는지 판단하는 유효성 검사 함수 구현
choekko Nov 1, 2023
4d774b5
feat: 숫자를 입력받고, 해당 숫자가 '시도할 횟수' 형식에 알맞는지 판단하는 유효성 검사 함수 구현
choekko Nov 1, 2023
74a56fe
feat: 0~9 사이 무작위값으로 전진 여부를 반환하는 함수 구현
choekko Nov 1, 2023
21c7729
feat: 양의 정수 값을 받아서 해당 값에 해당하는 횟수만큼 전진 여부 판단 후, 그 결과를 배열로 반환하는 함수 구현
choekko Nov 1, 2023
8feaa23
feat: 자동차 이름 배열과 시도할 횟수를 받아서, 각 자동차별 전진 여부 배열을 객체 형식으로 반환하는 함수 구현 & j…
choekko Nov 1, 2023
8e5053a
feat: 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 우승한 자동차들의 이름 배열을 반환하는 함수 구현
choekko Nov 1, 2023
cf49a4c
feat: 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 출력 형식에 맞게 반복 출력하는 함수 구현
choekko Nov 1, 2023
68a11d3
feat: 우승한 자동차들의 이름 배열을 받아서, 출력 형식에 맞게 출력하는 함수 구현
choekko Nov 1, 2023
1a652a4
fix: 함수명 변경
choekko Nov 1, 2023
04fefe5
feat: 게임을 진행하는 play 메서드 구현
choekko Nov 1, 2023
66e776d
refactor: 불필요 if 문 제거 & jsDoc 오타 수정
choekko Nov 1, 2023
c357a72
docs: README 체크 목록 최신화
choekko Nov 1, 2023
9be6906
refactor: 컨벤션 검토 & jsDoc 오기재 수정
choekko Nov 1, 2023
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,14 @@ MissionUtils.Random.pickNumberInRange(0, 9);
- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다.
- [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다.
- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다.

## 기능 목록
- [x] string을 입력받고 해당 string이 '경주할 자동차 이름 형식'에 알맞는지 판단하는 유효성 검사 함수 구현
- [x] 숫자를 입력받고, 해당 숫자가 '시도할 횟수' 형식에 알맞는지 판단하는 유효성 검사 함수 구현
- [x] 0~9 사이 무작위값으로 전진 여부를 반환하는 함수 구현
- [x] 양의 정수 값을 받아서 해당 값에 해당하는 횟수만큼 전진 여부 판단 후, 그 결과를 배열로 반환하는 함수 구현
- [x] 자동차 이름 배열과 시도할 횟수를 받아서, 각 자동차별 전진 여부 배열을 객체 형식으로 반환하는 함수 구현
- [x] 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 우승한 자동차들의 이름 배열을 반환하는 함수 구현
- [x] 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 출력 형식에 맞게 반복 출력하는 함수 구현
- [x] 우승한 자동차들의 이름 배열을 받아서, 출력 형식에 맞게 출력하는 함수 구현
- [x] 상기 함수들을 토대로 게임을 진행하는 play 메서드 구현
119 changes: 119 additions & 0 deletions __tests__/GameUtilTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Random, Console } from '@woowacourse/mission-utils';
import {
getCarsMovementInfo,
getMovingResult,
getWinners,
hasMovedForward,
printCarsMovementInfo,
printWinners
} from '../src/game.js';

describe('게임 유틸', () => {
beforeEach(() => {
jest.restoreAllMocks();
})

describe('hasMovedForward', () => {
test('Random.pickNumberInRange 유틸이 4미만의 값을 반환한다면, false를 반환해야 한다', () => {
jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(3);
expect(hasMovedForward()).toBe(false);
})

test('Random.pickNumberInRange 유틸이 4이상의 값을 반환한다면, true를 반환해야 한다', () => {
jest.spyOn(Random, 'pickNumberInRange').mockReturnValue(5);
expect(hasMovedForward()).toBe(true);
})
})

describe('getMovingResult', () => {
test('자연수를 입력하지 않으면, 에러를 반환해야 한다.', () => {
expect(() => getMovingResult(-1)).toThrowError();
})
test('반환값은 모두 boolean 이어야 한다.', () => {
const movingResult = getMovingResult(3);
movingResult.forEach((result) => {
expect(result).toEqual(expect.any(Boolean));
})
})
test('입력한 자연수만큼의 길이를 갖는 배열을 반환해야한다.', () => {
const count = 5;
const movingResult = getMovingResult(count);
expect(movingResult.length).toBe(count);
})
})

describe('getCarsMovementInfo', () => {
test('올바른 carNames와 count를 입력하면, 해당 carNames 요소들을 객체 key로 갖고 전진 여부 배열을 value로 갖는 object가 반환되어야 한다.', () => {
const carNames = ['foo', 'bar'];
const count = 5;
const carsMovementInfo = getCarsMovementInfo(carNames, count);
const carsMovementInfoKeys = Object.keys(carsMovementInfo);
const carsMovementInfoValues = Object.values(carsMovementInfo);

carNames.forEach(carName => {
expect(carsMovementInfoKeys).toContain(carName);
})

carsMovementInfoValues.forEach(movingResult => {
expect(movingResult.length).toBe(count);

movingResult.forEach((result) => {
expect(result).toEqual(expect.any(Boolean));
})
})
})
})

describe('getWinners', () => {
test('올바른 carsMovementInfo를 입력하면, 1명 이상의 우승자 이름을 담고 있는 배열을 반환해야 한다.', () => {
const carNames = ['foo', 'bar'];
const count = 5;
const carsMovementInfo = getCarsMovementInfo(carNames, count);
expect(getWinners(carsMovementInfo).length).toBeGreaterThanOrEqual(1);
})
})

describe('printCarsMovementInfo', () => {
function countOccurrences(inputString, search) {
// 문자열에서 검색 문자열의 모든 등장 횟수를 세는 함수
const regex = new RegExp(search, 'g');
const matches = inputString.match(regex);
return matches ? matches.length : 0;
}

test('올바른 carsMovementInfo 입력하면, 전진 횟수만큼 carName이 반복 노출되어야 한다', () => {
const carNames = ['foo', 'bar'];
const count = 5;
const carsMovementInfo = getCarsMovementInfo(carNames, count);
const printSpy = jest.spyOn(Console, 'print');

printCarsMovementInfo(carsMovementInfo);
const printOutput = printSpy.mock.calls.map((args) => args[0]).join('\n');
carNames.forEach(carName => {
expect(countOccurrences(printOutput, carName)).toBe(count);
})
})
})

describe('printWinners', () => {
test('string 배열을 입력하면, "최종 우승자 : " 로 시작하는 문자열이 출력되어야 한다.', () => {
const winners = ['foo', 'bar'];
const printSpy = jest.spyOn(Console, 'print');

printWinners(winners);
const printOutput = printSpy.mock.calls.map((args) => args[0]).join('\n');

expect(printOutput.startsWith('최종 우승자 : ')).toBe(true);
})

test('string 배열을 입력했을 때 출력되는 값에는 해당 string 요소들이 ","를 구분자로 붙은 문자열이 포함되어야 한다.', () => {
const winners = ['foo', 'bar'];
const printSpy = jest.spyOn(Console, 'print');

printWinners(winners);
const printOutput = printSpy.mock.calls.map((args) => args[0]).join('\n');

expect(printOutput).toContain(winners.join(', '));
})
})
})
86 changes: 86 additions & 0 deletions __tests__/ValidationTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
hasDuplicateString,
isNaturalNumber,
isOnlySpace,
isShorterThan5Chars,
validateCarNamesString, validateCountString
} from '../src/validation.js';

describe('유효성 검사 함수', () => {
describe('isShorterThan5Chars', () => {
test('5자리 이하 문자열을 입력하면, true를 반환한다', () => {
expect(isShorterThan5Chars('123')).toBe(true);
})

test('6자리 이상 문자열을 입력하면, false를 반환한다.', () => {
expect(isShorterThan5Chars('123456')).toBe(false);
})
})

describe('hasDuplicateString', () => {
test('중복된 문자열을 갖는 배열을 입력하면 true를 반환한다.', () => {
expect(hasDuplicateString(['foo', 'foo', 'bar'])).toBe(true);
})

test('중복된 문자열을 갖지 않는 배열을 입력하면 false를 반환한다.', () => {
expect(hasDuplicateString(['foo', 'bar'])).toBe(false);
})
})

describe('isOnlySpace', () => {
test('공백만 포함된 문자열을 입력하면 true를 반환한다.', () => {
expect(isOnlySpace('\t')).toBe(true);
expect(isOnlySpace(' ')).toBe(true);
expect(isOnlySpace(' a')).toBe(false);
})
})

describe('validateCarNamesString', () => {
test('5자리 이상의 자동차 이름을 포함한 문자열을 입력하면 에러를 반환한다.', () => {
expect(() => validateCarNamesString('foo,bar,foobar')).toThrowError();
expect(() => validateCarNamesString('foobar')).toThrowError();
})

test('공백만으로 이루어진 자통자 이름을 포함한 문자열을 입력하면 에러를 반환한다.', () => {
expect(() => validateCarNamesString('foo, ,bar')).toThrowError();
})

test('중복된 자동차 이름을 포함한 문자열을 입력하면 에러를 반환한다.', () => {
expect(() => validateCarNamesString('foo,foo')).toThrowError();
})

test('중복된 이름 없이 5자리 이하 자동차 이름만을 포함한 문자열을 입력하면 true를 반환한다.', () => {
expect(validateCarNamesString('foo,bar')).toBe(true);
})
})

describe('isNaturalNumber', () => {
test('음의 정수를 입력하면 false를 반환한다.', () => {
expect(isNaturalNumber(-1)).toBe(false);
})

test('문자열을 입력하면 false를 반환한다.', () => {
expect(isNaturalNumber('123')).toBe(false);
})

test('양의 소수를 입력하면 false를 반환한다.', () => {
expect(isNaturalNumber(1.2)).toBe(false);
})

test('자연수를 입력하면 true를 반환한다.', () => {
expect(isNaturalNumber(3)).toBe(true);
})
})

describe('validateCountString', () => {
test('음의 정수 문자열을 입력하면 에러를 반환한다.', () => {
expect(() => validateCountString('-1')).toThrowError();
})
test('양의 소수 문자열을 입력하면 에러를 반환한다.', () => {
expect(() => validateCountString('1.23')).toThrowError();
})
test('자연수 문자열을 입력하면 true를 반환한다.', () => {
expect(validateCountString('123')).toBe(true);
})
})
})
24 changes: 23 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { Console } from '@woowacourse/mission-utils';
import { validateCarNamesString, validateCountString } from './validation.js';
import { getCarsMovementInfo, getWinners, printCarsMovementInfo, printWinners } from './game.js';

class App {
async play() {}
async play() {
const carNamesAsString = await Console.readLineAsync('경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n');
validateCarNamesString(carNamesAsString);
const carNames = carNamesAsString.split(',');

const countAsString = await Console.readLineAsync('시도할 횟수는 몇 회인가요?\n');
validateCountString(countAsString);
const count = Number(countAsString);

Console.print('\n실행 결과');

const carsMovementInfo = getCarsMovementInfo(carNames, count);
const winners = getWinners(carsMovementInfo);

printCarsMovementInfo(carsMovementInfo);
Console.print('\n');

printWinners(winners);
}
}

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

/**
* 전진 여부 판단 함수
* @returns {boolean}
*/
export const hasMovedForward = () => {
const randomNumber = Random.pickNumberInRange(0, 9);

return randomNumber >= 4;
}


/**
* 횟수만큼 전진 여부 판단 후, 그 결과를 배열로 반환하는 함수
* @param {number} count
* @returns {boolean[]}
*/
export const getMovingResult = (count) => {
if (!isNaturalNumber(count)) {
throw new Error('[ERROR] 인자로 자연수를 입력해주세요. (getMovingResult)');
}
return Array.from({ length: count }).map(() => hasMovedForward());
}

/**
* 자동차 이름 배열과 시도할 횟수를 받아서, 각 자동차별 전진 여부 배열을 객체 형식으로 반환하는 함수
*
* @param {string[]} carNames
* @param {number} count
* @returns {{[carName: string]: boolean[]}}
*/
export const getCarsMovementInfo = (carNames, count) => {
const carsMovementInfo = {};

for (const carName of carNames) {
carsMovementInfo[carName] = getMovingResult(count);
}

return carsMovementInfo;
}

/**
* 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 우승한 자동차들의 이름 배열을 반환하는 함수
*
* @param {{[carName: string]: boolean[]}} carsMovementInfo
* @returns {string[]}
*/
export const getWinners = (carsMovementInfo) => {
let winners = [];
let maxMovingCount = -Infinity;

Object.entries(carsMovementInfo).forEach(([carName, movingResult]) => {
const movingCount = movingResult.reduce((acc, curr) => acc + Number(curr), 0);
if (movingCount > maxMovingCount) {
winners = [carName];
maxMovingCount = movingCount;
} else if (movingCount === maxMovingCount) {
winners.push(carName);
}
});

return winners;
}

/**
* 각 자동차별 전진 여부 배열 정보를 가진 객체를 받아서, 출력 형식에 맞게 반복 출력하는 함수
* @param {{[carName: string]: boolean[]}} carsMovementInfo
*/
export const printCarsMovementInfo = (carsMovementInfo) => {
const carNames = Object.keys(carsMovementInfo);
const movingCount = carsMovementInfo[carNames[0]].length;

const status = {};
carNames.forEach(carName => status[carName] = '');

const result = [];

Array.from({ length: movingCount }, (_, i) => i).forEach(i => {
carNames.forEach(carName => {
status[carName] += carsMovementInfo[carName][i] ? '-' : '';
})

const roundResult = [];
Object.entries(status).forEach(([carName, currentStatus]) => {
roundResult.push(`${carName} : ${currentStatus}`);
})
result.push(roundResult.join('\n'));
})

Console.print(result.join('\n\n'));
}

/**
* 우승자를 출력하는 함수
* @param {string[]} winners
*/
export const printWinners = (winners) => {
Console.print(`최종 우승자 : ${winners.join(', ')}`);
}
Loading