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

[5기]3주차 Wordle 과제 제출 - kr #31

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5d78aa1
docs(README.md): 기능 목록 작성
ycheese Jun 9, 2024
0f37271
feat: 게임 시작 입출력 클래스 생성
ycheese Jun 9, 2024
152e102
feat: 오늘의 정답 제공자 클래스 생성
ycheese Jun 9, 2024
a2f92be
feat: 사용자 입력과 정답 비교 로직 추가
ycheese Jun 9, 2024
e7d01ae
refactor: tile enum 추가
ycheese Jun 9, 2024
a20cbf5
feat: 게임 라운드 클래스 생성
ycheese Jun 9, 2024
ca477cc
refactor: answer 클래스의 책임을 round 클래스로 분리
ycheese Jun 9, 2024
2249453
feat: 라운드 별 결과에 따라 반복 로직 추가
ycheese Jun 9, 2024
8f522ab
feat: 입력값 검증 로직 추가
ycheese Jun 9, 2024
278f963
test: 입력값 검증 테스트 추가
ycheese Jun 9, 2024
e29c493
feat: 게임 진행 클래스 생성
ycheese Jun 9, 2024
ec19ce4
feat: 라운드 결과 갱신/출력 로직 추가
ycheese Jun 9, 2024
6d576d0
refactor: 메시지 출력을 Console 클래스로 이동
ycheese Jun 9, 2024
9ca1628
feat: 이전 라운드 결과 저장 및 출력
ycheese Jun 9, 2024
c06266b
refactor: printResult() 인자를 RoundResult 에서 String 으로 변경
ycheese Jun 9, 2024
e945ca1
feat: 오늘 날짜에 따라 정답 단어를 읽어오는 로직 추가
ycheese Jun 9, 2024
5c2c650
feat: 입력한 단어의 파일 포함 여부 검증 로직 추가
ycheese Jun 15, 2024
1dffe39
fix: YELLOW 타일 판단 기준 로직 수정
ycheese Jun 15, 2024
751cb85
refactor: 중복 로직 제거 및 변수명 변경
ycheese Jun 15, 2024
aba19f3
refactor: 메소드명을 직관적으로 변경
ycheese Jun 15, 2024
3b862be
refactor: 메소드명을 직관적으로 변경
ycheese Jun 15, 2024
e3592ae
test: Round 클래스 테스트 추가
ycheese Jun 15, 2024
c1b4bb9
feat: 게임 관련 상수를 관리하는 클래스 생성
ycheese Jun 15, 2024
bb20d2e
refactor: 전역 상수를 WorldGameConfig 클래스로 분리
ycheese Jun 15, 2024
5af5e3a
test: RoundResult 클래스 테스트 추가
ycheese Jun 15, 2024
52db4de
Update: 사용자 입력 오류 시 다시 입력받게 수정
ycheese Jun 15, 2024
c1944e5
refactor: 입력값 검증 로직 InputValidator 클래스로 분리
ycheese Jun 15, 2024
386e974
refactor: 단어 파일 출력 로직 WordFileReader 클래스로 분리
ycheese Jun 15, 2024
f59a5ef
refactor: 입력값 검증 로직 InputValidator 클래스로 분리
ycheese Jun 15, 2024
5e87483
refactor: 입력값 검증 로직 조건별 메소드 분리
ycheese Jun 15, 2024
c69c28a
refactor: 입력값 검증 로직 조건별 메소드 분리
ycheese Jun 15, 2024
26942b5
test: InputValidator 클래스 테스트 추가
ycheese Jun 15, 2024
d3e012a
refactor: 정답인지 검증하는 로직 가독성 개선
ycheese Jun 16, 2024
c7310bb
refactor: 코드 컨벤션 적용
ycheese Jun 16, 2024
a0e0221
test: WordFileReader 클래스 테스트 추가
ycheese Jun 16, 2024
8e0fcb2
refactor: 클래스 패키지 분리
ycheese Jun 16, 2024
f7ee01e
refactor: 여백 최적화
ycheese Jun 16, 2024
02fd48e
docs: README.md 수정
ycheese Jun 16, 2024
c7acade
refactor: words.txt의 절대경로로 Path를 사용하여 발생가능한 exception 최소화
ckr3453 Jun 16, 2024
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
118 changes: 30 additions & 88 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,31 @@
# 미션 - 워들

## 🔍 진행 방식

- 미션은 **과제 진행 요구 사항**, **기능 요구 사항**, **프로그래밍 요구 사항** 세 가지로 구성되어 있다.
- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
- **기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.**

---

## 🚀 기능 요구 사항

선풍적인 인기를 끌었던 영어 단어 맞추기 게임이다.

- 6x5 격자를 통해서 5글자 단어를 6번 만에 추측한다.
- 플레이어가 답안을 제출하면 프로그램이 정답과 제출된 단어의 각 알파벳 종류와 위치를 비교해 판별한다.
- 판별 결과는 흰색의 타일이 세 가지 색(초록색/노란색/회색) 중 하나로 바뀌면서 표현된다.
- 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.
- 정답과 답안은 `words.txt`에 존재하는 단어여야 한다.
- 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.

### 입출력 요구 사항

#### 실행 결과 예시

```
WORDLE을 6번 만에 맞춰 보세요.
시도의 결과는 타일의 색 변화로 나타납니다.
정답을 입력해 주세요.
hello

⬜⬜🟨🟩⬜

정답을 입력해 주세요.
label

⬜⬜🟨🟩⬜
🟨⬜⬜⬜🟩

정답을 입력해 주세요.
spell

⬜⬜🟨🟩⬜
🟨⬜⬜⬜🟩
🟩🟩⬜🟩🟩

정답을 입력해 주세요.
spill

4/6

⬜⬜🟨🟩⬜
🟨⬜⬜⬜🟩
🟩🟩⬜🟩🟩
🟩🟩🟩🟩🟩
```

---

## 🎯 프로그래밍 요구 사항

- JDK 21 버전에서 실행 가능해야 한다.
- 프로그램 실행의 시작점은 `Application`의 `main()`이다.
- `build.gradle` 파일은 변경할 수 없으며, **제공된 라이브러리 이외의 외부 라이브러리는 사용하지 않는다.**
- 프로그램 종료 시 `System.exit()`를 호출하지 않는다.
- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 등의 이름을 바꾸거나 이동하지 않는다.
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 기본적으로 [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)를 원칙으로 한다.
- 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 3항 연산자를 쓰지 않는다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- JUnit 5와 AssertJ를 이용하여 정리한 기능 목록이 정상적으로 작동하는지 테스트 코드로 확인한다.
- 테스트 도구 사용법이 익숙하지 않다면 아래 문서를 참고하여 학습한 후 테스트를 구현한다.
- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide)
- [AssertJ User Guide](https://assertj.github.io/doc)
- [AssertJ Exception Assertions](https://www.baeldung.com/assertj-exception-assertion)
- [Guide to JUnit 5 Parameterized Tests](https://www.baeldung.com/parameterized-tests-junit-5)
- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
- else 예약어를 쓰지 않는다.
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
- 힌트: MVC 패턴 기반으로 구현한 후, View와 Controller를 제외한 Model에 대한 단위 테스트 추가에 집중한다.
## 페어
- 치즈
- kr

## 기능 목록
### 입출력
- [X] 게임 시작 메세지를 콘솔에 출력한다.
- [X] 플레이어는 한 번의 입력마다 5자의 영문을 입력한다.
- [X] 플레이어가 입력한 결과에 따라 문자별로 5개의 색상으로 표현한다.
- ⬜️ 회색: 정답에 존재하지 않는 문자
- 🟨 노랑색: 정답 내에 존재하는 문자이나, 위치가 다름
- 🟩 초록색: 정답에 해당하는 문자
- [X] 표현한 색상을 직전 결과에 스택 형식으로 쌓는다.
### 게임 진행
- [X] 플레이어의 턴(입력) 횟수는 6회로 제한한다.
- [X] 정답을 생성한다.
- [X] 답안과 정답이 같은지 비교한다.
- [X] 주어진 입력 횟수 내에 정답을 맞추면 게임을 종료한다.
- [X] 정답을 맞추지 못하면 횟수와 콘솔 내용을 초기화하고 재시작한다.
### 파일
- [X] 정답은 words.txt 에서 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어를 가져온다.
- [X] 플레이어의 답안이 words.txt 에 존재하는지 확인한다.

## 테스트 시나리오
- [X] 사용자의 입력이 5자의 영문이 아니면 예외가 발생한다.
- [X] 오늘의 정답 단어 인덱스는 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 이다.
- [X] 사용자의 입력이 words.txt 에 존재하지 않으면 예외가 발생한다.
- [X] 문자와 위치가 일치하면 GREEN 상태를 반환한다.
- [X] 문자는 일치하지만 위치가 다르면 YELLOW 상태를 반환한다.
- [X] 문자가 일치하지 않으면 GRAY 상태를 반환한다.
Comment on lines +25 to +31
Copy link

Choose a reason for hiding this comment

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

테스트 시나리오를 미리 작성하셨군요 !! 👍

8 changes: 8 additions & 0 deletions src/main/java/kr/co/wordle/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package kr.co.wordle;

public class Application {
public static void main(String[] args) {
WordleGame wordleGame = new WordleGame();
wordleGame.start();
}
}
46 changes: 46 additions & 0 deletions src/main/java/kr/co/wordle/WordleGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package kr.co.wordle;

import kr.co.wordle.domain.Answer;
import kr.co.wordle.domain.Round;
import kr.co.wordle.view.Console;

import static kr.co.wordle.config.WordleGameConfig.MAX_ROUND;

public class WordleGame {
private final Console console;
private final Answer answer;
private final StringBuilder roundResults;

private int currentRound;

public WordleGame() {
this.console = new Console();
this.answer = new Answer();
this.roundResults = new StringBuilder();
this.currentRound = 0;
Copy link

Choose a reason for hiding this comment

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

매직넘버는 상수로 빼면 어떨까요~? 또는 Config로 게임관련된 설정을 관리하고 있는 것 같은데 요런 부분도 한곳에 모아두면 좋을 것같네요!

}

public void start() {
console.init();
Round round = null;
while (isRoundInProgress(round, roundResults)) {
currentRound++;
String input = console.userInput();
round = new Round(input);
roundResults.append(round.roundResult(answer)).append("\n");
Copy link

Choose a reason for hiding this comment

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

"\n"를 더해주는건 UI적 요소인거같은데 Console이 책임을 갖는게 더 좋지않을까 싶은데 어떻게 생각하시나요?🙂

console.printRoundResult(roundResults);
}
}

private boolean isRoundInProgress(Round round, StringBuilder roundResults) {
if (round == null) {
return true;
}
if (round.isFinished()) {
console.printRound(currentRound, MAX_ROUND);
console.printRoundResult(roundResults);
return false;
}
return currentRound <= MAX_ROUND;
}
}
10 changes: 10 additions & 0 deletions src/main/java/kr/co/wordle/config/WordleGameConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.co.wordle.config;

public class WordleGameConfig {
public static final String WORDS_FILE_PATH = "src/main/resources/words.txt";
public static final int WORD_LENGTH = 5;
public static final int MAX_ROUND = 6;

private WordleGameConfig() {
}
}
35 changes: 35 additions & 0 deletions src/main/java/kr/co/wordle/domain/Answer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.co.wordle.domain;

import kr.co.wordle.domain.provider.AnswerProvider;

public class Answer {

private final String value;
private final int[] countPerCharacter = new int[26];

public Answer() {
this.value = AnswerProvider.todayAnswer();
countPerCharacter();
}

protected Answer(String value) {
this.value = value;
countPerCharacter();
}
Comment on lines +15 to +18
Copy link

Choose a reason for hiding this comment

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

테스트코드에서만 사용되는 생성자이기때문에 protected로 정의한걸까용?


private void countPerCharacter() {
char[] chars = value.toCharArray();
for (char ch : chars) {
countPerCharacter[ch - 'a']++;
}
}

public int[] getCountPerCharacter() {
return countPerCharacter;
}

public char charAt(int index) {
return value.charAt(index);
}
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

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

String처럼 charAt을 구현해주었군요!!


}
45 changes: 45 additions & 0 deletions src/main/java/kr/co/wordle/domain/Round.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.co.wordle.domain;

import kr.co.wordle.support.InputValidator;
import java.util.Arrays;

import static kr.co.wordle.config.WordleGameConfig.WORD_LENGTH;

public class Round {

private final String input;
private final RoundResult roundResult;

public Round(String input) {
if (InputValidator.isNotValid(input)) {
throw new IllegalArgumentException();
}
Comment on lines +14 to +16
Copy link

Choose a reason for hiding this comment

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

input에 대한 검증 책임을 InputValidator라는 유틸성 클래스에게 주고있는데, VO(값객체)를 생성하여 스스로 검증할 수 있도록 만들어주는건 어떨까요? 그러면 더욱 응집도가 높아질거같아요~ 그리고 IllegalArgumentException 을 공통적으로 던지는것 같은데 custom Exception을 통해 케이스를 좀 나눠보는건 어떨까요??

this.input = input;
this.roundResult = new RoundResult();
}

public String roundResult(Answer answer) {
char[] inputChars = input.toCharArray();
int[] countPerCharacter = Arrays.copyOf(answer.getCountPerCharacter(), 26);
for (int i = 0; i< WORD_LENGTH; i++) {
Tile key = getTile(countPerCharacter, answer.charAt(i), inputChars[i]);
roundResult.update(key);
}
return roundResult.toString();
}

public Tile getTile(int[] counts, char source, char target) {
if(counts[target - 'a'] == 0) {
return Tile.GRAY;
}
if(source != target) {
return Tile.YELLOW;
}
counts[target - 'a']--;
return Tile.GREEN;
}

public boolean isFinished() {
return roundResult.isAllGreen();
}
}
30 changes: 30 additions & 0 deletions src/main/java/kr/co/wordle/domain/RoundResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package kr.co.wordle.domain;

import java.util.EnumMap;
import java.util.Map;

import static kr.co.wordle.config.WordleGameConfig.WORD_LENGTH;

public class RoundResult {
private final Map<Tile, Integer> countPerTile;
private final StringBuilder result;
Comment on lines +9 to +10
Copy link

Choose a reason for hiding this comment

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

라운드별 결과를 이중으로 담고있는 것으로 보여요.

countPerTile은 타일의 갯수
result는 UI에 출력할 타일의 문자열같은데 책임을 조금 분리해보는 것이 어떨까요?


public RoundResult() {
this.countPerTile = new EnumMap<>(Tile.class);
Copy link

Choose a reason for hiding this comment

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

EnumMpa의 활용 너무 좋습니다~

result = new StringBuilder();
}

public void update(Tile tile) {
countPerTile.compute(tile, (k, v) -> (v == null) ? 1 : v + 1);
result.append(tile);
}

public boolean isAllGreen() {
return countPerTile.getOrDefault(Tile.GREEN, 0) == WORD_LENGTH;
}

@Override
public String toString() {
return result.toString();
}
Comment on lines +26 to +29
Copy link

Choose a reason for hiding this comment

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

toString을 Tile과 RoundResult에 정의해주셨는데 이부분도 UI와의 결합이 높아보여요~ 😢

}
18 changes: 18 additions & 0 deletions src/main/java/kr/co/wordle/domain/Tile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kr.co.wordle.domain;

public enum Tile {
GREEN("🟩"),
YELLOW("🟨"),
GRAY("⬜️");
Comment on lines +3 to +6
Copy link

Choose a reason for hiding this comment

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

게임 결과를 Enum으로 정의하셨네요~~ 다형성을 활용하는 것 너무 좋습니다.
하지만 위에서 말씀드린것처럼 UI와의 결합이 높아보여요!


private final String text;

Tile(String text) {
this.text = text;
}

@Override
public String toString() {
return text;
}
}
27 changes: 27 additions & 0 deletions src/main/java/kr/co/wordle/domain/provider/AnswerProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kr.co.wordle.domain.provider;

import kr.co.wordle.support.WordFileReader;

import java.time.LocalDate;
import java.time.Period;
import java.util.List;

public class AnswerProvider {

private static final LocalDate REFERENCE_DATE = LocalDate.of(2021, 6, 19);

private AnswerProvider() {
}

public static String todayAnswer() {
List<String> words = WordFileReader.readWordsInFile();
Copy link

Choose a reason for hiding this comment

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

파일에서 Word를 읽는 작업이 AnswerProvider에서도 InputValidator에서도 총 두번 일어나는 것으로 보여요. 이를 직접 호출하기보다는 외부에서 주입을 받는 것이 어떨까요?

int dayDiff = dayDiff();
return words.get(dayDiff % words.size());
}

private static int dayDiff() {
LocalDate currentDate = LocalDate.now();
Period period = Period.between(REFERENCE_DATE, currentDate);
return period.getDays();
}
}
34 changes: 34 additions & 0 deletions src/main/java/kr/co/wordle/support/InputValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.co.wordle.support;

import java.util.List;

import static kr.co.wordle.config.WordleGameConfig.WORD_LENGTH;

public class InputValidator {

private static final List<String> words = WordFileReader.readWordsInFile();

private InputValidator() {
}

public static boolean isNotValid(String input) {
if (input == null) {
return true;
}
if (input.length() != WORD_LENGTH) {
return true;
}
if (isNotAllAlphabet(input)) {
return true;
}
return isNotInWords(input);
}

private static boolean isNotAllAlphabet(String input) {
return input.chars().anyMatch(ch -> ch < 'a' || ch > 'z');
Copy link

Choose a reason for hiding this comment

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

'a', 'z'도 상수로 빼보는게 어떨까요~

}

private static boolean isNotInWords(String input) {
return !words.contains(input);
}
Comment on lines +31 to +33
Copy link

Choose a reason for hiding this comment

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

메서드명에 부정어구를 넣는것 보다 긍정어구를 넣는게 가독성이 더 좋습니다!
https://latte1114.tistory.com/519

}
Loading