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 과제 제출 - 와잼 #38

Open
wants to merge 62 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f41c990
test: 통합 테스트 - 성공케이스 작성
jongmin4943 Jun 8, 2024
ecce462
docs(README): 게임진행순서, 용어사전 정리
hoa0217 Jun 8, 2024
68be191
docs(README): 모델링(클래스 다이어그램) 작성
hoa0217 Jun 8, 2024
9664220
feat(Position): Position 클래스 생성
hoa0217 Jun 8, 2024
d225a84
feat(Letter): Letter 클래스 생성
hoa0217 Jun 8, 2024
b8ece65
feat(Word): Word 클래스 생성
hoa0217 Jun 8, 2024
76fdedf
feat(Result): Result 클래스 생성
hoa0217 Jun 8, 2024
d97c0c2
refactor(Letter): isSameAlphabetDifferentPosition 메서드명 변경
hoa0217 Jun 9, 2024
846ec02
fix(Result): equals hashCode 구현
hoa0217 Jun 9, 2024
7fdb063
feat(Result): Results 클래스 생성
hoa0217 Jun 9, 2024
d03c4b4
test(Result): Result Fixture 분리
hoa0217 Jun 9, 2024
34c2333
feat(Word): compare 메서드 추가
hoa0217 Jun 9, 2024
60ca6a9
fix(Word): compare 메서드에 노랑타일로직 추가
hoa0217 Jun 9, 2024
e3a595d
test(Word): 글자와 위치가 일부가 같은 경우 테스트 추가
hoa0217 Jun 9, 2024
0a66559
test(Word): 글자와 위치가 전부 다른 경우 테스트 추가
hoa0217 Jun 9, 2024
aa72923
fix(Results): 필드 배열에서 List 로 변경
hoa0217 Jun 9, 2024
20cf464
refactor(Results): 필드 구현체 SortedSet 으로 변경
hoa0217 Jun 9, 2024
151e2ad
fix(Word): 같은 문자가 2개 입력되었을 때, 해당 문자가 정답에 하나만 존재하지만 위치가 틀린 경우 로직 추가
hoa0217 Jun 9, 2024
e14a085
test(Time): LocalDate 모킹 유틸 추가
hoa0217 Jun 12, 2024
b201c59
feat(AnswerFormula): AnswerFormula calculate 구현
hoa0217 Jun 12, 2024
f161544
feat(FileReader): FileReader 구현
hoa0217 Jun 12, 2024
5c08e85
feat(FileReader): FileReader 구현
hoa0217 Jun 12, 2024
bb6eb4f
fix(AnswerFormula): calculate 메서드 반환 타입 int로 변경
hoa0217 Jun 13, 2024
e52073e
fix(AnswerFormula): calculate 메서드 파라미터 예외처리 추가
hoa0217 Jun 13, 2024
b647035
feat(WordBook): 파일 기반 WordBook 클래스 생성
hoa0217 Jun 13, 2024
74acae5
feat(Alphabet): Alphabet 클래스 생성
hoa0217 Jun 13, 2024
7c56c0a
refactor(Letter): Alphabet을 값객체(VO)로 갖는다.
hoa0217 Jun 13, 2024
ee4a197
Merge remote-tracking branch 'origin/jamwa' into jamwa
hoa0217 Jun 15, 2024
db4f022
feat(Record): 단어 비교 기록 클래스 생성
hoa0217 Jun 15, 2024
553fefc
test(Result): 초록색 타일 여부 확인 테스트 추가
hoa0217 Jun 15, 2024
808df83
fix(Result): 초록색,회색 타일 여부 추가
hoa0217 Jun 15, 2024
65b95af
feat(InputView): 사용자 입력을 받는 클래스 생성
hoa0217 Jun 15, 2024
a9e9e76
feat(OutputView): 출력하는 클래스 생성
hoa0217 Jun 15, 2024
e848b10
feat(WordBook): 단어를 찾는 메서드 추가
hoa0217 Jun 15, 2024
6ca7944
test(WordBook): 시간관련 의존성 제거
hoa0217 Jun 15, 2024
4e50c35
fix(Position): 음수 예외처리 추가
hoa0217 Jun 15, 2024
bad65f2
fix(Alphabet): 알파벳은 대소문자를 구분하지 않는다
hoa0217 Jun 15, 2024
f861ed0
fix(ConsoleOutputView): 성공, 실패 종료문구 분리
hoa0217 Jun 15, 2024
1158a12
fix(Record): existAllGreen, isCountOver 메서드 추출
hoa0217 Jun 15, 2024
cc4afbf
feat(Wordle): 게임완성
hoa0217 Jun 15, 2024
8e397f0
fix(Exception): 예외 정리
hoa0217 Jun 15, 2024
3fbfa43
refactor(Word): compare 메서드 리팩토링
hoa0217 Jun 15, 2024
5f37a11
feat(FileReader): FileReader 구현
hoa0217 Jun 15, 2024
609a398
refactor(Word): 코드 정리
hoa0217 Jun 17, 2024
7e58797
test: 테스트에 사용되는 리소스(word.text)및 케이스 변경
hoa0217 Jun 17, 2024
a678cd3
refactor: 코드 정리
hoa0217 Jun 17, 2024
d170631
test: 콘솔 통합테스트 시간 고정
hoa0217 Jun 17, 2024
261dbd5
docs: 리드미 정리
hoa0217 Jun 17, 2024
8ba771c
Merge remote-tracking branch 'refs/remotes/upstream/jamwa' into jongm…
jongmin4943 Jun 17, 2024
d83dca1
test: Windows OS 에서 실패하는 newLine 관련 테스트 보정
jongmin4943 Jun 17, 2024
ec811af
style: 빠진 `@Override` 구문 추가
jongmin4943 Jun 17, 2024
a34f2b1
fix: 불필요한 static 선언 제거
jongmin4943 Jun 24, 2024
7f4d669
fix(ConsoleOutputView): 불필요한 static 선언 제거
jongmin4943 Jun 24, 2024
2cb3d49
fix(FileReader): try-catch-resource 구문으로 변경
jongmin4943 Jun 24, 2024
37cc83d
fix(Record): `isCountOver` 메서드 private 으로 변경
jongmin4943 Jun 24, 2024
6ea0bd5
fix(Alphabet): final 구문 추가
jongmin4943 Jun 24, 2024
6732d19
fix(Wordle): turn 단어 제거
jongmin4943 Jun 24, 2024
90c6a7d
feat(Letter): `isSamePosition` 메서드 추가
jongmin4943 Jun 24, 2024
451a9a1
feat(Letter): `Comparable` 인터페이스 구현
jongmin4943 Jun 24, 2024
925115e
feat(ResultV2): `Result` 클래스 변형을 위한 `ResultV2` 클래스 생성
jongmin4943 Jun 24, 2024
fd9b259
fix: 기존 `Result` 를 `ResultV2` 로 대체 작업
jongmin4943 Jun 24, 2024
500da3b
fix(ConsoleOutputView): 불필요한 static 선언 제거
jongmin4943 Jun 24, 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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ repositories {
}

dependencies {
testImplementation 'org.mockito:mockito-inline:3.12.0'

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.

개인적 욕심인 ConsoleIntegrationTest 를 위한 선택이랍니다 하핫..

testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testImplementation 'org.assertj:assertj-core:3.25.3'
}
Expand Down
163 changes: 163 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# 미션 - 워들

## 게임 진행 순서

- 단어장(`words.txt`)에 있는 단어를 읽어들인다.
- 읽어들인 단어들에서 정답인 단어를 정한다.
- 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.
- 문자 5개를 입력한다.
- 5개가 아닌 경우 재입력을 받는다.
- 단어장에 존재하지 않는 단어인 경우 재입력을 받는다.
- 알파벳이 아닌 경우 재입력을 받는다.
- 입력받은 문자와 정답을 비교한다.
- 비교 결과는 타일이 초록색/노란색/회색 중 하나로 바뀌면서 표현된다.
- 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색
- 같은 문자가 2개 입력되었을 때, 해당 문자가 정답에 하나만 존재하지만 위치가 틀린 경우 첫번 째 문자만 노란색으로 표시된다.
- 정답: lurid, 입력: hello, 결과: ⬜⬜🟨⬜⬜
- 6번 안에 맞추면 게임을 종료한다.
- 6번 안에 맞추지 못하면 그래도 종료한다.

## 용어 사전

Choose a reason for hiding this comment

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

용어 사전 너무 좋네요! DDD 그 자체 👍

Copy link
Author

Choose a reason for hiding this comment

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

DDD 천재 호아님의 가르침! 🙇


| 한글명 | 영문명 | 설명 |
|--------|----------------|------------------------------------------------|
| 워들 | Wordle | 5글자 영어 단어 맞추기 게임 |
| 단어장 | Word Book | 이 게임에서 사용될 수 있는 단어 모음 |
| 입력 단어 | Input Word | 플레이어가 입력하는 5글자 단어 |
| 정답 단어 | Answer Word | 오늘 게임의 정답인 5글자 단어 |
| 글자 | Letter | 단어를 구성하는 알파벳 |
| 영문 | Alphabet | 글자를 구성하는 최소단위 |
| 위치 | Position | 단어를 구성하는 글자의 위치 |
| 플레이어 | Player | 게임에 참여하는 사용자 |
| 결과 | Result | 입력단어와 정답단어를 비교해서 표현되는 타일모음 |
| 비교 | Compare | 입력단어와 정답단어의 글자와 위치를 비교하는 행위 |
| 초록색 타일 | Green Tile | 글자와 위치가 동일한 경우 표현되는 타일 |
| 노란색 타일 | Yellow Tile | 글자는 포함되지만 위치가 다른 경우 표현되는 타일 |
| 회색 타일 | Gray Tile | 글자와 위치가 모두 다른 경우 표현되는 타일 |
| 결과모음 | Results | 라운드가 진행될 때 마다 누적된 결과모음 |
| 기록모음 | Record | 누적된 결과모음의 기록 |
| 기준일 | Base Date | 오늘의 정답 단어를 계산하는 기준일(2021년 6월 19일) |
| 정답 공식 | Answer Formula | 오늘의 정답 단어를 계산하는 공식 `(현재 날짜 - 기준일) % 단어장의 단어 수` |
| 시작 | Start | 플레이어가 워들을 시작하는 행위 |
| 종료 | End | 워들이 종료되는 행위(라운드가 전부 끝났거나, 그 전에 정답을 맞추면 종료된다) |

## 모델링

### 클래스 다이어그램

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.

천재 호아님의 작품입니닷 😆


```mermaid
---
title: Wordle
---
classDiagram
class Word {
-List<Letter> letters
+compare(Word inputWord) Results
+equals(Object o) boolean
}

class WordComparator {
-List<Letter> pendingLetters
-Results results
+WordComparator(letters: List<Letter>)
+compare(inputWord: Word): Results
-process(targetLetter: Letter, predicate: Predicate<Letter>, tile: Tile)
-fillEmptyToGray(targetLetter: Letter)
}

Word --|> WordComparator

class Letter {
-Alphabet alphabet
-Position position
+isSameAlphabet(Letter letter) boolean
+getPosition() Position
+equals(Object o) boolean
}

class Alphabet {
-char alphabet
+equals(Object o) boolean
}

class Position {
-char position
+compareTo(Position position) boolean
+equals(Object o) boolean
}

class Record {
-List<Results> record
+add(Results results) void
+isEnd() boolean
+isCountOver() boolean
+existAllGreen() boolean
+iterator() Iterator<Results>
+size() int
}
class Results {
-SortedSet<Result> results
+add(Result result) void
+isCheckedPosition(Position position) boolean
+isAllGreen() boolean
}
class Result {
-Tile tile
-Position poistion
+isSamePosition(Position position) boolean
+equals(Object o) boolean
+compareTo(Result result) int
+isGreen() boolean
+isYellow() boolean
+isGray() boolean
}
class Tile {
<<enumeration>>
+GREEN
+YELLOW
+GRAY
}
class AnswerFormula {
<<interface>>
+calculate(int wordCount) int
}
class BaseAnswerFormula {
+calculate(int wordCount) int
}
BaseAnswerFormula ..|> AnswerFormula
class Wordle {
-WordBook wordBook
-InputView inputView
-OutputView outputView
-Record record
-AnswerFormula answerFormula
+startGame() void
-runGame(Word answerWord) void
-processTurn(Word answerWord) void
-concludeGame() void
-handleWrongAnswer(Runnable runnable) void
}
class WordBook {
<<interface>>
+pick(AnswerFormula formula) Word
+exist(Word word) boolean
+find(String target) Word
}
class FileWordBook {
-List<Word> words
+pick(AnswerFormula formula) Word
+exist(Word word) boolean
+find(String target) Word
}
FileWordBook ..|> WordBook
```

## 🚀 세부 요구 사항

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

import wordle.application.Wordle;
import wordle.domain.FileWordBook;
import wordle.domain.WordBook;
import wordle.infra.FileReader;
import wordle.ui.ConsoleInputView;
import wordle.ui.ConsoleOutputView;
import wordle.ui.InputView;
import wordle.ui.OutputView;

public class Application {

public static void main(String[] args) {
WordBook wordBook = new FileWordBook(new FileReader());
InputView inputView = new ConsoleInputView();
OutputView outputView = new ConsoleOutputView();
Wordle wordle = new Wordle(wordBook, inputView, outputView);
wordle.startGame();
}
}
72 changes: 72 additions & 0 deletions src/main/java/wordle/application/Wordle.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package wordle.application;

import wordle.domain.AnswerFormula;
import wordle.domain.BaseAnswerFormula;
import wordle.domain.Record;
import wordle.domain.Results;
import wordle.domain.Word;
import wordle.domain.WordBook;
import wordle.exception.WordleException;
import wordle.exception.WordleInvalidInputException;
import wordle.ui.InputView;
import wordle.ui.OutputView;

public class Wordle {

private final WordBook wordBook;

private final InputView inputView;

private final OutputView outputView;

private final Record record;

Choose a reason for hiding this comment

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

Record를 멤버 변수로 두니까 Wordle 내의 전체 메서드들이 이 객체를 참조할 수 있어서
로직을 전개하는 것이 편하네요!
게임 한 번에 하나만 존재해야 하는 객체라 생성 시점에 초기화해서 쭉 사용하는 것이 맞는 방향이라는 생각도 들고요.
왜 저희는 이 방법을 생각 안했나 싶네요 🫢

Copy link
Author

Choose a reason for hiding this comment

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

게임 한 번에 하나만 존재해야 하는 객체라 생성 시점에 초기화해서 쭉 사용하는 것이 맞는 방향이라는 생각도 들고요.

이런 관점으로도 보일 수 있겠네요! 😮
페어코딩을 할 때 가장 작은 객체부터 만들어 나가며 조립 하다보니 자연스레 도출되었던 클래스 였습니다 😃
결과들을 모아두는 책임을 가진 클래스가 필요했고, 그 클래스는 어떤 책임을 가지면 좋을까 고민하다 보니 Record 가 탄생했습니다! 👶


private final AnswerFormula answerFormula;

public Wordle(final WordBook wordBook, final InputView inputView, final OutputView outputView) {
this.wordBook = wordBook;
this.inputView = inputView;
this.outputView = outputView;
this.record = new Record();
this.answerFormula = new BaseAnswerFormula();
}

public void startGame() {
final Word answerWord = wordBook.pick(answerFormula);
outputView.welcome();
runGame(answerWord);
concludeGame();
}

private void runGame(final Word answerWord) {
while (!record.isEnd()) {
outputView.showRecord(record);
handleWrongAnswer(() -> process(answerWord));
}
}

private void process(final Word answerWord) {
outputView.askAnswer();
final Word inputWord = wordBook.find(inputView.input());
final Results results = answerWord.compare(inputWord);
record.add(results);
}

private void concludeGame() {
if (record.existAllGreen()) {
outputView.successEnd(record);
return;
}
outputView.failEnd(record);
}

private void handleWrongAnswer(final Runnable runnable) {
try {
runnable.run();
} catch (final WordleInvalidInputException e) {
outputView.wrongAnswer();
} catch (final WordleException e) {
outputView.unexpectedEnd();
}
}
}
37 changes: 37 additions & 0 deletions src/main/java/wordle/domain/Alphabet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package wordle.domain;

import java.util.Objects;
import wordle.exception.InvalidAlphabetException;

public class Alphabet {

private static final char MIN_ALPHABET = 'a';
private static final char MAX_ALPHABET = 'z';
private final char alphabet;

public Alphabet(final char alphabet) {
final char lowerAlphabet = Character.toLowerCase(alphabet);
if (lowerAlphabet < MIN_ALPHABET || lowerAlphabet > MAX_ALPHABET) {
throw new InvalidAlphabetException();
}

this.alphabet = lowerAlphabet;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Alphabet alphabet1 = (Alphabet) o;
return alphabet == alphabet1.alphabet;
}

@Override
public int hashCode() {
return Objects.hash(alphabet);
}
}
7 changes: 7 additions & 0 deletions src/main/java/wordle/domain/AnswerFormula.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wordle.domain;

@FunctionalInterface
public interface AnswerFormula {

int calculate(int wordCount);
}
19 changes: 19 additions & 0 deletions src/main/java/wordle/domain/BaseAnswerFormula.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wordle.domain;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import wordle.exception.AnswerFormulaException;

public class BaseAnswerFormula implements AnswerFormula {

private static final LocalDate BASE = LocalDate.of(2021, 6, 19);
private static final int MIN_WORD_COUNT = 1;

public int calculate(int wordCount) {
if (wordCount < MIN_WORD_COUNT) {
throw new AnswerFormulaException();
}

return (int) ChronoUnit.DAYS.between(BASE, LocalDate.now()) % wordCount;

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.

구월님쪽 계산법도 아주 훌륭한 접근이었습니다! 👍

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

import java.util.List;
import wordle.exception.WordNotExistException;
import wordle.infra.FileReader;

public class FileWordBook implements WordBook {

public static final String FILE_PATH = "words.txt";
private final List<Word> words;

public FileWordBook(FileReader fileReader) {
this.words = fileReader.readByLine(FILE_PATH)
.stream()
.map(Word::new)
.toList();
}

@Override
public Word pick(AnswerFormula answerFormula) {
int index = answerFormula.calculate(words.size());

Choose a reason for hiding this comment

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

AnswerFormula의 역할이 굉장히 흥미로운데요,
단어 목록의 size만을 인자로 받아서 몇 번째 index를 반환할지 계산하여 return한다는 점이 매우 신선하네요!
이렇게 하면 정답을 선택하는 방식의 계산을 담당하는 AnswerFormula는 단어 목록에 대해 알지 못하기 때문에 완전히 결합을 끊어낼 수 있다는 굉장히 큰 장점이 있는 것 같습니다 👍

다만 네이밍을 조금 더 직관적으로 할 수 있을 것 같아서 조심스럽게 제안해봅니다.
AnswerIndexSelector 와 같은 네이밍은 어떠신가요? 🤔

Copy link
Author

Choose a reason for hiding this comment

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

용어사전 정리할때, 현실세계에 있는 단어들을 이용해 용어를 정리하다보니, 정답 공식 이라는 용어가 추출되어 그대로 반영된 부분입니다 😃

제가 얕게 아는 DDD 세계관에서 용어는 비개발자도 이해가 가능한 단어 이어야 한다는 점에서 저런 용어가 나온 것 같습니다. 그런데 단어장 or 사전에서 색인 이라는 개념이 있으니 AnswerIndexSelector 도 훌륭한 명칭이라 생각이 드네요! 감사합니다 👍

return words.get(index);
}

@Override
public boolean exist(Word word) {
return words.contains(word);
}

@Override
public Word find(String target) {
Word targetWord = new Word(target);
return words.stream()
.filter(targetWord::equals)
.findFirst()
.orElseThrow(WordNotExistException::new);
}
}
Loading