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 과제 제출 - 얄뭉 #37

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
08a59e6
feat(domain): 기초 도메인 설계
Jun 11, 2024
b8a2253
feat(WordleApplication): GameManager 생성자 추가 및 일별 단어 선정 구현
Jun 11, 2024
e640bb7
feat: domain 병합
Jun 13, 2024
cd10e1b
test: domain 병합
Jun 13, 2024
41c538d
chore: Scacnner추가
Jun 13, 2024
18beffc
chore: 명시적인 변수명으로 변경
Jun 16, 2024
8534b1a
fix: 중복 단어가 있을 때 첫번째 idx를 리턴하는 오류오 인덱스의 단어 비교로 수정
Jun 16, 2024
f43ebb4
feat: 결과 출력을 위한 Iterator 상속 및 구현
Jun 16, 2024
f1d4626
chore: 불필요한 클래스 제거
Jun 16, 2024
3d86ff7
feat: 성공 시 라운드 노출을 위한 메소드 추가
Jun 16, 2024
bdc2094
feat: 메세지 노출 view 기능 구현
Jun 16, 2024
2b28b8d
feat: 게임 기능 구현
Jun 16, 2024
77f0923
refact: 힌트 노출 뎁스 메소드로 추출
Jun 16, 2024
1d5c4c7
refactor: Answer, InputWord를 같은 속성인 Word로 통합
Jun 16, 2024
babbf2c
refactor: MatchResults와 MatchResult를 일급컬랙션으로 변경
Jun 16, 2024
f01a856
refactor: 변경 사항에 맞게 게임 시작 기능 수정
Jun 16, 2024
aab91e4
refactor(test): 변경된 구조에 맞게 test 로직 변경
Jun 16, 2024
106c07d
chore: String을 Intstream으로 hint 결과값을 바로 리턴하도록 수정
Jun 16, 2024
e6da42b
chore: 불필요한 메서드 정리
Jun 16, 2024
62efc46
doc: README.md 구현목록 및 리펙터링 포인트 추가
Jun 17, 2024
a986ed0
chore: 상수가 아닌 변수 이름 lowerCamelCase변경
Jun 28, 2024
21c735d
chore: 오타 수정
Jun 28, 2024
f9a565b
chore: 기본 생성자 protected 변경
Jun 28, 2024
1b8bf73
fix: 정답 확인 비교 로직 수정
Jun 28, 2024
f116a69
chore: 원시타입 상수는 HigherCamelCase
Jun 28, 2024
ae34f09
refact: Stream.toList로 변경
Jun 28, 2024
d45e4de
refact: 일치하는 단어가 있을 경우 다른 위치는 NOT_EXIST 처리를 위한 클래스 추가
Jun 28, 2024
5a536b9
refact: HintLetter 추가로 리펙토링
Jun 28, 2024
1e02fe6
fix: HintLetter 추가로 정답 로직 변경
Jun 28, 2024
fe0cbb2
refact: 불필요한 메소드 삭제
Jun 28, 2024
b443f7b
refact: 불필요한 메소드 삭제
Jun 28, 2024
fa4d0ff
refact: 불필요한 엔터 삭제
Jun 28, 2024
62e8b94
fix: 입력 단어 유효성 오류 시 게임 종료하지 않고, 재시도
Jun 28, 2024
b056160
refact: 마지막 시도까지 실패 시 문구 노출 후 종료
Jun 28, 2024
42bcfe6
refactor: 기본 생성자 추가
Jun 28, 2024
486d28d
refactor: 상수 변경
Jun 28, 2024
83010de
refactor: 불필요한 공백 제거
Jun 28, 2024
bf9fbea
refactor: 클래스 내 멤버 배치 전체적으로 수정
Jun 28, 2024
922446c
refactor: Round에 넘겨줄 변수 상수처리
Jun 28, 2024
43345b3
test: 단어입력 테스트 작성
Jun 28, 2024
ae1a0b5
test: 단어입력 테스트 단위 쪼개서 작성
Jun 28, 2024
8100660
test: 정답 확인 테스트 작성
Jun 29, 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
53 changes: 47 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# 미션 - 워들

## 🙆🏻‍♀️페어 구성🙆🏻
- 점프
- 얄뭉
---

## 🔍 진행 방식

- 미션은 **과제 진행 요구 사항**, **기능 요구 사항**, **프로그래밍 요구 사항** 세 가지로 구성되어 있다.
Expand All @@ -12,13 +17,16 @@

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

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

### 입출력 요구 사항

Expand Down Expand Up @@ -87,3 +95,36 @@ spill
- 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
- 힌트: MVC 패턴 기반으로 구현한 후, View와 Controller를 제외한 Model에 대한 단위 테스트 추가에 집중한다.
---
### 구현 목록
- 단어 불러오기
- 매일 바뀌는 정답단어 선택
- 단어 입력
- 입력시 단어목록에 있나 검증
- 비교 로직
- 같을때, 있을떄, 없을때 따른 비교결과
- 게임로직
- 6회 초과시 게임오버
- 정답시 게임클리어
- ui
- 비교 결과, 라운드 출력

---
### 리팩터링 포인트
Copy link

Choose a reason for hiding this comment

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

리팩토링 포인트를 작성해 주신 부분이 인상깊네요 !
미리 페어와 상의 하에 리스트업을 해두고 리팩토링을 하신 걸까요 ?
취소선이 그어진 항목은 왜 진행하지 않기로 하셨을까요 ?

Copy link
Author

Choose a reason for hiding this comment

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

페어분과 주로 논의한 내용이 나눠진 클래스들을 합치는 것과 String 값 처리였는데 클래스의 분리는 충분하다고 판단했습니다! 😁
String의 경우 배열로 변경하려고 했으나 클래스가 하나 더 생길 수 있다는 점과 IntStream으로 처리해보자는 의견이 나와서 진행하지 않았습니다!

- [x] final 붙혀주기
- [x] Inputword 에 availableWords 상태로 가지고 있을 필요는 없다.
- [x] InputWord와 Answer를 합치기?
- [x] match 에 for 문안 함수로 발라내기
- [x] isEndGame 플래그 변수 네이밍 변경
- [x] 검증
- [x] 5글자 검증
- [x] 영단어 검증
- [x] 예외 try catch
- [x] GameManager에 있는 List 의 변수명 변경
- [x] HintView에 3depth 줄이기
- [x] InputWord의 마지막 테스트함수 이름
- [x] MatchResult에 inputChar가 쓰이지 않고 있어서 클래스 자체를 지우고 그냥 Hint만 쓰기
- ~~- 구조 변경~~
- ~~- GameManager의 의존성 줄여두기~~
- ~~-String 말고 char 배열로 쓸지? -> 좀 더 고민해보기~~
- ~~-분리된 view 부분 합칠지?~~
15 changes: 15 additions & 0 deletions src/main/java/WordleApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import config.FileConfig;
import controller.GameManager;
import infra.WordLoader;

import java.util.List;

public class WordleApplication {
Copy link

Choose a reason for hiding this comment

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

잘못된 답을 입력해도 입력 한 번만에 게임이 종료되네요 👀

Copy link
Author

Choose a reason for hiding this comment

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

페어분 소스를 옮기다가 마지막 반영분을 누락했더라구요🤦🏻‍♀️
이번 리펙토링 기간에 기능을 몇가지 수정했는데요~

  1. 요구사항 미반영분
  2. validation 실패 시 종료하지 않고 진행
  3. 6번 실패 시 게임 종료 문구 노출

를 수정했습니다~

public static void main(String[] args) {

List<String> words = WordLoader.read(FileConfig.FILE_PATH);
GameManager gameManager = new GameManager(words);
gameManager.start();
}

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

public class FileConfig {
Copy link

Choose a reason for hiding this comment

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

FileConfig 클래스도 WordLoader와 동일한 이유로 private 기본 생성자를 만들어두면 어떨까요 ?

Copy link
Author

Choose a reason for hiding this comment

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

반영했습니다~ 👍


protected FileConfig() {
}
public static final String FILE_PATH = "src/main/resources/words.txt";
}
86 changes: 86 additions & 0 deletions src/main/java/controller/GameManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package controller;

import domain.MatchResult;
import domain.MatchResults;
import domain.Round;
import domain.Word;
import ui.GuideTextView;
import ui.HintView;
import ui.InputView;
import ui.RoundView;

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

public class GameManager {

private final static int LIMIT = 6;
private final static int CURRENT = 1;
private final Round round;
private final MatchResults matchResults;
private final Word answer;
private final GuideTextView guideTextView;
private final InputView inputView;
private final HintView hintView;
private final RoundView roundView;
private final List<String> availableWords;
private boolean isWinning = false;

public GameManager(List<String> availableWords) {
this.answer = Word.createAnswer(LocalDate.now(), availableWords);
this.availableWords = availableWords;
this.round = new Round(LIMIT, CURRENT);
this.matchResults = new MatchResults();
this.guideTextView = new GuideTextView();
this.inputView = new InputView();
this.hintView = new HintView();
this.roundView = new RoundView();
}
public void start() {
guideTextView.render();
while(!this.isWinning && round.isNotFinalRound()) {
startRound();
}
}

private void startRound() {
Word inputWord = null;
boolean canCreateInputWord = false;

while (!canCreateInputWord) {
String input = inputView.input();
inputWord = Word.createInput(input, this.availableWords);
canCreateInputWord = inputWord.getAvailableWord();
}

checkAnswer(inputWord);
renderResult();
round.goNext();
}

private void renderResult() {
checkRenderRound();
hintView.render(matchResults);
checkFinalRound();
}

private void checkRenderRound() {
if(this.isWinning || !round.isNotFinalRound()) {
roundView.render(round.getCurrent(),round.getLimit());
}
}

private void checkFinalRound() {
if(!this.isWinning && round.isFinalRound()) {
roundView.render(round.getCurrent(),round.getLimit());
roundView.renderLoseGame();;
}
}

private void checkAnswer(Word inputWord) {
MatchResult matchResultOfInput = answer.match(inputWord);
this.matchResults.add(matchResultOfInput);
this.isWinning = matchResultOfInput.isEndGame();
}

}
20 changes: 20 additions & 0 deletions src/main/java/domain/Hint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package domain;

public enum Hint {
Copy link

Choose a reason for hiding this comment

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

enum 타입 정의 👍🏻

Copy link
Author

Choose a reason for hiding this comment

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

👍

CORRECT("🟩"), // \uD83D\uDFE9
EXIST("🟨"), // \uD83D\uDFE8
NOT_EXIST("⬜");
private final String tile;
Copy link

Choose a reason for hiding this comment

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

컨벤션이 안 맞는 부분이 있네요 👀
(🎁 힌트)

Copy link
Author

@yeojiin yeojiin Jun 28, 2024

Choose a reason for hiding this comment

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

해당 부분은 주석 부분은 정리했는데 정확히 어디 부분이 안 맞는 지 알려주실 수 있을까요?

그리고 공유해주신 문서도 읽고, 더 찾아보면서 흥미로운 부분을 발견했는데요.
상수는 upperSnakeCase 로 작성하는게 맞다고 나오는데 (constant-names) 이 부분을 보면
final이나 static final 보다는 실제로 값이 변할 가능성이 있는지 없는지에 따라 upperSnakeCaselowerCamelCase 를 선택하는 것 같네요~

찾아보면서 java.jar 파일을 참고했는데 java.naming의 VersionHelper 클래스를 보니

VersionHelper

public final class VersionHelper {
    private static final VersionHelper helper = new VersionHelper();

    /**
     * Determines whether classes may be loaded from an arbitrary URL code base.
     */
    private static final boolean TRUST_URL_CODE_BASE;

...
}

와 같이 구현되어 있더라구요.
아마 원시값은 UPPER_SNAKE_CASE으로 유지하고, 원시값이 아닌 경우에는 lowerCamelCase를 사용하는게 아닌가 싶습니다.
따라서 FileConfig 부분을 FILE_PATH -> filePath -> FILE_PATH 로 두번의 수정을 했습니다.😂

코멘트 주신 덕분에 자세히 알아볼 수 있어서 재밌었습니다!

Copy link

Choose a reason for hiding this comment

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

후기까지 세세히 남겨주신 덕분에 저도 또 한 번 배워갑니다 🫡


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

public String getTile() {
return tile;
}

public static boolean isCorrect(Hint hint) {
return Hint.CORRECT.equals(hint);
Copy link

Choose a reason for hiding this comment

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

enum 비교 관련해서 코멘트를 멋지게 달고 싶어서 서치하다가
흥미로운 글을 발견했는데요.
같이 보시면 좋을 것 같아 공유 드립니다 😆

Copy link
Author

Choose a reason for hiding this comment

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

오~ 정말 재밌는 글이네요! 습관적으로 equals를 사용했는데
역시 치이사님 덕분에 좋은 지식 얻어갑니다! 최고! 💯 👍

}
}
44 changes: 44 additions & 0 deletions src/main/java/domain/HintLetter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package domain;

import java.util.List;
import java.util.Objects;

public class HintLetter {
private final Character letter;

private Hint hint;

public HintLetter(Character letter, Hint hint) {
this.letter = letter;
this.hint = hint;
}

public void changeCorrectToNotExist(List<Character> correctedChar) {
if(correctedChar.contains(letter) && !isCorrectHint()) {
this.hint = Hint.NOT_EXIST;
}
}

public boolean isCorrectHint() {
return Hint.isCorrect(hint);
}

public String getHintTile() {
return hint.getTile();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HintLetter that = (HintLetter) o;
return Objects.equals(letter, that.letter) && hint == that.hint;
}

@Override
public int hashCode() {
return Objects.hash(letter, hint);
}
}


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

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class MatchResult {
private final List<HintLetter> hints;
public MatchResult(List<HintLetter> hints) {
this.hints = hints;
}

public boolean isEndGame() {
return hints.stream()
.allMatch(HintLetter::isCorrectHint);
}
public void add(HintLetter hintLetter) {
hints.add(hintLetter);
}

public String getHintTiles() {
return hints.stream()
.map(HintLetter::getHintTile)
.collect(Collectors.joining());
}

@Override
public boolean equals(Object o) {

if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MatchResult that = (MatchResult) o;
return Objects.equals(hints, that.hints);
}

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


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

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

public class MatchResults implements Iterable<MatchResult> {
Copy link

Choose a reason for hiding this comment

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

클래스 내 멤버 배치와 관련해서 궁금한 점이 있어요 🙋🏻‍♀️

저는 보통 상수 -> 일반 필드 -> 기본 생성자 -> 인자가 있는 생성자 -> public 메소드 -> private 메소드 -> 오버라이딩한 메소드 순으로 배치하곤 해요.

이 순서를 어떻게 정하느냐는 개인의 취향이지만,
정한 순서를 일관적으로 가져가는 게 가독성 측면에서 좋다고 생각합니다 !

얄뭉님은 어떻게 생각하시나요 ?
전체적으로 코드에 반영해보는 건 어떨까요 ?
(다른 클래스에도 해당되는 공통적인 코멘트지만 여기에 달아두겠습니다 ... 🙏🏻)

Copy link
Author

@yeojiin yeojiin Jun 28, 2024

Choose a reason for hiding this comment

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

좋은 코멘트 반영했습니다~~~ 👍 🙌🏻

private final List<MatchResult> results;
public MatchResults() {
this.results = new ArrayList<>();
}
public void add(MatchResult matchResultOfInput) {
this.results.add(matchResultOfInput);
}
@Override
public Iterator<MatchResult> iterator() {
return this.results.iterator();
}
}
31 changes: 31 additions & 0 deletions src/main/java/domain/Round.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package domain;

public class Round {
private final int limit;
private int current;

public Round(int limit, int current) {
this.limit = limit;
this.current = current;
}

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

public int getLimit() {
return limit;
}

public int getCurrent() {
return current;
}

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

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