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

[숫자야구 미션 다시 풀어보기 리뷰] 리뷰용 PR입니다. #873

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
394604d
Docs: 기능 목록 및 예외 상황 작성
kang-kibong Nov 24, 2023
42544ca
Feat: 게임시작 안내문을 출력하는 기능 추가
kang-kibong Nov 24, 2023
27745a8
Feat: 숫자를 입력받는 기능 추가
kang-kibong Nov 24, 2023
1e50443
Test: 숫자 입력이 3자리가 아닌 경우 테스트 추가
kang-kibong Nov 24, 2023
d0b6975
Feat: 숫자 입력이 3자리가 아닌 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
ae6b132
Test: 숫자가 아닌 경우 테스트 추가
kang-kibong Nov 24, 2023
8fc32cc
Feat: 숫자가 아닌 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
1af9a76
Test: 숫자가 음수일 경우 테스트 추가
kang-kibong Nov 24, 2023
289d818
Feat: 숫자가 음수일 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
e313d92
Test: 숫자가 중복일 경우 테스트 추가
kang-kibong Nov 24, 2023
2038ba1
Feat: 숫자가 중복일 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
3decfa8
Test: 숫자를 입력하지 않았을 경우 테스트 추가
kang-kibong Nov 24, 2023
68c78dd
Feat: 숫자를 입력하지 않았을 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
d292780
Feat: 게임 재시작 여부를 입력받는 기능 추가
kang-kibong Nov 24, 2023
4a58852
Test: 게임 재시작 여부 입력이 1, 2가 아닌 다른 값을 입력한 경우 테스트 추가
kang-kibong Nov 24, 2023
8e0fb7a
Feat: 게임 재시작 여부 입력이 1, 2가 아닌 다른 값을 입력한 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
10800ce
Test: 게임 재시작 여부 입력이 음수일 경우 테스트 추가
kang-kibong Nov 24, 2023
e4cd366
Feat: 게임 재시작 여부 입력이 음수일 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
2b0d0c1
Test: 게임 재시작 여부 입력이 숫자가 아닌 경우 테스트 추가
kang-kibong Nov 24, 2023
12e17ea
Feat: 게임 재시작 여부 입력이 숫자가 아닌 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
93259b5
Test: 게임 재시작 여부를 입력하지 않았을 경우 테스트 추가
kang-kibong Nov 24, 2023
6fa2359
Feat: 게임 재시작 여부를 입력하지 않았을 경우 유효성 검사 추가
kang-kibong Nov 24, 2023
4bcb76e
Docs: 기능 목록 체크박스 수정
kang-kibong Nov 24, 2023
893e5da
Refactor: given, when, then 추가
kang-kibong Nov 25, 2023
c0cc2e4
Rename: test 파일 이름 수정
kang-kibong Nov 28, 2023
e002eae
Test: 스트라이크 개수를 반환하는 기능 테스트 추가
kang-kibong Nov 28, 2023
e6aba10
Docs: 스트라이크 개수 및 볼 개수 기능 목록 추가
kang-kibong Nov 28, 2023
e9d2896
Test: 볼의 개수를 반환하는 기능 테스트 추가
kang-kibong Nov 28, 2023
8307783
Docs: 1~9까지의 3개의 숫자를 랜덤으로 반환하는 기능 목록 추가
kang-kibong Nov 28, 2023
95b2f25
Feat: 1~9까지의 3개의 숫자를 랜덤으로 반환하는 기능 추가
kang-kibong Nov 28, 2023
63acbd7
Feat: 입력한 숫자의 스트라이크의 개수를 반환하는 기능 추가
kang-kibong Nov 28, 2023
ffd6607
Feat: 입력한 숫자의 볼의 개수를 반환하는 기능 추가
kang-kibong Nov 28, 2023
e5e6aca
Refactor: NumbersValidator 테스트 코드 리팩토링
kang-kibong Nov 29, 2023
09988d0
Refactor: RestartValidator 테스트 코드 리팩토링
kang-kibong Nov 29, 2023
c758d66
Refactor: generateRandomNumbers 메서드 리팩토링
kang-kibong Nov 30, 2023
405681d
Docs: 볼, 스트라이크 결과 메시지를 반환하는 기능 목록 추가
kang-kibong Nov 30, 2023
0d137ec
Test: 볼, 스트라이크 결과 메시지를 반환하는 기능 테스트 추가
kang-kibong Nov 30, 2023
f88435c
Feat: 볼, 스트라이크 결과 메시지를 반환하는 기능 추가
kang-kibong Nov 30, 2023
6d35742
Feat: 숫자 야구 게임 결과를 출력하는 기능 추가
kang-kibong Nov 30, 2023
d762fc5
Feat: 성공 안내문을 출력하는 기능 추가
kang-kibong Nov 30, 2023
93b8e6e
Feat: BaseBallGameController 기능 추가
kang-kibong Nov 30, 2023
421a8f0
Refactor: BaseBallGameController 리팩토링
kang-kibong Nov 30, 2023
bb8e0ec
Style: eslint 적용
kang-kibong Nov 30, 2023
c12fbc3
Docs: 기능 목록 수정
kang-kibong Nov 30, 2023
356bad7
Refactor: validator들을 private으로 수정
kang-kibong Nov 30, 2023
9eddc65
Refactor: app.play() 제거
kang-kibong Nov 30, 2023
ba38576
Feat: BaseballGameService 서비스 추가
kang-kibong Dec 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
65 changes: 65 additions & 0 deletions __tests__/domain/HintTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Hint from '../../src/domain/Hint.js';

describe('Hint 클래스 테스트', () => {
describe('calcultaeStrikeCount 메서드는 numbers와 computerNumbers를 입력받아 스크라이크 개수를 반환한다.', () => {
const cases = [
{ numbers: [1, 2, 3], computerNumbers: [2, 3, 1], expected: 0 },
{ numbers: [1, 2, 3], computerNumbers: [1, 2, 3], expected: 3 },
{ numbers: [4, 2, 1], computerNumbers: [4, 1, 2], expected: 1 },
];

test.each(cases)(
'사용자의 번호 $numbers와 랜덤으로 생성된 번호 $computerNumbers가 주어지는 경우, calcultaeStrikeCount()는 개수 $expected를 반환한다.',
({ numbers, computerNumbers, expected }) => {
// when
const hint = new Hint(numbers, computerNumbers);
const strikeCount = hint.calculateStrikeCount();
// then
expect(strikeCount).toEqual(expected);
},
);
});

describe('calculateBallCount 메서드는 strikeCount를 입력받아 볼 개수를 반환한다.', () => {
const cases = [
{ numbers: [1, 2, 3], computerNumbers: [2, 3, 1], expected: 3 },
{ numbers: [1, 2, 3], computerNumbers: [1, 2, 3], expected: 0 },
{ numbers: [4, 2, 1], computerNumbers: [4, 1, 2], expected: 2 },
];

test.each(cases)(
'스트라이크 개수 $strikeCount가 주어지는 경우, calculateBallCount()는 개수 $expected를 반환한다.',
({ numbers, computerNumbers, expected }) => {
// when
const hint = new Hint(numbers, computerNumbers);
const strikeCount = hint.calculateStrikeCount();
const ballCount = hint.calculateBallCount(strikeCount);

// then
expect(ballCount).toEqual(expected);
},
);
});

describe('generateHintMessage 메서드는 strikeCount와 ballCount를 입력받아 결과 메시지의 배열을 반환한다.', () => {
const cases = [
{ numbers: [1, 2, 3], computerNumbers: [2, 3, 1], expected: ['3볼'] },
{ numbers: [1, 2, 3], computerNumbers: [1, 2, 3], expected: ['3스트라이크'] },
{ numbers: [4, 2, 1], computerNumbers: [4, 1, 2], expected: ['2볼', '1스트라이크'] },
];

test.each(cases)(
'스트라이크 개수 $strikeCount와 볼의 개수 $ballCount가 주어지는 경우, generateHintMessage()는 결과 메시지를 배열 형태의 $expected로 반환한다.',
({ numbers, computerNumbers, expected }) => {
// when
const hint = new Hint(numbers, computerNumbers);
const strikeCount = hint.calculateStrikeCount();
const ballCount = hint.calculateBallCount(strikeCount);
const hintMessage = hint.generateHintMessage(strikeCount, ballCount);

// then
expect(hintMessage).toEqual(expected);
},
);
});
});
40 changes: 40 additions & 0 deletions __tests__/validators/NumbersValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import ERROR from '../../src/constants/error.js';
import NumbersValidator from '../../src/validators/NumbersValidator.js';

describe('숫자 입력 예외 상황 테스트', () => {
const cases = [
{
input: '12',
description: '숫자가 3자리가 아닌 경우 예외처리를 한다.',
expected: ERROR.numbers.length,
},
{
input: 'asd',
description: '숫자가 아닌 경우 예외처리를 한다.',
expected: ERROR.numbers.notANumber,
},
{
input: '-123',
description: '숫자가 음수인 경우 예외처리를 한다.',
expected: ERROR.numbers.negative,
},
{
input: '122',
description: '숫자가 중복된 경우 예외처리를 한다.',
expected: ERROR.numbers.duplicated,
},
{
input: '',
description: '숫자를 입력하지 않을 경우 예외처리를 한다',
expected: ERROR.numbers.empty,
},
];

test.each(cases)('사용자 $input을 통해 에러를 반환한다.', ({ input, expected }) => {
// when
const result = () => NumbersValidator.validateNumbers(input);

// then
expect(result).toThrow(expected);
});
});
35 changes: 35 additions & 0 deletions __tests__/validators/RestartValidatorTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import ERROR from '../../src/constants/error.js';
import RestartValidator from '../../src/validators/RestartValidator.js';

describe('게임 재시작 여부 입력 예외 상황 테스트', () => {
const cases = [
{
input: '0',
description: '1, 2가 아닌 다른 값을 입력한 경우 예외처리를 한다.',
expected: ERROR.restart.choice,
},
{
input: '-1',
description: '값이 음수일 경우 예외처리를 한다.',
expected: ERROR.restart.negative,
},
{
input: 'asd',
description: '값이 숫자가 아닌 경우 예외처리를 한다.',
expected: ERROR.numbers.notANumber,
},
{
input: '',
description: '값을 입력하지 않았을 경우 예외처리를 한다.',
expected: ERROR.restart.empty,
},
];

test.each(cases)('게임 재시작 여부인 $input을 통해 에러를 반환한다.', ({ input, expected }) => {
// when
const result = () => RestartValidator.validateRestart(input);

// then
expect(result).toThrow(expected);
});
});
36 changes: 36 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## 📄 기능 목록

- 입력

- [x] 숫자를 입력받는 기능
- [x] 게임 재시작 여부를 입력받는 기능

- 출력

- [x] 게임시작 안내문을 출력하는 기능
- [x] 숫자 야구 게임 결과를 출력하는 기능
- [x] 성공 안내문을 출력하는 기능

- 기능

- [x] 입력한 숫자의 힌트를 제공하는 기능
- [x] 입력한 숫자의 볼의 개수를 반환하는 기능
- [x] 입력한 숫자의 스트라이크의 개수를 반환하는 기능
- [x] 볼, 스트라이크 결과 메시지를 반환하는 기능
- [x] 1~9까지의 3개의 숫자를 랜덤으로 반환하는 기능

## 🎯 예외 상황

- 숫자 입력

- [x] 3자리가 아닌 경우
- [x] 숫자가 아닌 경우
- [x] 음수일 경우
- [x] 중복일 경우
- [x] 입력하지 않았을 경우

- 재시작 여부 입력
- [x] 1, 2가 아닌 다른 값을 입력한 경우
- [x] 음수일 경우
- [x] 숫자가 아닌 경우
- [x] 입력하지 않았을 경우
8 changes: 7 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import BaseballGameController from './controller/BaseballGameController.js';

class App {
async play() {}
#baseballGameController = new BaseballGameController();

async play() {
await this.#baseballGameController.startGame();
}
}

export default App;
22 changes: 22 additions & 0 deletions src/constants/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const number = Object.freeze({
zero: 0,
numberSize: 3,
});

const range = Object.freeze({
from: 1,
to: 9,
});

const restart = Object.freeze({
start: 1,
exit: 2,
});

const CONSTANTS = Object.freeze({
number,
range,
restart,
});

export default CONSTANTS;
19 changes: 19 additions & 0 deletions src/constants/error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const numbers = Object.freeze({
length: '[ERROR] 입력하신 숫자가 3자리가 아닙니다.',
notANumber: '[ERROR] 숫자를 입력해주세요.',
negative: '[ERROR] 숫자가 음수입니다. 다시 입력해주세요.',
duplicated: '[ERROR] 숫자가 중복되었습니다. 다시 입력해주세요.',
empty: '[ERROR] 숫자를 입력해주세요.',
});

const restart = Object.freeze({
choice: '[ERROR] 1, 2가 아닌 다른 값을 입력하셨습니다.',
empty: '[ERROR] 게임 재시작 여부를 입력해주세요.'
});

const ERROR = Object.freeze({
numbers,
restart,
});

export default ERROR;
19 changes: 19 additions & 0 deletions src/constants/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const read = Object.freeze({
numbers: '숫자를 입력해주세요 : ',
restart: '게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\n',
});

const print = Object.freeze({
gameStart: '숫자 야구 게임을 시작합니다.',
ball: '볼',
strike: '스트라이크',
nothing: '낫싱',
endGame: '3개의 숫자를 모두 맞히셨습니다! 게임 종료',
});

const MESSAGE = Object.freeze({
read,
print,
});

export default MESSAGE;
48 changes: 48 additions & 0 deletions src/controller/BaseBallGameController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import BaseballGameService from '../service/BaseballGameService.js';
import InputView from '../view/InputView.js';
import OutputView from '../view/OutputView.js';

class BaseballGameController {
#baseballGameService;

constructor() {
this.#baseballGameService = new BaseballGameService();
}

async startGame() {
OutputView.printStartString();

return this.#inputUserNumbers();
}

async #inputUserNumbers() {
const numbers = await InputView.readNumbers();
const { strikeCount, hintMessage } = await this.#baseballGameService.baseballResult(numbers);

return this.#handleInputOrEnd(strikeCount, hintMessage);
}

#handleInputOrEnd(strikeCount, hintMessage) {
OutputView.printHintString(hintMessage);
if (this.#baseballGameService.isGameEnd(strikeCount)) {
OutputView.printEndString();

return this.#inputRestart();
}

return this.#inputUserNumbers();
}

async #inputRestart() {
const restart = await InputView.readRestart();
if (this.#baseballGameService.shouldRestart(restart)) {
this.#baseballGameService.resetGame();

return this.#inputUserNumbers();
}

return Promise.resolve();
}
}

export default BaseballGameController;
42 changes: 42 additions & 0 deletions src/domain/Hint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import CONSTANTS from '../constants/constants.js';
import MESSAGE from '../constants/message.js';

class Hint {
#numbers;

#computerNumbers;

constructor(numbers, computerNumbers) {
this.#numbers = numbers;
this.#computerNumbers = computerNumbers;
}

calculateStrikeCount() {
return this.#numbers.reduce(
(count, digit, index) => (digit === this.#computerNumbers[index] ? count + 1 : count),
0,
);
}

calculateBallCount(strikeCount) {
return (
this.#numbers.reduce(
(count, digit) => (this.#computerNumbers.includes(digit) ? count + 1 : count),
0,
) - strikeCount
);
}

generateHintMessage(strikeCount, ballCount) {
const hintMessage = [];
if (ballCount !== CONSTANTS.number.zero) hintMessage.push(`${ballCount}${MESSAGE.print.ball}`);
if (strikeCount !== CONSTANTS.number.zero)
hintMessage.push(`${strikeCount}${MESSAGE.print.strike}`);
if (ballCount === CONSTANTS.number.zero && strikeCount === CONSTANTS.number.zero)
hintMessage.push(MESSAGE.print.nothing);

return hintMessage;
}
}

export default Hint;
34 changes: 34 additions & 0 deletions src/service/BaseballGameService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import CONSTANTS from '../constants/constants.js';
import generateRandomNumbers from '../utils/generateRandomNumbers.js';
import Hint from '../domain/Hint.js';

class BaseballGameService {
#computerNumbers;

constructor() {
this.#computerNumbers = generateRandomNumbers(CONSTANTS.number.numberSize);
}

async baseballResult(numbers) {
const hint = new Hint(numbers, this.#computerNumbers);
const strikeCount = hint.calculateStrikeCount();
const ballCount = hint.calculateBallCount(strikeCount);
const hintMessage = hint.generateHintMessage(strikeCount, ballCount);

return { strikeCount, hintMessage };
}

isGameEnd(strikeCount) {
return strikeCount === CONSTANTS.number.numberSize;
}

shouldRestart(restart) {
return restart === CONSTANTS.restart.start;
}

resetGame() {
this.#computerNumbers = generateRandomNumbers(CONSTANTS.number.numberSize);
}
}

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

const generateRandomNumbers = length => {
const randomNumbers = [];
while (randomNumbers.length < length) {
const number = Random.pickNumberInRange(CONSTANTS.range.from, CONSTANTS.range.to);
if (!randomNumbers.includes(number)) randomNumbers.push(number);
}
return randomNumbers;
};

export default generateRandomNumbers;
Loading