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 과제 제출 - 점프 #34

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
da90527
feat: 정답단어 생성
who-is-hu Jun 11, 2024
a1ad7ff
feat: 단어 목록 읽기
who-is-hu Jun 11, 2024
ca9d324
feat: 시작 안내 텍스트 컴포넌트
who-is-hu Jun 11, 2024
c0b32cb
feat: 사용자 입력 단어 생성
who-is-hu Jun 11, 2024
be1f118
feat: 비교 로직 수정
who-is-hu Jun 11, 2024
3012f26
test: 비교 로직 테스트
who-is-hu Jun 11, 2024
450e186
fix: isCorrent 로직 수정
who-is-hu Jun 13, 2024
76a9bbe
refactor: 타일문자열 변수명 변경
who-is-hu Jun 13, 2024
1350d00
feat: 입력후 출력까지 로직 1회 구현
who-is-hu Jun 13, 2024
82b774b
feat: 정답이면 라운드 출력후 게임종료
who-is-hu Jun 13, 2024
6077df0
feat: 6회 반복 루프 돌기
who-is-hu Jun 13, 2024
b978226
feat: 단어 글자수 제한 검증
who-is-hu Jun 16, 2024
83d0f29
feat: 영어단어만 입력하는지 검증
who-is-hu Jun 16, 2024
4ccbeb9
feat: 단어 생성시 에러나면 다시 시도하게
who-is-hu Jun 16, 2024
29474bf
refactor: MatchResult에 사용하지않는 inputChar 제거
who-is-hu Jun 16, 2024
e1c13e9
remove: 불필요한 테스트 제거
who-is-hu Jun 16, 2024
13ece19
refactor(test): 테스트 결과 확인하기 쉽게 수정
who-is-hu Jun 16, 2024
961f2b6
chore: 테스트 이름 변경
who-is-hu Jun 16, 2024
4ed2af8
refactor: InputWord와 AnswerWord를 합침
who-is-hu Jun 16, 2024
d1dcaea
refactor: Word의 match 함수안 depth 제거
who-is-hu Jun 16, 2024
ebcec6b
docs: readme 에 리팩터링한것 목록 반영
who-is-hu Jun 16, 2024
a004c58
refactor: final 할수있는것 달아주기
who-is-hu Jun 16, 2024
3dc51ba
refactor: 생성자에 tile 받는 매개변수 이름 변경
who-is-hu Jun 16, 2024
cc4f992
remove: 사용하지 않는 함수 제거
who-is-hu Jun 16, 2024
126c6c8
feat: MatchResult equals 다른클래스는 false 반환하게
who-is-hu Jun 16, 2024
f2b6817
refactor: 불필요한 getCharBy 제거
who-is-hu Jun 16, 2024
832b82a
refactor: 필드에 final 달아주기
who-is-hu Jun 16, 2024
db374da
refactor: Round 생성시 현재라운드는 1로 고정
who-is-hu Jun 16, 2024
e91405c
refactor: 라운드 제한 주입받아서 출력하게 변경
who-is-hu Jun 16, 2024
e15dd60
refactor: 변수 직접 사용하지않고 getter/setter 사용
who-is-hu Jun 16, 2024
dc59ce2
refactor: 변수명 단순하게 변경
who-is-hu Jun 16, 2024
7079129
chore: 불필요 공백 제거
who-is-hu Jun 16, 2024
43b3e08
doc: readme 원래있던 내용 제거
who-is-hu Jun 17, 2024
59449a3
fix(test): 테스트 케이스 깨지는것 수정
who-is-hu Jun 26, 2024
11352b8
fix: 입력단어의 같은글자가 여러번 중복될때 고려못한 케이스들 수정
who-is-hu Jun 26, 2024
01ecb31
refactor: isCorrect 스태틱 제거
who-is-hu Jun 26, 2024
fc9ef84
fix: typo
who-is-hu Jun 26, 2024
9685a01
refactor: 게임 종료 판단 로직을 MatchResult에 위임
who-is-hu Jun 26, 2024
ee43ca3
refactor: MatchResult 하는일이 많아져서 그럴싸한 이름
who-is-hu Jun 26, 2024
d086b9a
fix: 최대라운드 오타 수정
who-is-hu Jun 27, 2024
0b8b734
refactor: 단어가될 문자열 가져온다는 네이밍
who-is-hu Jun 27, 2024
84eb915
refactor: 정답단어 생성 책임을 WordDictionary로 옮기고 Word의 생성책임 줄임
who-is-hu Jun 27, 2024
86f8ec2
refactor(test): 불러온 문자열 내용까지 검증
who-is-hu Jun 27, 2024
d259f53
refactor(test): 테스트 이름 변경
who-is-hu Jun 27, 2024
c18b653
refactor: word.isCorrect는 어감이 이상해서 이름 변경
who-is-hu Jun 27, 2024
a55d0ff
refactor: 입력단어 유효성 검증 커스텀 에러 사용
who-is-hu Jun 27, 2024
02ba623
feat: 입력단어 사전에 있는지 검사 빼먹은것 추가
who-is-hu Jun 27, 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
117 changes: 31 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,34 @@
# 미션 - 워들

## 🔍 진행 방식
### 구현 목록
- 단어 불러오기
- 매일 바뀌는 정답단어 선택
- 단어 입력
- 입력시 단어목록에 있나 검증
- 비교 로직
- 같을때, 있을떄, 없을때 따른 비교결과
- 게임로직
- 6회 초과시 게임오버
- 정답시 게임클리어
- ui
- 비교 결과, 라운드 출력

### 리팩터링 포인트
- [x] final 붙혀주기
- [x] Inputword 에 availableWords 상태로 가지고 있을 필요는 없다.
- [x] InputWord와 Answer를 합치기?
- [x] match 에 for 문안 함수로 발라내기
- [x] isEndGame 플래그 변수 네이밍 변경
- [x] 검증
- [x] 5글자 검증
- [x] 영단어 검증
- [x] 예외 try catch
- [x] GameManager에 있는 List<MatchResults> 의 변수명 변경
- [x] HintView에 3depth 줄이기
- [x] InputWord의 마지막 테스트함수 이름
- [x] MatchResult에 inputChar가 쓰이지 않고 있어서 클래스 자체를 지우고 그냥 Hint만 쓰기
- ~~[ ] 구조 변경~~
- ~~[ ] GameManager의 의존성 줄여두기~~
- ~~[ ] String 말고 char 배열로 쓸지? -> 좀 더 고민해보기~~
- ~~[ ] 분리된 view 부분 합칠지?~~

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

---

## 🚀 기능 요구 사항

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

- 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에 대한 단위 테스트 추가에 집중한다.
14 changes: 14 additions & 0 deletions src/main/java/WordleApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import config.FileConfig;
import controller.GameManager;
import infra.WordStringLoader;

import java.util.List;

public class WordleApplication {
public static void main(String[] args) {
List<String> wordStringList = WordStringLoader.readAll(FileConfig.FILE_PATH);
GameManager gameManager = new GameManager(wordStringList);

gameManager.start();
}
}
5 changes: 5 additions & 0 deletions src/main/java/config/FileConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package config;

public class FileConfig {
public final static String FILE_PATH = "src/main/resources/words.txt";
}
Comment on lines +3 to +5

Choose a reason for hiding this comment

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

FILE_PATH 를 config 로 관리하셨군요 👍

61 changes: 61 additions & 0 deletions src/main/java/controller/GameManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package controller;

import domain.*;
import ui.*;

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

public class GameManager {
private final GameState state;
private final GuideTextView guideTextView;
private final InputView inputView;
private final HintView hintView;
private final RoundView roundView;
private final AnswerView answerView;
private final WordDictionary wordDictionary;

public GameManager(List<String> wordStringList) {
this.wordDictionary = new WordDictionary(wordStringList);
this.state = new GameState();
this.guideTextView = new GuideTextView();
this.inputView = new InputView();
this.hintView = new HintView();
this.roundView = new RoundView();
Comment on lines +21 to +24

Choose a reason for hiding this comment

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

View 관련을 묶을 수 있다면 묶어진 객체가 하나 더 나와도 좋을 것 같아요 👍

Copy link
Author

Choose a reason for hiding this comment

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

저는 이 컴포넌트들이 각자 연관성없이 바뀔거라 생각해서 클래스를 다 나누어봤는데요
혹시 묶는 이유는 어떤걸까요??

Choose a reason for hiding this comment

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

음 단순히 코드를 읽는 독자 입장에서, View 라는 suffix 가 있으니 연관성이 있다고 느껴져요. 🤔

점프님은 GameManager 가 너무 많은 객체에 대해 알고 있으면 어떤 단점이 있다고 생각하시나요?

Copy link
Author

Choose a reason for hiding this comment

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

저는 웹 프런트 개발할때 생각하면서 각자 하나의 컴포넌트쩌럼 써봤는데요
사실 묶어서 파라미터같은걸로 넘겨주거나 해야하는게 아니면 어떤 단점이 있는지 잘 모르겠어요!
여러개 묶은 객체에 의존해도 뷰에 변경이 일어날때 수정할때 공수는 비슷할것같은데
제가 모르는 개념같은게 있을까요? 한 수 가르침주십쇼

Choose a reason for hiding this comment

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

하나의 컴포넌트 개념으로 구성하신 점은 너무 훌륭하네요 👍
저는 이렇게 생각해보았어요!

Controller 개념인 GameManager 입장에서는 어떤 View 를 쓸지 모르고, 그냥 View 라는 통로만 바라보고,
View 의 구현체가 알아서 어떻게 보여줄지만 정하면 View 가 바뀔때 GameManager 가 안바뀔 수 있을 것 같아요 😄

말씀하신대로 View 의 구현체가 하나의 컴포넌트처럼 작동하게끔 만들수도 있고, 더 나아가 콘솔에서 웹으로 바꿔진다 했을때, HTML 을 내려주는 방식으로 바꿀때 GameManager 의 변경을 최소화 할 수 있지 않을까 생각해요!

제 개인적 관점이니 혹시 다른 의견이 있으시거나 설명이 부족하다면 얘기를 나눠보면 좋을 것 같아요 👍

this.answerView = new AnswerView();
}

public void start() {
Word answer = wordDictionary.answerWord(LocalDate.now());
guideTextView.render(Round.ROUND_LIMIT);
while (state.shouldContinueGame()) {
startRound(answer);
}
if (state.isNotWinning()) {
answerView.render(answer);
}
}

private void startRound(Word answer) {
String input = inputView.input();
Word inputWord;
try {
inputWord = new Word(input);
} catch (IllegalArgumentException e) {

Choose a reason for hiding this comment

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

IllegalArgumentException 을 catch 하고 있네요 👀 createInput 메서드가 IllegalArgumentException 을 던지는 케이스 일 경우 재시도를 하는 로직인 것 같은데, CustomException 을 만들어서 처리해보는건 어떨까요? :)

Copy link
Author

Choose a reason for hiding this comment

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

좋습니다~

System.out.println(e.getMessage());
return;
}
if (wordDictionary.hasNot(inputWord)) {
System.out.println("사전에 없는 단어입니다.");
return;
Comment on lines +45 to +50
Copy link

@jongmin4943 jongmin4943 Jun 29, 2024

Choose a reason for hiding this comment

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

CustomException 들이 단순히 RuntimeException 을 상속받고있어서, 잘못 입력되는 경우 그냥 게임이 종료 되고 있어요! catch 부분이 변경 되어야 할 것 같아요 😢

또한, View 를 따로 분리하셨으니 여기서 출력을 하는것 보단 책임을 넘겨보는건 어떨까요? 👍

}

MatchResult matchResult = answer.match(inputWord);
if (matchResult.isWinning()) {
roundView.render(state.currentRound(), Round.ROUND_LIMIT);
}
state.add(matchResult);

hintView.render(state);
}
}
40 changes: 40 additions & 0 deletions src/main/java/domain/GameState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package domain;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GameState implements Iterable<MatchResult> {
private final Round round = new Round();
private final List<MatchResult> matchResults;

public GameState() {
this.matchResults = new ArrayList<>();
}

@Override
public Iterator<MatchResult> iterator() {
return matchResults.iterator();
}

public void add(MatchResult matchResult) {
matchResults.add(matchResult);
round.goNext();
}

public boolean shouldContinueGame() {
return round.isNotFinalRound() && isNotWinning();
}
Comment on lines +20 to +27

Choose a reason for hiding this comment

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

해당 부분의 대한 테스트 코드도 있으면 좋을 것 같아요 👍


public int currentRound() {
return round.getCurrent();
}

public boolean isWinning() {
return matchResults.stream().anyMatch(MatchResult::isWinning);
}
Comment on lines +33 to +35

Choose a reason for hiding this comment

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

matchResults 를 순회하며 전부 CORRECT 인 것을 찾고 있네요!
현재 요구사항이라면, 전부 순회하는 것이 아닌 마지막으로 들어온 MatchResult 가 전부 CORRECT 인것을 찾는게 좀 더 효율적이지 않을까요? :)


public boolean isNotWinning() {
return !isWinning();
}
}
22 changes: 22 additions & 0 deletions src/main/java/domain/Hint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package domain;

public enum Hint {

CORRECT("🟩"), //\uD83D\\uDFE9
EXIST("🟨"), //\uD83D\\uDFE8
NOT_EXIST("⬜");

private final String tile;
Comment on lines +3 to +9

Choose a reason for hiding this comment

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

게임의 결과를 보여줄 타일들을 Enum 타입으로 정의 해놓으셨네요 👍

개인적으로 🟩 와 같은 타일의 모양들은 view 의 영역이라 생각되어서 도메인 영역과 분리되면 좋을 것 같다 생각하는데 어떻게 생각하시나요? 😃

Copy link
Author

Choose a reason for hiding this comment

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

장단점이 있는것같아요
저도 분리파였는데 Hint 종류가 늘어났을때 view에 반영이 안될수 있는게 더 손해인것같아서 설득당했어요
미리 누락을 방지할만한 방법이 있을까요?

Choose a reason for hiding this comment

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

맞습니다. 장단점이 분명 존재하죠 👍

이런식의 접근은 어떨까요?

  • Hint 의 종류가 늘어났을때, view 와 분리가 되어 있다면, view 도 추가 작업이 생기는 것이 맞는 순환이라 생각해요.
  • view 가 웹으로 분리된다고 가정하면, 타일의 모양은 css 나 html 로 처리될 가능성이 크다고 생각해요.

점프님 생각은 어떠신가요? 😃

Copy link
Author

Choose a reason for hiding this comment

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

와잼님 말이 백번 맞지만 실제로 누락을 자주 봐서 공감이 갔달까요..ㅋㅋ
enum 같은거 서버에만 추가되서 클라쪽은 깨지거나 기본값으로 나오거나 하는..

Choose a reason for hiding this comment

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

그럴수도 있겠네요! 😮 저는 Front-Back 작업을 둘 다 하다보니 둘을 최대한 분리하는 관점으로 많이 생각하게 되는 것 같아요. 두 팀이 나눠져 있는데 prod 에 나갈때까지 누락 되는 부분이 있다면, 협업관점에서 문제가 있는 부분이 아닐까 라는 생각도 듭니다 🤔

한쪽에서만 관리하면 좋긴 하겠지만 상황에 맞춰 적절하게 선택을 하면 좋을 것 같습니다! 👍


Hint(String tile) {
this.tile = tile;
}

public String getTile() {
return tile;
}

public boolean isCorrect() {
return this == CORRECT;
}
}
39 changes: 39 additions & 0 deletions src/main/java/domain/MatchResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package domain;

import java.util.Iterator;
import java.util.List;

public class MatchResult implements Iterable<Hint>{
private final List<Hint> hints;

public MatchResult(List<Hint> hints) {
this.hints = hints;
}
Comment on lines +6 to +11

Choose a reason for hiding this comment

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

Hint 들을 모아두는 일급컬렉션을 사용하셨군요! 💯 💯


@Override
public Iterator<Hint> iterator() {
return hints.iterator();
}

public boolean isWinning() {
return hints.stream().allMatch(Hint::isCorrect);
}

public void add(Hint hint) {
hints.add(hint);
}

@Override
public int hashCode() {
return super.hashCode();
}

@Override
public boolean equals(Object obj) {
if(obj == null) return false;
if(obj.getClass() != this.getClass()) return false;
return this.hints.equals(((MatchResult) obj).hints);
}
}


23 changes: 23 additions & 0 deletions src/main/java/domain/Round.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package domain;
public class Round {
public final static int ROUND_LIMIT = 6;
private final int limit;
private int current;

public Round() {
limit = ROUND_LIMIT;
current = 1;
}
Comment on lines +2 to +10

Choose a reason for hiding this comment

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

Round 클래스에서 limit 를 받던 것을 제거하셨네요 :)

생성 시 상수로 가지고 있는 ROUND_LIMIT 을 final 변수에 할당하고 사용되고 있는데, 변수로 담아서 쓰는 이유가 명확하지 않다고 느껴져요 🤔

어떤 의도로 변수에 담아서 사용하시는 걸까요?


public void goNext() {
this.current++;
}

public int getCurrent() {
return current;
}

public boolean isNotFinalRound() {
return current <= limit;
}
}
Loading