diff --git a/build.gradle b/build.gradle index 0b3a9762..b12b2426 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ repositories { } dependencies { + testImplementation 'org.mockito:mockito-inline:3.12.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' testImplementation 'org.assertj:assertj-core:3.25.3' } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..20067e04 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,161 @@ +# 미션 - 워들 + +## 게임 진행 순서 + +- 단어장(`words.txt`)에 있는 단어를 읽어들인다. +- 읽어들인 단어들에서 정답인 단어를 정한다. + - 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다. +- 문자 5개를 입력한다. + - 5개가 아닌 경우 재입력을 받는다. + - 단어장에 존재하지 않는 단어인 경우 재입력을 받는다. + - 알파벳이 아닌 경우 재입력을 받는다. +- 입력받은 문자와 정답을 비교한다. +- 비교 결과는 타일이 초록색/노란색/회색 중 하나로 바뀌면서 표현된다. + - 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색 + - 같은 문자가 n개 입력되었을 때, 해당 문자가 정답에 하나만 존재하지만 위치가 틀린 경우 첫번 째 문자만 노란색으로 표시된다. + - 정답: lurid, 입력: hello, 결과: ⬜⬜🟨⬜⬜ +- 6번 안에 맞추면 게임을 종료한다. +- 6번 안에 맞추지 못하면 그래도 종료한다. + +## 용어 사전 + +| 한글명 | 영문명 | 설명 | +|--------|----------------|------------------------------------------------------------| +| 워들 | Wordle | 5글자 영어 단어 맞추기 게임 | +| 단어장 | Word Book | 이 게임에서 사용될 수 있는 단어 모음 | +| 단어 | 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 | 워들이 종료되는 행위(라운드가 전부 끝났거나, 그 전에 정답을 맞추면 종료된다) | + +## 모델링 + +### 클래스 다이어그램 + +```mermaid +--- +title: Wordle +--- +classDiagram + class Word { + -List letters + +compare(Word inputWord) Results + +equals(Object o) boolean + } + + class WordComparator { + -List pendingLetters + -Results results + +WordComparator(letters: List) + +compare(targetWord: Word): Results + -process(targetLetter: Letter, predicate: Predicate, 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 record + +add(Results results) void + +isCountOver() boolean + +existAllGreen() boolean + +iterator() Iterator + +size() int + } + class Results { + -SortedSet results + +add(Result result) void + +isCheckedPosition(Position position) boolean + +isAnswer() 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 { + <> + +GREEN + +YELLOW + +GRAY + } + class AnswerFormula { + <> + +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 + -processRound(Word answerWord) void + -concludeGame() void + -handleWrongAnswer(Runnable runnable) void + } + class WordBook { + <> + +pick(AnswerFormula formula) Word + +exist(Word word) boolean + +find(String target) Word + } + class FileWordBook { + -List words + +pick(AnswerFormula formula) Word + +exist(Word word) boolean + +find(String target) Word + } + FileWordBook ..|> WordBook +``` + +## 🚀 세부 요구 사항 + +- 6x5 격자를 통해서 5글자 단어를 6번 만에 추측한다. +- 플레이어가 답안을 제출하면 프로그램이 정답과 제출된 단어의 각 알파벳 종류와 위치를 비교해 판별한다. +- 판별 결과는 흰색의 타일이 세 가지 색(초록색/노란색/회색) 중 하나로 바뀌면서 표현된다. + - 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색 + - 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다. +- 정답과 답안은 `words.txt`에 존재하는 단어여야 한다. +- 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다. diff --git a/src/main/java/wordle/Application.java b/src/main/java/wordle/Application.java new file mode 100644 index 00000000..96b964a2 --- /dev/null +++ b/src/main/java/wordle/Application.java @@ -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(); + } +} diff --git a/src/main/java/wordle/application/Wordle.java b/src/main/java/wordle/application/Wordle.java new file mode 100644 index 00000000..92c5715b --- /dev/null +++ b/src/main/java/wordle/application/Wordle.java @@ -0,0 +1,75 @@ +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.WordNotExistException; +import wordle.exception.WordleException; +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; + + private final AnswerFormula answerFormula; + + public Wordle(WordBook wordBook, InputView inputView, OutputView outputView) { + this.wordBook = wordBook; + this.inputView = inputView; + this.outputView = outputView; + this.record = new Record(); + this.answerFormula = new BaseAnswerFormula(); + } + + public void startGame() { + Word answerWord = wordBook.pick(answerFormula); + outputView.welcome(); + concludeGame(runGame(answerWord)); + } + + private boolean runGame(Word answerWord) { + while (!record.isCountOver()) { + outputView.showRecord(record); + handleWrongAnswer(() -> processRound(answerWord)); + + if (record.existAnswer()) { + return true; + } + } + return false; + } + + private void processRound(Word answerWord) { + outputView.askAnswer(); + Word inputWord = wordBook.find(inputView.input()) + .orElseThrow(() -> new WordNotExistException(inputView.input())); + Results results = answerWord.compare(inputWord); + record.add(results); + } + + private void handleWrongAnswer(Runnable runnable) { + try { + runnable.run(); + } catch (WordleException e) { + outputView.unexpectedEnd(e.getMessage()); + } + } + + private void concludeGame(boolean isGameWon) { + if (isGameWon) { + outputView.successEnd(record); + return; + } + outputView.failEnd(record); + } +} diff --git a/src/main/java/wordle/domain/Alphabet.java b/src/main/java/wordle/domain/Alphabet.java new file mode 100644 index 00000000..8960e65f --- /dev/null +++ b/src/main/java/wordle/domain/Alphabet.java @@ -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(char alphabet) { + char lowerAlphabet = Character.toLowerCase(alphabet); + if (lowerAlphabet < MIN_ALPHABET || lowerAlphabet > MAX_ALPHABET) { + throw new InvalidAlphabetException("올바르지 않은 알파벳입니다. (" + alphabet + ")"); + } + + this.alphabet = lowerAlphabet; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Alphabet alphabet1 = (Alphabet) o; + return alphabet == alphabet1.alphabet; + } + + @Override + public int hashCode() { + return Objects.hash(alphabet); + } +} diff --git a/src/main/java/wordle/domain/AnswerFormula.java b/src/main/java/wordle/domain/AnswerFormula.java new file mode 100644 index 00000000..fb4f8d35 --- /dev/null +++ b/src/main/java/wordle/domain/AnswerFormula.java @@ -0,0 +1,7 @@ +package wordle.domain; + +@FunctionalInterface +public interface AnswerFormula { + + int calculate(int wordCount); +} diff --git a/src/main/java/wordle/domain/BaseAnswerFormula.java b/src/main/java/wordle/domain/BaseAnswerFormula.java new file mode 100644 index 00000000..fe9356f0 --- /dev/null +++ b/src/main/java/wordle/domain/BaseAnswerFormula.java @@ -0,0 +1,19 @@ +package wordle.domain; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import wordle.exception.AnswerFormulaWordCountException; + +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 AnswerFormulaWordCountException(); + } + + return (int) ChronoUnit.DAYS.between(BASE, LocalDate.now()) % wordCount; + } +} diff --git a/src/main/java/wordle/domain/FileWordBook.java b/src/main/java/wordle/domain/FileWordBook.java new file mode 100644 index 00000000..78245852 --- /dev/null +++ b/src/main/java/wordle/domain/FileWordBook.java @@ -0,0 +1,37 @@ +package wordle.domain; + +import java.util.List; +import java.util.Optional; +import wordle.infra.FileReader; + +public class FileWordBook implements WordBook { + + public static final String FILE_PATH = "words.txt"; + private final List 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()); + return words.get(index); + } + + @Override + public boolean exist(Word word) { + return words.contains(word); + } + + @Override + public Optional find(String target) { + Word targetWord = new Word(target); + return words.stream() + .filter(targetWord::equals) + .findFirst(); + } +} diff --git a/src/main/java/wordle/domain/Letter.java b/src/main/java/wordle/domain/Letter.java new file mode 100644 index 00000000..28e35445 --- /dev/null +++ b/src/main/java/wordle/domain/Letter.java @@ -0,0 +1,40 @@ +package wordle.domain; + +import java.util.Objects; + +public class Letter { + + private final Alphabet alphabet; + private final Position position; + + public Letter(final char alphabet, final int position) { + this.alphabet = new Alphabet(alphabet); + this.position = new Position(position); + } + + public boolean isSameAlphabet(Letter letter) { + return letter.alphabet.equals(this.alphabet); + } + + public Position getPosition() { + return position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Letter letter = (Letter) o; + return Objects.equals(alphabet, letter.alphabet) && Objects.equals(position, + letter.position); + } + + @Override + public int hashCode() { + return Objects.hash(alphabet, position); + } +} diff --git a/src/main/java/wordle/domain/Position.java b/src/main/java/wordle/domain/Position.java new file mode 100644 index 00000000..cc6d5159 --- /dev/null +++ b/src/main/java/wordle/domain/Position.java @@ -0,0 +1,39 @@ +package wordle.domain; + +import java.util.Objects; +import wordle.exception.InvalidPositionException; + +public class Position implements Comparable { + + private static final int MIN_POSITION = 0; + private final int position; + + public Position(int position) { + if (position < MIN_POSITION) { + throw new InvalidPositionException("0보다 작은 위치는 존재할 수 없습니다."); + } + + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Position position1 = (Position) o; + return position == position1.position; + } + + @Override + public int hashCode() { + return Objects.hash(position); + } + + public int compareTo(Position position) { + return Integer.compare(this.position, position.position); + } +} diff --git a/src/main/java/wordle/domain/Record.java b/src/main/java/wordle/domain/Record.java new file mode 100644 index 00000000..f0f58d74 --- /dev/null +++ b/src/main/java/wordle/domain/Record.java @@ -0,0 +1,42 @@ +package wordle.domain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Record implements Iterable { + + public static final int MAX_COUNT = 6; + private final List record; + + public Record() { + this.record = new ArrayList<>(); + } + + public void add(Results results) { + record.add(results); + } + + public boolean isCountOver() { + return record.size() >= MAX_COUNT; + } + + public boolean existAnswer() { + if (record.isEmpty()) { + return false; + } + + return record + .getLast() + .isAnswer(); + } + + @Override + public Iterator iterator() { + return record.iterator(); + } + + public int size() { + return record.size(); + } +} diff --git a/src/main/java/wordle/domain/Result.java b/src/main/java/wordle/domain/Result.java new file mode 100644 index 00000000..f27600cd --- /dev/null +++ b/src/main/java/wordle/domain/Result.java @@ -0,0 +1,56 @@ +package wordle.domain; + +import java.util.Objects; + +public class Result implements Comparable { + + private final Tile tile; + private final Position position; + + public Result(Tile tile, int position) { + this(tile, new Position(position)); + } + + public Result(Tile tile, Position position) { + this.tile = tile; + this.position = position; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Result result = (Result) o; + return tile == result.tile && Objects.equals(position, result.position); + } + + @Override + public int hashCode() { + return Objects.hash(tile, position); + } + + public boolean isSamePosition(Position position) { + return this.position.equals(position); + } + + @Override + public int compareTo(Result o) { + return position.compareTo(o.position); + } + + public boolean isGreen() { + return this.tile == Tile.GREEN; + } + + public boolean isYellow() { + return this.tile == Tile.YELLOW; + } + + public boolean isGray() { + return this.tile == Tile.GRAY; + } +} diff --git a/src/main/java/wordle/domain/Results.java b/src/main/java/wordle/domain/Results.java new file mode 100644 index 00000000..49990c9f --- /dev/null +++ b/src/main/java/wordle/domain/Results.java @@ -0,0 +1,32 @@ +package wordle.domain; + +import java.util.Iterator; +import java.util.SortedSet; +import java.util.TreeSet; + +public class Results implements Iterable { + + private final SortedSet results; + + public Results() { + this.results = new TreeSet<>(); + } + + @Override + public Iterator iterator() { + return results.iterator(); + } + + public void add(Result result) { + results.add(result); + } + + public boolean isCheckedPosition(Position position) { + return results.stream() + .anyMatch(result -> result.isSamePosition(position)); + } + + public boolean isAnswer() { + return results.stream().allMatch(Result::isGreen); + } +} diff --git a/src/main/java/wordle/domain/Tile.java b/src/main/java/wordle/domain/Tile.java new file mode 100644 index 00000000..5f0c14f8 --- /dev/null +++ b/src/main/java/wordle/domain/Tile.java @@ -0,0 +1,5 @@ +package wordle.domain; + +public enum Tile { + GREEN, YELLOW, GRAY +} diff --git a/src/main/java/wordle/domain/Word.java b/src/main/java/wordle/domain/Word.java new file mode 100644 index 00000000..bcbe174d --- /dev/null +++ b/src/main/java/wordle/domain/Word.java @@ -0,0 +1,105 @@ +package wordle.domain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import wordle.exception.InvalidWordException; + +public class Word implements Iterable { + + public static final int WORD_LENGTH = 5; + private final List letters; + + public Word(String word) { + if (word.length() != WORD_LENGTH) { + throw new InvalidWordException("단어는 5글자여야 합니다. (" + word + ")"); + } + + this.letters = new ArrayList<>(); + for (int i = 0; i < word.length(); i++) { + Letter letter = new Letter(word.charAt(i), i); + this.letters.add(letter); + } + } + + public Results compare(Word targetWord) { + WordComparator wordComparator = new WordComparator(this.letters); + return wordComparator.compare(targetWord); + } + + static class WordComparator { + + private final List pendingLetters; + private final Results results; + + public WordComparator(List letters) { + this.pendingLetters = new ArrayList<>(letters); + this.results = new Results(); + } + + public Results compare(Word targetWord) { + for (Letter letter : targetWord) { + process(letter, letter::equals, Tile.GREEN); + } + + for (Letter letter : targetWord) { + process(letter, letter::isSameAlphabet, Tile.YELLOW); + } + + for (Letter letter : targetWord) { + fillEmptyToGray(letter); + } + + return results; + } + + private void process(Letter targetLetter, Predicate predicate, Tile tile) { + Position position = targetLetter.getPosition(); + if (results.isCheckedPosition(position)) { + return; + } + + pendingLetters.stream() + .filter(predicate) + .findFirst() + .ifPresent(letter -> { + pendingLetters.remove(letter); + results.add(new Result(tile, position)); + }); + } + + private void fillEmptyToGray(Letter targetLetter) { + Position position = targetLetter.getPosition(); + if (results.isCheckedPosition(position)) { + return; + } + + pendingLetters.forEach( + letter -> results.add(new Result(Tile.GRAY, position))); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Word word = (Word) o; + return Objects.equals(letters, word.letters); + } + + @Override + public int hashCode() { + return Objects.hash(letters); + } + + @Override + public Iterator iterator() { + return this.letters.iterator(); + } +} diff --git a/src/main/java/wordle/domain/WordBook.java b/src/main/java/wordle/domain/WordBook.java new file mode 100644 index 00000000..6e3d89f3 --- /dev/null +++ b/src/main/java/wordle/domain/WordBook.java @@ -0,0 +1,12 @@ +package wordle.domain; + +import java.util.Optional; + +public interface WordBook { + + Word pick(AnswerFormula answerFormula); + + boolean exist(Word word); + + Optional find(String word); +} diff --git a/src/main/java/wordle/exception/AnswerFormulaWordCountException.java b/src/main/java/wordle/exception/AnswerFormulaWordCountException.java new file mode 100644 index 00000000..535b4b5a --- /dev/null +++ b/src/main/java/wordle/exception/AnswerFormulaWordCountException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class AnswerFormulaWordCountException extends WordleException { + + public AnswerFormulaWordCountException() { + super("계산을 위해서 단어는 1글자 이상이어야 합니다."); + } +} diff --git a/src/main/java/wordle/exception/FileReadFailException.java b/src/main/java/wordle/exception/FileReadFailException.java new file mode 100644 index 00000000..6489244c --- /dev/null +++ b/src/main/java/wordle/exception/FileReadFailException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class FileReadFailException extends WordleException { + + public FileReadFailException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/exception/InvalidAlphabetException.java b/src/main/java/wordle/exception/InvalidAlphabetException.java new file mode 100644 index 00000000..2d92d7bf --- /dev/null +++ b/src/main/java/wordle/exception/InvalidAlphabetException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class InvalidAlphabetException extends WordleInvalidInputException { + + public InvalidAlphabetException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/exception/InvalidPositionException.java b/src/main/java/wordle/exception/InvalidPositionException.java new file mode 100644 index 00000000..d18f2c4a --- /dev/null +++ b/src/main/java/wordle/exception/InvalidPositionException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class InvalidPositionException extends WordleInvalidInputException { + + public InvalidPositionException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/exception/InvalidWordException.java b/src/main/java/wordle/exception/InvalidWordException.java new file mode 100644 index 00000000..2979e5fc --- /dev/null +++ b/src/main/java/wordle/exception/InvalidWordException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class InvalidWordException extends WordleInvalidInputException { + + public InvalidWordException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/exception/WordNotExistException.java b/src/main/java/wordle/exception/WordNotExistException.java new file mode 100644 index 00000000..7580a929 --- /dev/null +++ b/src/main/java/wordle/exception/WordNotExistException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class WordNotExistException extends WordleInvalidInputException { + + public WordNotExistException(String word) { + super("올바르지 않은 단어입니다. (" + word + ")"); + } +} diff --git a/src/main/java/wordle/exception/WordleException.java b/src/main/java/wordle/exception/WordleException.java new file mode 100644 index 00000000..a1b03ae4 --- /dev/null +++ b/src/main/java/wordle/exception/WordleException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class WordleException extends RuntimeException { + + public WordleException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/exception/WordleInvalidInputException.java b/src/main/java/wordle/exception/WordleInvalidInputException.java new file mode 100644 index 00000000..13aa7d21 --- /dev/null +++ b/src/main/java/wordle/exception/WordleInvalidInputException.java @@ -0,0 +1,8 @@ +package wordle.exception; + +public class WordleInvalidInputException extends WordleException { + + public WordleInvalidInputException(String message) { + super(message); + } +} diff --git a/src/main/java/wordle/infra/FileReader.java b/src/main/java/wordle/infra/FileReader.java new file mode 100644 index 00000000..7f1d4927 --- /dev/null +++ b/src/main/java/wordle/infra/FileReader.java @@ -0,0 +1,33 @@ +package wordle.infra; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import wordle.exception.FileReadFailException; + +public class FileReader { + + public List readByLine(String filePath) { + if (filePath == null || filePath.isBlank()) { + throw new FileReadFailException("파일 경로가 비었습니다."); + } + + try { + URL systemResource = getUrl(filePath); + return Files.lines(Path.of(systemResource.toURI())).toList(); + } catch (IOException | URISyntaxException e) { + throw new FileReadFailException("올바르지 않은 파일입니다. ("+ filePath +")"); + } + } + + private URL getUrl(String filePath) { + URL systemResource = ClassLoader.getSystemResource(filePath); + if (systemResource == null) { + throw new FileReadFailException("올바르지 않은 파일 경로입니다. (" + filePath + ")"); + } + return systemResource; + } +} diff --git a/src/main/java/wordle/ui/ConsoleInputView.java b/src/main/java/wordle/ui/ConsoleInputView.java new file mode 100644 index 00000000..4af1e068 --- /dev/null +++ b/src/main/java/wordle/ui/ConsoleInputView.java @@ -0,0 +1,17 @@ +package wordle.ui; + +import java.util.Scanner; + +public class ConsoleInputView implements InputView { + + private final Scanner scanner; + + public ConsoleInputView() { + this.scanner = new Scanner(System.in); + } + + @Override + public String input() { + return scanner.nextLine(); + } +} diff --git a/src/main/java/wordle/ui/ConsoleOutputView.java b/src/main/java/wordle/ui/ConsoleOutputView.java new file mode 100644 index 00000000..22d436bd --- /dev/null +++ b/src/main/java/wordle/ui/ConsoleOutputView.java @@ -0,0 +1,61 @@ +package wordle.ui; + +import wordle.domain.Record; +import wordle.domain.Result; +import wordle.domain.Results; + +public class ConsoleOutputView implements OutputView { + + @Override + public void welcome() { + System.out.printf("WORDLE을 %d번 만에 맞춰 보세요.\n", Record.MAX_COUNT); + System.out.println("시도의 결과는 타일의 색 변화로 나타납니다."); + } + + @Override + public void askAnswer() { + System.out.println("정답을 입력해 주세요."); + } + + @Override + public void showRecord(Record record) { + for (Results results : record) { + showResults(results); + System.out.println(); + } + System.out.println(); + } + + private static void showResults(Results results) { + for (Result result : results) { + System.out.print(findTile(result)); + } + } + + private static String findTile(Result result) { + if (result.isGreen()) { + return "🟩"; + } + if (result.isYellow()) { + return "🟨"; + } + return "⬜"; + } + + @Override + public void successEnd(Record record) { + System.out.printf("\n%d/%d\n\n", record.size(), Record.MAX_COUNT); + showRecord(record); + } + + @Override + public void failEnd(Record record) { + System.out.printf("\nX/%d\n\n", Record.MAX_COUNT); + showRecord(record); + } + + @Override + public void unexpectedEnd(String message) { + System.out.println(message); + } +} diff --git a/src/main/java/wordle/ui/InputView.java b/src/main/java/wordle/ui/InputView.java new file mode 100644 index 00000000..45645717 --- /dev/null +++ b/src/main/java/wordle/ui/InputView.java @@ -0,0 +1,6 @@ +package wordle.ui; + +public interface InputView { + + String input(); +} diff --git a/src/main/java/wordle/ui/OutputView.java b/src/main/java/wordle/ui/OutputView.java new file mode 100644 index 00000000..b105ee71 --- /dev/null +++ b/src/main/java/wordle/ui/OutputView.java @@ -0,0 +1,18 @@ +package wordle.ui; + +import wordle.domain.Record; + +public interface OutputView { + + void welcome(); + + void askAnswer(); + + void showRecord(Record record); + + void successEnd(Record record); + + void failEnd(Record record); + + void unexpectedEnd(String message); +} diff --git a/src/test/java/wordle/ApplicationTest.java b/src/test/java/wordle/ApplicationTest.java new file mode 100644 index 00000000..87894cf0 --- /dev/null +++ b/src/test/java/wordle/ApplicationTest.java @@ -0,0 +1,38 @@ +package wordle; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ApplicationTest extends ConsoleIntegrationTest { + + @Test + void 게임_정상_진행_테스트() { + run("hello", "12345" ,"label", "spell", "spill"); + assertThat(output()).containsIgnoringWhitespaces( + """ + ⬜⬜🟨🟩⬜ + """, + """ + ⬜⬜🟨🟩⬜ + 🟨⬜⬜⬜🟩 + """, + """ + ⬜⬜🟨🟩⬜ + 🟨⬜⬜⬜🟩 + 🟩🟩⬜🟩🟩 + """, + """ + ⬜⬜🟨🟩⬜ + 🟨⬜⬜⬜🟩 + 🟩🟩⬜🟩🟩 + 🟩🟩🟩🟩🟩 + """ + ); + } + + @Override + public void runMain() { + Application.main(new String[]{}); + } +} diff --git a/src/test/java/wordle/ConsoleIntegrationTest.java b/src/test/java/wordle/ConsoleIntegrationTest.java new file mode 100644 index 00000000..c5fdec92 --- /dev/null +++ b/src/test/java/wordle/ConsoleIntegrationTest.java @@ -0,0 +1,65 @@ +package wordle; + +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.Mockito.mockStatic; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.time.Duration; +import java.time.LocalDate; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public abstract class ConsoleIntegrationTest { + + private static final LocalDate ANSWER_DATE = LocalDate.parse("2021-06-22"); + public static final long TIME_LIMIT = 10L; + private PrintStream standardOut; + private OutputStream captor; + + @BeforeEach + protected final void init() { + standardOut = System.out; + captor = new ByteArrayOutputStream(); + System.setOut(new PrintStream(captor)); + } + + @AfterEach + protected final void printOutput() { + System.setOut(standardOut); + System.out.println(output()); + } + + protected final String output() { + return captor.toString().trim(); + } + + protected final void run(final String... args) { + assertTimeTest(() -> { + try (MockedStatic localDateMockedStatic = mockStatic(LocalDate.class, + Mockito.CALLS_REAL_METHODS)) { + localDateMockedStatic.when(LocalDate::now).thenReturn(ANSWER_DATE); + command(args); + runMain(); + } catch (final Exception e) { + e.printStackTrace(); + } + }); + } + + private void assertTimeTest(Executable executable) { + assertTimeoutPreemptively(Duration.ofSeconds(TIME_LIMIT), executable); + } + + private void command(final String... args) { + final byte[] buf = String.join("\n", args).getBytes(); + System.setIn(new ByteArrayInputStream(buf)); + } + + protected abstract void runMain(); +} diff --git a/src/test/java/wordle/TimeTestSupporter.java b/src/test/java/wordle/TimeTestSupporter.java new file mode 100644 index 00000000..869861d3 --- /dev/null +++ b/src/test/java/wordle/TimeTestSupporter.java @@ -0,0 +1,22 @@ +package wordle; + +import static org.mockito.Mockito.mockStatic; + +import java.time.LocalDate; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class TimeTestSupporter { + + public static final LocalDate mockedDate = LocalDate.of(2024, 6, 12); + + public static void runWithMock(final LocalDate mockDate, final Runnable runnable) { + try (MockedStatic localDateMockedStatic = mockStatic(LocalDate.class, + Mockito.CALLS_REAL_METHODS)) { + localDateMockedStatic.when(LocalDate::now).thenReturn(mockDate); + runnable.run(); + } catch (final Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/wordle/domain/AlphabetTest.java b/src/test/java/wordle/domain/AlphabetTest.java new file mode 100644 index 00000000..1544509c --- /dev/null +++ b/src/test/java/wordle/domain/AlphabetTest.java @@ -0,0 +1,33 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import wordle.exception.InvalidAlphabetException; + +public class AlphabetTest { + + @Test + void 알파벳은_대소문자를_구분하지_않는다() { + final Alphabet alphabetA = new Alphabet('a'); + + assertThat(alphabetA).isEqualTo(new Alphabet('A')); + } + + @Test + void 알파벳이_같으면_동등한_객체이다() { + final Alphabet alphabetA = new Alphabet('a'); + + assertThat(alphabetA).isEqualTo(new Alphabet('a')); + } + + @ParameterizedTest + @ValueSource(chars = {'1', 'ㄱ', ' ', '!', '.'}) + void 알파벳이_아니면_예외가_발생한다(final char input) { + assertThatThrownBy(() -> new Alphabet(input)) + .isInstanceOf(InvalidAlphabetException.class); + } +} diff --git a/src/test/java/wordle/domain/BaseAnswerFormulaTest.java b/src/test/java/wordle/domain/BaseAnswerFormulaTest.java new file mode 100644 index 00000000..f2822b8b --- /dev/null +++ b/src/test/java/wordle/domain/BaseAnswerFormulaTest.java @@ -0,0 +1,34 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import wordle.TimeTestSupporter; +import wordle.exception.AnswerFormulaWordCountException; + +public class BaseAnswerFormulaTest { + + @ParameterizedTest + @CsvSource(value = {"10:9", "100:89", "10000:1089"}, delimiter = ':') + void 오늘의_정답_공식을_생성할_수_있다(int wordCount, long expected) { + TimeTestSupporter.runWithMock(TimeTestSupporter.mockedDate, () -> { + BaseAnswerFormula answerFormula = new BaseAnswerFormula(); + + int index = answerFormula.calculate(wordCount); + + assertThat(index).isEqualTo(expected); + }); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + void 파라미터는_1보다_작을_수_없다(int wordCount) { + BaseAnswerFormula answerFormula = new BaseAnswerFormula(); + + assertThatThrownBy(() -> answerFormula.calculate(wordCount)) + .isInstanceOf(AnswerFormulaWordCountException.class); + } +} diff --git a/src/test/java/wordle/domain/FileWordBookTest.java b/src/test/java/wordle/domain/FileWordBookTest.java new file mode 100644 index 00000000..baab6bfa --- /dev/null +++ b/src/test/java/wordle/domain/FileWordBookTest.java @@ -0,0 +1,40 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import wordle.infra.FileReader; + +public class FileWordBookTest { + + private WordBook wordBook; + + @BeforeEach + void setUp() { + wordBook = new FileWordBook(new FileReader()); + } + + @ParameterizedTest + @CsvSource(value = {"hello:true", "exist:false" }, delimiter = ':') + void WordBook에_단어가_존재하는지_확인_할_수있다(String input, boolean expected) { + final boolean actual = wordBook.exist(new Word(input)); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void WordBook에서_공식을_통해_오늘의_정답_단어를_뽑는다() { + Word actual = wordBook.pick((wordCount) -> 0); + assertThat(actual).isEqualTo(new Word("hello")); + } + + @Test + void WordBook에서_단어를_찾을_수_있다() { + Word word = wordBook.find("spill").get(); + + assertThat(word).isNotNull(); + } +} diff --git a/src/test/java/wordle/domain/LetterTest.java b/src/test/java/wordle/domain/LetterTest.java new file mode 100644 index 00000000..b91e63fc --- /dev/null +++ b/src/test/java/wordle/domain/LetterTest.java @@ -0,0 +1,40 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +public class LetterTest { + + @Test + void Letter를_생성한다() { + assertDoesNotThrow(() -> new Letter('a', 0)); + } + @Test + void 알파벳과_위치가_같으면_동등한_객체이다() { + Letter letter = new Letter('a', 0); + + assertThat(letter).isEqualTo(new Letter('a', 0)); + } + @Test + void 알파벳과이_다르면_동등하지_않은_객체이다() { + Letter letter = new Letter('a', 0); + + assertThat(letter).isNotEqualTo(new Letter('b', 0)); + } + + @Test + void 위치가_다르면_동등하지_않은_객체이다() { + Letter letter = new Letter('a', 0); + + assertThat(letter).isNotEqualTo(new Letter('a', 1)); + } + + @Test + void 알파벳이_같은지_확인할_수_있다() { + Letter letter = new Letter('a', 0); + + assertThat(letter.isSameAlphabet(new Letter('a', 1))).isTrue(); + } +} diff --git a/src/test/java/wordle/domain/PositionTest.java b/src/test/java/wordle/domain/PositionTest.java new file mode 100644 index 00000000..913e444e --- /dev/null +++ b/src/test/java/wordle/domain/PositionTest.java @@ -0,0 +1,34 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import wordle.exception.InvalidPositionException; + +public class PositionTest { + + @Test + void Position은_음수가_될_수_없다() { + assertThatThrownBy(() -> new Position(-1)) + .isInstanceOf(InvalidPositionException.class); + } + + @Test + void Position이_같으면_동등한_객체이다() { + Position position = createPosition(0); + + assertThat(position).isEqualTo(createPosition(0)); + } + + @Test + void Position이_다르면_동등하지않은_객체이다() { + Position position = createPosition(0); + + assertThat(position).isNotEqualTo(createPosition(1)); + } + + private static Position createPosition(int position1) { + return new Position(position1); + } +} diff --git a/src/test/java/wordle/domain/RecordTest.java b/src/test/java/wordle/domain/RecordTest.java new file mode 100644 index 00000000..14e7b4ab --- /dev/null +++ b/src/test/java/wordle/domain/RecordTest.java @@ -0,0 +1,45 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import wordle.fixture.ResultFixture; + +public class RecordTest { + + @Test + void Record에_초록색타일_5개_인_결과모음이_있으면_종료여부가_true이다() { + Record record = createAllGreenRecord(); + + assertThat(record.existAnswer()).isTrue(); + } + + @Test + void Record가_결과모음이_6개이상이면_종료여부가_true이다() { + Record record = createAllGrayRecord(); + + assertThat(record.isCountOver()).isTrue(); + } + + private static Record createAllGreenRecord() { + Record record = new Record(); + + record.add(ResultFixture.createGreenResults(5)); + return record; + } + + private static Record createAllGrayRecord() { + Record record = new Record(); + + for (int i = 0; i < Record.MAX_COUNT; i++) { + record.add(ResultFixture.createResults( + Tile.GRAY, + Tile.GRAY, + Tile.GRAY, + Tile.GRAY, + Tile.GRAY) + ); + } + return record; + } +} diff --git a/src/test/java/wordle/domain/ResultTest.java b/src/test/java/wordle/domain/ResultTest.java new file mode 100644 index 00000000..e7b37062 --- /dev/null +++ b/src/test/java/wordle/domain/ResultTest.java @@ -0,0 +1,42 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; +import wordle.fixture.ResultFixture; + +public class ResultTest { + @Test + void Result_생성_테스트() { + assertDoesNotThrow(()->new Result(Tile.GREEN, 0)); + } + + @Test + void 타일과_위치가_같으면_동등한_객체이다() { + Result result = ResultFixture.createGreenResult(0); + + assertThat(result).isEqualTo(new Result(Tile.GREEN, 0)); + } + + @Test + void 타일이_초록색인지_확인할_수_있다() { + Result result = ResultFixture.createGreenResult(0); + + assertThat(result.isGreen()).isTrue(); + } + + @Test + void 타일이_노란색인지_확인할_수_있다() { + Result result = ResultFixture.createYellowResult(0); + + assertThat(result.isYellow()).isTrue(); + } + + @Test + void 타일이_회색인지_확인할_수_있다() { + Result result = ResultFixture.createGrayResult(0); + + assertThat(result.isGray()).isTrue(); + } +} diff --git a/src/test/java/wordle/domain/ResultsTest.java b/src/test/java/wordle/domain/ResultsTest.java new file mode 100644 index 00000000..4bb9c29c --- /dev/null +++ b/src/test/java/wordle/domain/ResultsTest.java @@ -0,0 +1,28 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import wordle.fixture.ResultFixture; + +class ResultsTest { + + @Test + void Results_생성테스트(){ + Results results = ResultFixture.createGreenResults(5); + + assertThat(results).containsExactly( + ResultFixture.createGreenResult(0), + ResultFixture.createGreenResult(1), + ResultFixture.createGreenResult(2), + ResultFixture.createGreenResult(3), + ResultFixture.createGreenResult(4)); + } + + @Test + void Results_가_전부_초록색타일인지_알수있다() { + Results results = ResultFixture.createGreenResults(5); + + assertThat(results.isAnswer()).isTrue(); + } +} \ No newline at end of file diff --git a/src/test/java/wordle/domain/WordTest.java b/src/test/java/wordle/domain/WordTest.java new file mode 100644 index 00000000..f5fb8111 --- /dev/null +++ b/src/test/java/wordle/domain/WordTest.java @@ -0,0 +1,116 @@ +package wordle.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import wordle.exception.InvalidWordException; +import wordle.fixture.ResultFixture; + +public class WordTest { + + @Test + void Word를_생성한다() { + assertDoesNotThrow(() -> new Word("apple")); + } + + @ParameterizedTest + @ValueSource(strings = {"test", "testss"}) + void Word를_생성할_때_다섯글자가_아니면_실패한다(String input) { + assertThatThrownBy(() -> new Word(input)) + .isInstanceOf(InvalidWordException.class); + } + + @Test + void 같은_Word를_비교하면_초록_결과들을_반환한다(){ + Word baseWord = new Word("abcde"); + Word targetWord = new Word("abcde"); + + Results results = baseWord.compare(targetWord); + + assertThat(results).containsExactly( + ResultFixture.createGreenResult(0), + ResultFixture.createGreenResult(1), + ResultFixture.createGreenResult(2), + ResultFixture.createGreenResult(3), + ResultFixture.createGreenResult(4)); + } + + @Test + void 글자는_같지만_위치가_전부_다른_Word를_비교하면_노란_결과들을_반환한다(){ + Word baseWord = new Word("abcde"); + Word targetWord = new Word("edbac"); + + Results results = baseWord.compare(targetWord); + + assertThat(results).containsExactly( + ResultFixture.createYellowResult(0), + ResultFixture.createYellowResult(1), + ResultFixture.createYellowResult(2), + ResultFixture.createYellowResult(3), + ResultFixture.createYellowResult(4)); + } + + @Test + void 글자와_위치가_일부가_같은_Word를_비교하면_초록_노란_결과들을_반환한다(){ + Word baseWord = new Word("abcde"); + Word targetWord = new Word("edcba"); + + Results results = baseWord.compare(targetWord); + + assertThat(results).containsExactly( + ResultFixture.createYellowResult(0), + ResultFixture.createYellowResult(1), + ResultFixture.createGreenResult(2), + ResultFixture.createYellowResult(3), + ResultFixture.createYellowResult(4)); + } + + @Test + void 글자와_위치가_전부_다른_Word를_비교하면_회색_결과들을_반환한다(){ + Word baseWord = new Word("abcde"); + Word targetWord = new Word("fghij"); + + Results results = baseWord.compare(targetWord); + + assertThat(results).containsExactly( + ResultFixture.createGrayResult(0), + ResultFixture.createGrayResult(1), + ResultFixture.createGrayResult(2), + ResultFixture.createGrayResult(3), + ResultFixture.createGrayResult(4)); + } + + @Test + void 글자와_위치가_전부_같은게_두개_위치만_다른게_한개_전부_다른게_두개인_Word를_비교하면_결과들을_반환한다(){ + Word answerWord = new Word("abcde"); // 정답 + Word inputWord = new Word("abejk"); // input + + Results results = answerWord.compare(inputWord); + + assertThat(results).containsExactly( + ResultFixture.createGreenResult(0), + ResultFixture.createGreenResult(1), + ResultFixture.createYellowResult(2), + ResultFixture.createGrayResult(3), + ResultFixture.createGrayResult(4)); + } + + @Test + void 같은_문자가_2개_입력되었을_때_해당_문자가_정답에_하나만_존재하지만_위치가_틀린_경우_첫번째_문자만_노란색으로_반환된다(){ + Word answerWord = new Word("lurid"); // 정답 + Word inputWord = new Word("hello"); // input + + Results results = answerWord.compare(inputWord); + + assertThat(results).containsExactly( + ResultFixture.createGrayResult(0), + ResultFixture.createGrayResult(1), + ResultFixture.createYellowResult(2), + ResultFixture.createGrayResult(3), + ResultFixture.createGrayResult(4)); + } +} diff --git a/src/test/java/wordle/fixture/RecordFixture.java b/src/test/java/wordle/fixture/RecordFixture.java new file mode 100644 index 00000000..e3d3efa1 --- /dev/null +++ b/src/test/java/wordle/fixture/RecordFixture.java @@ -0,0 +1,17 @@ +package wordle.fixture; + +import wordle.domain.Record; +import wordle.domain.Results; + +public class RecordFixture { + + public static Record create(Results results, Results... resultsList) { + Record record = new Record(); + record.add(results); + for (Results value : resultsList) { + record.add(value); + } + return record; + } + +} diff --git a/src/test/java/wordle/fixture/ResultFixture.java b/src/test/java/wordle/fixture/ResultFixture.java new file mode 100644 index 00000000..e8e61366 --- /dev/null +++ b/src/test/java/wordle/fixture/ResultFixture.java @@ -0,0 +1,38 @@ +package wordle.fixture; + +import java.util.stream.IntStream; +import wordle.domain.Result; +import wordle.domain.Results; +import wordle.domain.Tile; + +public class ResultFixture { + + public static Results createGreenResults(int count) { + Results results = new Results(); + IntStream.range(0, count) + .mapToObj(ResultFixture::createGreenResult) + .forEach(results::add); + return results; + } + + public static Results createResults(Tile tile, Tile... tiles) { + Results results = new Results(); + results.add(new Result(tile, 0)); + IntStream.range(1, tiles.length + 1) + .mapToObj(idx -> new Result(tiles[idx - 1], idx)) + .forEach(results::add); + return results; + } + + public static Result createGreenResult(int position) { + return new Result(Tile.GREEN, position); + } + + public static Result createYellowResult(int position) { + return new Result(Tile.YELLOW, position); + } + + public static Result createGrayResult(int position) { + return new Result(Tile.GRAY, position); + } +} diff --git a/src/test/java/wordle/infra/FileReaderTest.java b/src/test/java/wordle/infra/FileReaderTest.java new file mode 100644 index 00000000..e66e3203 --- /dev/null +++ b/src/test/java/wordle/infra/FileReaderTest.java @@ -0,0 +1,37 @@ +package wordle.infra; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import wordle.exception.FileReadFailException; + +public class FileReaderTest { + + private FileReader fileReader; + + @BeforeEach + public void setUp() { + fileReader = new FileReader(); + } + + @Test + void FileReader로_텍스트파일을_읽어온다() { + List textList = fileReader.readByLine("words.txt"); + + assertThat(textList).containsExactly("hello","label","spell","spill"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"noExist.txt"}) + void FileReader로_없는_파일을_읽어오면_예외를_던진다(final String input) { + assertThatThrownBy(() -> fileReader.readByLine(input)) + .isInstanceOf(FileReadFailException.class); + } +} diff --git a/src/test/java/wordle/study/TimeTest.java b/src/test/java/wordle/study/TimeTest.java new file mode 100644 index 00000000..69209680 --- /dev/null +++ b/src/test/java/wordle/study/TimeTest.java @@ -0,0 +1,25 @@ +package wordle.study; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import wordle.TimeTestSupporter; + +public class TimeTest { + + private static final LocalDate mockedDate = LocalDate.of(2024, 6, 12); + + // test(Time): LocalDate 모킹 유틸 추가 + @Test + void LocalDate를_mocking_할_수_있다() { + TimeTestSupporter.runWithMock(mockedDate, () -> { + LocalDate now = LocalDate.now(); + LocalDate base = LocalDate.of(2021, 6, 19); + + long ans = ChronoUnit.DAYS.between(base, now); + + Assertions.assertThat(ans).isEqualTo(1089); + }); + } +} diff --git a/src/test/java/wordle/ui/ConsoleOutputViewTest.java b/src/test/java/wordle/ui/ConsoleOutputViewTest.java new file mode 100644 index 00000000..5f7ecc85 --- /dev/null +++ b/src/test/java/wordle/ui/ConsoleOutputViewTest.java @@ -0,0 +1,123 @@ +package wordle.ui; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import wordle.domain.Record; +import wordle.domain.Tile; +import wordle.fixture.RecordFixture; +import wordle.fixture.ResultFixture; + +class ConsoleOutputViewTest { + + private PrintStream standardOut; + private OutputStream captor; + + private ConsoleOutputView consoleOutputView; + + @BeforeEach + void init() { + standardOut = System.out; + captor = new ByteArrayOutputStream(); + consoleOutputView = new ConsoleOutputView(); + System.setOut(new PrintStream(captor)); + } + + @AfterEach + void printOutput() { + System.setOut(standardOut); + } + + @Test + void Record를_출력할_수_있다() { + Record record = createSuccessRecordFixture(); + + consoleOutputView.showRecord(record); + + assertThat(captor.toString()).isEqualTo(""" + ⬜🟩⬜🟨⬜ + ⬜⬜⬜🟨⬜ + ⬜🟩🟨🟨⬜ + ⬜🟩⬜🟨⬜ + ⬜⬜⬜⬜⬜ + 🟩🟩🟩🟩🟩 + + """); + } + + @Test + void 게임_성공종료를_출력할_수_있다() { + Record record = createSuccessRecordFixture(); + + consoleOutputView.successEnd(record); + + assertThat(captor.toString()).isEqualTo(""" + + 6/6 + + ⬜🟩⬜🟨⬜ + ⬜⬜⬜🟨⬜ + ⬜🟩🟨🟨⬜ + ⬜🟩⬜🟨⬜ + ⬜⬜⬜⬜⬜ + 🟩🟩🟩🟩🟩 + + """); + } + @Test + void 게임_실패종료를_출력할_수_있다() { + Record record = createFailureRecordFixture(); + + consoleOutputView.failEnd(record); + + assertThat(captor.toString()).isEqualTo(""" + + X/6 + + ⬜⬜⬜⬜⬜ + ⬜⬜⬜⬜⬜ + ⬜⬜⬜🟨⬜ + ⬜⬜⬜🟨🟨 + ⬜⬜🟨🟨🟨 + 🟩🟨🟨🟨🟨 + + """); + } + + private static Record createSuccessRecordFixture() { + return RecordFixture.create( + ResultFixture.createResults(Tile.GRAY, Tile.GREEN, Tile.GRAY, Tile.YELLOW, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.YELLOW, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GREEN, Tile.YELLOW, Tile.YELLOW, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GREEN, Tile.GRAY, Tile.YELLOW, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY), + ResultFixture.createResults(Tile.GREEN, Tile.GREEN, Tile.GREEN, Tile.GREEN, + Tile.GREEN) + ); + } + + private static Record createFailureRecordFixture() { + return RecordFixture.create( + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.YELLOW, + Tile.GRAY), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.YELLOW, + Tile.YELLOW), + ResultFixture.createResults(Tile.GRAY, Tile.GRAY, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW), + ResultFixture.createResults(Tile.GREEN, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, + Tile.YELLOW) + ); + } +} \ No newline at end of file diff --git a/src/test/resources/words.txt b/src/test/resources/words.txt new file mode 100644 index 00000000..71650431 --- /dev/null +++ b/src/test/resources/words.txt @@ -0,0 +1,4 @@ +hello +label +spell +spill