-
Notifications
You must be signed in to change notification settings - Fork 43
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
base: main
Are you sure you want to change the base?
Changes from all commits
5d78aa1
0f37271
152e102
a2f92be
e7d01ae
a20cbf5
ca477cc
2249453
8f522ab
278f963
e29c493
ec19ce4
6d576d0
9ca1628
c06266b
e945ca1
5c2c650
1dffe39
751cb85
aba19f3
3b862be
e3592ae
c1b4bb9
bb20d2e
5af5e3a
52db4de
c1944e5
386e974
f59a5ef
5e87483
c69c28a
26942b5
d3e012a
c7310bb
a0e0221
8e0fcb2
f7ee01e
02fd48e
c7acade
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 상태를 반환한다. | ||
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(); | ||
} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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() { | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. String처럼 charAt을 구현해주었군요!! |
||
|
||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 라운드별 결과를 이중으로 담고있는 것으로 보여요. countPerTile은 타일의 갯수 |
||
|
||
public RoundResult() { | ||
this.countPerTile = new EnumMap<>(Tile.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. toString을 Tile과 RoundResult에 정의해주셨는데 이부분도 UI와의 결합이 높아보여요~ 😢 |
||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 게임 결과를 Enum으로 정의하셨네요~~ 다형성을 활용하는 것 너무 좋습니다. |
||
|
||
private final String text; | ||
|
||
Tile(String text) { | ||
this.text = text; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return text; | ||
} | ||
} |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메서드명에 부정어구를 넣는것 보다 긍정어구를 넣는게 가독성이 더 좋습니다! |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 시나리오를 미리 작성하셨군요 !! 👍