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

[3주차] Wordle 과제 제출 - 홀든 #17

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
822638d
docs: README를 수정합니다.
giibeom Mar 23, 2023
391683e
test: 워들 게임 기능의 요구 사항을 분석하고 인수 테스트를 작성합니다.
giibeom Mar 24, 2023
e09c83c
refactor: 정답 체크 관련 변수명들을 변경합니다
seung-00 Mar 26, 2023
0801e39
merge(#1): 워들 게임 요구 사항 분석 및 인수 테스트 작성
giibeom Mar 26, 2023
82f03b6
chore: PR 템플릿을 생성합니다.
giibeom Mar 26, 2023
8161def
refactor: 유효성 검사 기준을 문자열에서 알파벳으로 변경하여 인수 테스트 변수 및 메서드명을 수정합니다.
seung-00 Mar 26, 2023
3ddc7e7
feat: WordleGame 입력값 유효성 검사를 추가합니다.
seung-00 Mar 26, 2023
608e5c1
Merge pull request #2 from giibeom/feat/validate
Mar 26, 2023
4109fbe
merge(#3): 오늘의 단어를 선택하는 기능 추가
giibeom Mar 27, 2023
270a6ab
merge(#4): 정답 여부 확인 기능 추가
Mar 30, 2023
05bb971
merge(#5): 정답 결과를 반환하는 기능 추가
giibeom Apr 1, 2023
f3b42b5
merge(#6): 각 예외 상황 핸들링 추가
giibeom Apr 1, 2023
6afd2eb
최종 리팩터링 (#7)
Apr 2, 2023
827e04c
docs: README 에 원본 repository 관련 정보를 추가합니다
Apr 2, 2023
c1e373f
refactor: isClear -> isGameClear 으로 네이밍을 변경합니다
seung-00 Apr 15, 2023
2253ffe
feat: WordleGame 구조를 변경합니다
seung-00 Apr 16, 2023
8918793
refactor: 패키지 구조를 변경합니다
seung-00 Apr 16, 2023
97a17ac
feat: 구조를 변경합니다
seung-00 Apr 16, 2023
21a9341
feat: UI 동작을 수정합니다
seung-00 Apr 16, 2023
c26c96d
Merge pull request #1 from seung-00/refactor/review
Apr 16, 2023
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
13 changes: 13 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## 작업 요약

- 요구사항에 맞게 작업할 내용을 간략하게 정리합니다.

## 작업 내용

- [ ] 작업 1
- [ ] 작업 2

<br>

## 참고 자료
- 기술 문서나 테스트 결과 등 작업에 관련된 참고 자료를 정리합니다.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# 미션 - 워들

## 👥 페어 멤버
| ALEX |HOLDEN |
| --- | --- |
[<img src="https://avatars.githubusercontent.com/u/59248326?v=4" width="100">](https://github.com/giibeom)| [<img src="https://avatars.githubusercontent.com/u/46865281?v=4" width="100">](https://github.com/seung-00) |

* [Alex 님의 repository](https://github.com/giibeom/study-java-wordle) 에서 페어 프로그래밍을 진행했습니다.
* 이 repository 는 해당 repository 를 미러링했습니다.
* 진행 방식은 해당 [repository wiki](https://github.com/giibeom/study-java-wordle/wiki) 에 정리되어 있습니다.
Copy link

Choose a reason for hiding this comment

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

와우 ... 이걸 wiki로... 문서 정리가 정말 깔끔하고 트렌디한 느낌이네요..
한수 배웁니당



---

## 🔍 진행 방식

- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/woowaapplication/pair/game/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package woowaapplication.pair.game;

import woowaapplication.pair.game.wordle.WordleGame;

public class Application {
Copy link

Choose a reason for hiding this comment

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

wordle 패키지 안의 클래스 들이 패키지로 구분이 되었으면 좋겠어요!


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

import woowaapplication.pair.game.wordle.exception.OutOfChanceException;

public class Coin {

private int restChance;

public Coin(int restChance) {
this.restChance = restChance;
}

public void decreaseChance() {
if (restChance < 1) {
throw new OutOfChanceException();
}

restChance -= 1;
}

public int getRestChance() {
return restChance;
}

public boolean isOutOfChance() {
return restChance < 1;
}

public static Coin of(int restChance) {
return new Coin(restChance);
}
}
20 changes: 20 additions & 0 deletions src/main/java/woowaapplication/pair/game/wordle/FileReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package woowaapplication.pair.game.wordle;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import woowaapplication.pair.game.wordle.exception.ReadFileException;

public class FileReader {

public static List<String> readLinesFromFile(URL fileUrl) {
try {
return Files.readAllLines(Paths.get(fileUrl.toURI()));
} catch (IOException | URISyntaxException e) {
throw new ReadFileException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package woowaapplication.pair.game.wordle;

import woowaapplication.pair.game.wordle.exception.InvalidInputKeywordException;

public class KeywordValidator {
Copy link

Choose a reason for hiding this comment

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

이번 넥스트 스텝에서 배웠는데용.

자바에서는 생성자를 따로 선언하지 않은경우 public 기본 생성자를 생성합니다.
객체를 인스턴스화 하지 않는 경우라면 private 생성자를 선언해 두는건 어떨까요?

관련해서는 Effective Java 3/E item4 를 참고해 보라고 하더군요! 저도 아직은 확인 안해봤어요 ㅋ;


public static void validate(String keyword, int lengthLimit) {
if (keyword == null) {
throw new InvalidInputKeywordException();
}

if (!isAlphabetic(keyword)) {
throw new InvalidInputKeywordException();
}

if (!isLengthValid(keyword, lengthLimit)) {
throw new InvalidInputKeywordException();
}
Copy link

Choose a reason for hiding this comment

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

정규식으로 하나로 합칠 수 있지 않을까 싶어요!

}

private static boolean isAlphabetic(String keyword) {
return keyword.matches("[a-zA-Z]+");
Copy link

Choose a reason for hiding this comment

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

String의 matches는 Patter.matches를 사용하는데 이 함수가 PatternMatcher
기능 사용할 때마다 새로 컴파일하고 새로 생성해요.

그래서 여러번 match를 하는 경우이니 직접 구현하셔 객체를 싱글톤으로 구현하시는게 좋을 것 같아요!

}

private static boolean isLengthValid(String keyword, int length) {
return keyword.length() == length;
}
}
68 changes: 68 additions & 0 deletions src/main/java/woowaapplication/pair/game/wordle/WordleBlock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package woowaapplication.pair.game.wordle;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

public enum WordleBlock {
CORRECT( "🟩"),
EXIST_BUT_WRONG_SPOT( "🟨"),
WRONG( "⬜"),
;

private final String emoji;

WordleBlock(String emoji) {
this.emoji = emoji;
}

public String getEmoji() {
return emoji;
}

public static boolean isAllCorrect(WordleBlock[] wordleBlocks) {
return Arrays.stream(wordleBlocks)
.allMatch(block -> block == WordleBlock.CORRECT);
}

public static WordleBlock[] toList(String inputKeyword, String answerKeyword) {
WordleBlock[] resultBlocks = new WordleBlock[WordleGame.KEYWORD_LENGTH];
Set<Character> answerLetters = createAnswerLetters(answerKeyword);

for (int index = 0; index < inputKeyword.length(); index++) {
char inputLetter = inputKeyword.charAt(index);
char answerLetter = answerKeyword.charAt(index);

WordleBlock block = compareLetters(inputLetter, answerLetter, answerLetters);

resultBlocks[index] = block;
}

return resultBlocks;
}

private static HashSet<Character> createAnswerLetters(String answerKeyword) {
return answerKeyword.chars()
.mapToObj(letter -> (char) letter)
.collect(Collectors.toCollection(HashSet::new));
}

private static WordleBlock compareLetters(char inputLetter, char answerLetter, Set<Character> answerLetters) {
if (answerLetter == inputLetter) {
return WordleBlock.CORRECT;
}

if (answerLetters.contains(inputLetter)) {
return WordleBlock.EXIST_BUT_WRONG_SPOT;
}

return WordleBlock.WRONG;
}

public static String[] toEmojiList(WordleBlock[] wordleBlocks) {
return Arrays.stream(wordleBlocks)
.map(WordleBlock::getEmoji)
.toArray(String[]::new);
}
}
66 changes: 66 additions & 0 deletions src/main/java/woowaapplication/pair/game/wordle/WordleGame.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package woowaapplication.pair.game.wordle;

import java.util.List;
import java.util.Scanner;
import woowaapplication.pair.game.wordle.exception.InvalidAnswerKeywordException;
import woowaapplication.pair.game.wordle.exception.InvalidInputKeywordException;
import woowaapplication.pair.game.wordle.exception.OutOfChanceException;
import woowaapplication.pair.game.wordle.exception.ReadFileException;

public class WordleGame {

public static final int TOTAL_CHANCE = 6;
public static final int KEYWORD_LENGTH = 5;
private final WordleGameService wordleGameService;
private final WordleGameUI wordleGameUI;

public WordleGame() {
Coin coin = Coin.of(TOTAL_CHANCE);
WordleGameStorage wordleGameStorage = WordleGameStorage.of(coin);
this.wordleGameUI = WordleGameUI.of(wordleGameStorage);
this.wordleGameService = WordleGameService.of(wordleGameStorage);
}

public void start() {
Scanner sc = ready();

while (!wordleGameService.isGameOver()) {
try {
run(sc);
} catch (InvalidInputKeywordException e) {
System.out.println(e.getMessage());
} catch (InvalidAnswerKeywordException e) {
System.out.println(e.getMessage());
break;
} catch (ReadFileException e) {
System.out.println(e.getMessage());
break;
} catch (OutOfChanceException e) {
System.out.println(e.getMessage());
break;
}
Comment on lines +41 to +50
Copy link

Choose a reason for hiding this comment

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

Exception 이 나누어져 있는 건 명시적이어서 정말 좋은데

코드가 좀 길어져서 밑에 세개는 RuntimeException 으로 묶는 건 어떨까 싶습니당!

}

terminate();
}

private void run(Scanner sc) {
String inputKeyword = sc.nextLine();

KeywordValidator.validate(inputKeyword, KEYWORD_LENGTH);
List<String[]> gameResult = wordleGameService.playRound(inputKeyword);

wordleGameUI.printResult(gameResult);
}

private Scanner ready() {
Scanner sc = new Scanner(System.in);
WordleGameUI.printReady();

return sc;
}
Copy link

Choose a reason for hiding this comment

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

ScannerWordleGameUI 로 넘겨서 static으로 꺼내쓰게 하고

모든 UI관련 된 책임을 지게 하는게 좋아 보입니다. ㅎㅎ


private void terminate() {
WordleGameUI.printTerminate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package woowaapplication.pair.game.wordle;


import java.net.URL;
import java.time.LocalDate;
import java.time.Period;
import java.util.List;
import woowaapplication.pair.game.wordle.exception.InvalidAnswerKeywordException;

public class WordleGameService {

public static final String WORDS_FILE_NAME = "words.txt";

private final WordleGameStorage wordleGameStorage;

private final LocalDate standardDate = LocalDate.of(2021, 6, 19);
private final LocalDate comparisonDate;

public WordleGameService(WordleGameStorage wordleGameStorage, LocalDate comparisonDate) {
this.wordleGameStorage = wordleGameStorage;
this.comparisonDate = comparisonDate;
}

public WordleGameService(WordleGameStorage wordleGameStorage) {
this.wordleGameStorage = wordleGameStorage;
this.comparisonDate = LocalDate.now();
}

public boolean isGameOver() {
return wordleGameStorage.isGameOver() || wordleGameStorage.isClear();
}

public List<String[]> playRound(String inputKeyword) {
String answerKeyword = getAnswerKeyword();
Copy link

Choose a reason for hiding this comment

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

이 부분은 run을 할때마다 실행하는 것으로 보이는데
이렇게 되면 WordleGame 에서 while 문이 실행하는 동안
파일을 매번 읽어오게 되는거 같애요

성능적인면은 둘째 치더라도 어짜피 같은 것이기 떄문에 최초에 게임 실행 시에만
읽는 것이 좋아보여요.

또한, 혹시나 그럴일은 정말 작지만... 게임 실행을 23시 59분에 해서 다음날 00시 01분에 끝난다면..
문제를 푸는 도중에 단어가 바뀌지 않을까 싶어요.

WordleBlock[] wordleBlocks = WordleBlock.toList(inputKeyword, answerKeyword);

wordleGameStorage.checkAnswer(wordleBlocks);
wordleGameStorage.decreaseChance();

return wordleGameStorage.convertHistoryToEmoji();
}

public String getAnswerKeyword() {
List<String> keywords = readKeywordsFromFile();

if (keywords.isEmpty()) {
throw new InvalidAnswerKeywordException();
}

int index = findAnswerKeywordIndex(keywords);

return keywords.get(index);
}

private List<String> readKeywordsFromFile() {
URL resource = getClass().getClassLoader().getResource(WORDS_FILE_NAME);
return FileReader.readLinesFromFile(resource);
}

private int findAnswerKeywordIndex(List<String> keywords) {
Period period = Period.between(standardDate, comparisonDate);

return (period.getDays() % keywords.size()) - 1;
}

public static WordleGameService of(WordleGameStorage wordleGameStorage) {
return new WordleGameService(wordleGameStorage);
}

public static WordleGameService of(WordleGameStorage wordleGameStorage, LocalDate comparisonDate) {
return new WordleGameService(wordleGameStorage, comparisonDate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package woowaapplication.pair.game.wordle;

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

public class WordleGameStorage {

private final List<WordleBlock[]> wordleBlocksHistory = new ArrayList<>();
private final Coin coin;
private boolean isClear;

public WordleGameStorage(Coin coin) {
this.coin = coin;
this.isClear = false;
Copy link

Choose a reason for hiding this comment

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

boolean default는 어차피 false라서 초기화 안 해주셔도 될 것 같아요!

}

public int getRestChance() {
return coin.getRestChance();
}

public boolean isGameOver() {
return coin.isOutOfChance();
}

public boolean isClear() {
return isClear;
}
Copy link

Choose a reason for hiding this comment

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

isClear 보단 위의 isGameOver 처럼 isGameClear로 명명하시는게 어떨까요?
WordleGameUI에서 isClear를 사용하시는데 Storage가 비어 있다는 건가?
라는 착각으로 여기까지 들어와서 무엇인지 찾아봤거든요 ㅎㅎ


public void checkAnswer(WordleBlock[] wordleBlocks) {
wordleBlocksHistory.add(wordleBlocks);
isClear = WordleBlock.isAllCorrect(wordleBlocks);
}

public void decreaseChance() {
if (isClear) {
return;
}

coin.decreaseChance();
}

public List<String[]> convertHistoryToEmoji() {
return wordleBlocksHistory.stream()
.map(WordleBlock::toEmojiList)
.collect(Collectors.toList());
}

public static WordleGameStorage of(Coin coin) {
return new WordleGameStorage(coin);
}
}
Loading