diff --git a/README.md b/README.md index b722ac00..e38df1bb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,41 @@ # 미션 - 워들 +## 기능 요구 사항 +* Letter + * [x] Letter 를 생성한다. + * [x] 알파벳만 입력 가능하다. + * [x] 알파벳이 아니면 에러를 발생한다. +* Words Generator + * [X] `words.txt`에서 Words 를 읽어온다. +* Word + * [x] Word 를 생성한다. + * [x] 5글자의 알파벳만 받는다. + * [x] 5글자가 아니면 에러를 발생한다. + * [x] 알파벳이 아니면 에러를 발생한다. +* Words + * [x] Words 를 생성한다. + * [x] 주어진 Words 가 비어있으면 에러를 발생한다. + * [x] 주어진 Words 에 Word 가 없다면 에러를 발생한다. + * [x] 정답을 가져온다. + * [x] ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어를 가져온다. +* 정답 + * [x] 입력된 답안을 존재하는 Word 인지 비교한다. +* 답안 + * [x] 사용자가 워드를 입력한다. + * [x] 글자는 5글자까지 입력 가능하다. + * [x] 사용자가 정답을 6번까지 입력할 수 있다. + * [x] 입력된 답안은 `words.txt`에 존재하는 단어여야 한다. +* 출력 + * [x] 타일로 결과를 표시한다. + * [x] 위치와 글자가 맞으면 초록색 + * [x] 위치가 틀리고, 글자가 맞으면 노란색 + * [x] 위치와 글자가 틀리면 회색 + * [x] 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다. + +## 유비쿼터스 언어 +* Word + * 5글자의 영문 +* Words + * `words.txt`의 리스트 ## 🔍 진행 방식 diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..573f588f --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,9 @@ +import controller.GameController; +import service.GameService; + +public class Application { + public static void main(String[] args) { + GameController gameController = new GameController(new GameService()); + gameController.start(); + } +} diff --git a/src/main/java/config/FileConfig.java b/src/main/java/config/FileConfig.java new file mode 100644 index 00000000..9aadd746 --- /dev/null +++ b/src/main/java/config/FileConfig.java @@ -0,0 +1,6 @@ +package config; + +public class FileConfig { + + public final static String FILE_PATH = "src/main/resources/words.txt"; +} diff --git a/src/main/java/controller/GameController.java b/src/main/java/controller/GameController.java new file mode 100644 index 00000000..4e5d5eb2 --- /dev/null +++ b/src/main/java/controller/GameController.java @@ -0,0 +1,24 @@ +package controller; + +import domain.Answer; +import domain.Words; +import dto.GameHistory; +import service.GameService; + +import java.time.LocalDate; +import java.util.List; + +public class GameController { + + private final GameService gameService; + + public GameController(GameService gameService) { + this.gameService = gameService; + } + + public void start() { + Words words = gameService.init(); + List gameHistories = gameService.startGame(words); + gameService.endGame(gameHistories); + } +} diff --git a/src/main/java/domain/Answer.java b/src/main/java/domain/Answer.java new file mode 100644 index 00000000..2e229b78 --- /dev/null +++ b/src/main/java/domain/Answer.java @@ -0,0 +1,71 @@ +package domain; + +import java.util.*; +import java.util.stream.Collectors; + +public class Answer { + + private final Word answer; + + private boolean isSuccess = false; + + public Answer(Word answer) { + this.answer = answer; + } + + public List compare(Word answer) { + Map letterMap = getLetterMap(); + + List result = new ArrayList<>(); + + for (int i = 0; i < Word.WORD_LENGTH; i++) { + Long count = letterMap.get(answer.getWord().get(i)); + + Tile tile = Tile.getTile(count, this.answer.getWord().get(i), answer.getWord().get(i)); + result.add(tile); + + letterMap.put(answer.getWord().get(i), letterMap.getOrDefault(answer.getWord().get(i), 0L) - 1); + } + endGame(result); + + return result; + } + + private Map getLetterMap() { + return this.answer.getWord() + .stream() + .collect(Collectors.groupingBy(c -> c, Collectors.counting())); + } + + private void endGame(List result) { + int count = Collections.frequency(result, Tile.GREEN); + + if (count == Word.WORD_LENGTH) { + this.isSuccess = true; + } + } + + public boolean isSuccess() { + return isSuccess; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Answer that = (Answer) o; + return Objects.equals(answer, that.answer); + } + + @Override + public int hashCode() { + return Objects.hash(answer); + } + + @Override + public String toString() { + return "Answer{" + + "answer=" + answer + + '}'; + } +} diff --git a/src/main/java/domain/Letter.java b/src/main/java/domain/Letter.java new file mode 100644 index 00000000..4d810db6 --- /dev/null +++ b/src/main/java/domain/Letter.java @@ -0,0 +1,43 @@ +package domain; + +import java.util.Objects; + +public class Letter { + private final String letter; + + public Letter(Character letter) { + if (this.isNotAlphabet(letter)) { + throw new IllegalArgumentException(letter + "는 알파벳이 아닙니다."); + } + + this.letter = String.valueOf(letter); + } + + private boolean isNotAlphabet(Character letter) { + return !(letter >= 'A' && letter <= 'Z') && !(letter >= 'a' && letter <= 'z'); + } + + public String getLetter() { + return letter; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Letter letter1 = (Letter) o; + return Objects.equals(letter, letter1.letter); + } + + @Override + public int hashCode() { + return Objects.hash(letter); + } + + @Override + public String toString() { + return "Letter{" + + "letter='" + letter + '\'' + + '}'; + } +} diff --git a/src/main/java/domain/Tile.java b/src/main/java/domain/Tile.java new file mode 100644 index 00000000..38cbb764 --- /dev/null +++ b/src/main/java/domain/Tile.java @@ -0,0 +1,25 @@ +package domain; + +public enum Tile { + GREEN("\uD83D\uDFE9"), YELLOW("\uD83D\uDFE8"), GRAY("⬜"); + + private final String tile; + + Tile(String tile) { + this.tile = tile; + } + + public String getTile() { + return tile; + } + + public static Tile getTile(Long count, Letter answerLetter, Letter letter) { + if (count == null || count <= 0) { + return Tile.GRAY; + } + if (answerLetter.equals(letter)) { + return Tile.GREEN; + } + return Tile.YELLOW; + } +} diff --git a/src/main/java/domain/Word.java b/src/main/java/domain/Word.java new file mode 100644 index 00000000..f321064d --- /dev/null +++ b/src/main/java/domain/Word.java @@ -0,0 +1,53 @@ +package domain; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Word { + public static final int WORD_LENGTH = 5; + + private final List word; + + public Word(String word) { + validate(word); + + this.word = word.chars().mapToObj(c -> (char) c) + .map(Letter::new) + .collect(Collectors.toList()); + } + + public void validate(String word) { + if (this.isNotMatchWord(word)) { + throw new IllegalArgumentException(word + "는 5글자의 알파벳이 아닙니다."); + } + } + + private boolean isNotMatchWord(String word){ + return word.length() != WORD_LENGTH; + } + + public List getWord() { + return word; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Word word1 = (Word) o; + return Objects.equals(word, word1.word); + } + + @Override + public int hashCode() { + return Objects.hash(word); + } + + @Override + public String toString() { + return "Word{" + + "word=" + word + + '}'; + } +} diff --git a/src/main/java/domain/Words.java b/src/main/java/domain/Words.java new file mode 100644 index 00000000..612c3a5d --- /dev/null +++ b/src/main/java/domain/Words.java @@ -0,0 +1,46 @@ +package domain; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +public class Words { + public static final LocalDate DEFAULT_DATE = LocalDate.of(2021, 6, 19); + private final List words; + + public Words(List words) { + this.words = convert(words); + } + + private List convert(List words) { + if (words.size() == 0) { + throw new IllegalArgumentException("파일이 비어 있습니다."); + } + + return words.stream().map(Word::new).collect(Collectors.toList()); + } + + public Answer getAnswer(LocalDate now) { + long betweenDay = ChronoUnit.DAYS.between(DEFAULT_DATE, now); + + Word answer = this.words.get((int) (betweenDay % words.size())); + return new Answer(answer); + } + + public Word getWord(String word) { + Word inputWord = new Word(word); + validateExist(inputWord); + return inputWord; + } + + private void validateExist(Word word) { + if (!words.contains(word)) { + throw new IllegalArgumentException("존재하지 않는 단어입니다."); + } + } + + public List getWords() { + return words; + } +} diff --git a/src/main/java/dto/GameHistory.java b/src/main/java/dto/GameHistory.java new file mode 100644 index 00000000..31567ba1 --- /dev/null +++ b/src/main/java/dto/GameHistory.java @@ -0,0 +1,20 @@ +package dto; + +import domain.Tile; + +import java.util.List; +import java.util.stream.Collectors; + +public class GameHistory { + + private final List gameHistory; + + public GameHistory(List gameHistory) { + this.gameHistory = gameHistory; + } + + public String getGameResult() { + return gameHistory.stream().map(Tile::getTile) + .collect(Collectors.joining()); + } +} diff --git a/src/main/java/service/GameService.java b/src/main/java/service/GameService.java new file mode 100644 index 00000000..b7e60027 --- /dev/null +++ b/src/main/java/service/GameService.java @@ -0,0 +1,53 @@ +package service; + +import config.FileConfig; +import domain.Answer; +import domain.Tile; +import domain.Word; +import domain.Words; +import dto.GameHistory; +import support.WordsGenerator; +import view.InputView; +import view.OutputView; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +public class GameService { + + public Words init() { + InputView.inputStartGame(); + return new Words(WordsGenerator.read(FileConfig.FILE_PATH)); + } + + public List startGame(Words words) { + List gameHistories = new ArrayList<>(); + Answer answer = words.getAnswer(LocalDate.now()); + + int count = 0; + while (isRound(answer, count)) { + Word inputWord = words.getWord(InputView.inputWord()); + List tiles = answer.compare(inputWord); + gameHistories.add(new GameHistory(tiles)); + print(answer, gameHistories); + count++; + } + + return gameHistories; + } + + private static boolean isRound(Answer answer, int count) { + return count < InputView.GAME_TOTAL_ROUND && !answer.isSuccess(); + } + + private void print(Answer answer, List gameHistories) { + if (!answer.isSuccess()) { + OutputView.outputTiles(gameHistories); + } + } + + public void endGame(List gameHistories) { + OutputView.outputEndGame(gameHistories); + } +} diff --git a/src/main/java/support/WordsGenerator.java b/src/main/java/support/WordsGenerator.java new file mode 100644 index 00000000..3570af60 --- /dev/null +++ b/src/main/java/support/WordsGenerator.java @@ -0,0 +1,20 @@ +package support; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class WordsGenerator { + + private WordsGenerator() { + } + + public static List read(String filePath) { + try { + return Files.readAllLines(Paths.get(filePath)); + } catch (IOException e) { + throw new RuntimeException("파일을 읽을 수 없습니다.", e); + } + } +} diff --git a/src/main/java/view/InputView.java b/src/main/java/view/InputView.java new file mode 100644 index 00000000..b16e7d2d --- /dev/null +++ b/src/main/java/view/InputView.java @@ -0,0 +1,24 @@ +package view; + +import java.util.Scanner; + +public class InputView { + + public static final int GAME_TOTAL_ROUND = 6; + + private static final String INPUT_START_GAME_MESSAGE + = String.format("WORDLE을 %d번 만에 맞춰 보세요.\n시도의 결과는 타일의 색 변화로 나타납니다.", GAME_TOTAL_ROUND); + + private static final String INPUT_WORD_MESSAGE = "정답을 입력해주세요."; + + private static final Scanner scanner = new Scanner(System.in); + + public static void inputStartGame() { + System.out.println(INPUT_START_GAME_MESSAGE); + } + + public static String inputWord() { + System.out.println(INPUT_WORD_MESSAGE); + return scanner.nextLine(); + } +} diff --git a/src/main/java/view/OutputView.java b/src/main/java/view/OutputView.java new file mode 100644 index 00000000..28589141 --- /dev/null +++ b/src/main/java/view/OutputView.java @@ -0,0 +1,18 @@ +package view; + +import dto.GameHistory; + +import java.util.List; + +public class OutputView { + + public static void outputTiles(List gameHistories) { + gameHistories.forEach(gameHistory -> System.out.println(gameHistory.getGameResult())); + System.out.println(); + } + + public static void outputEndGame(List gameHistories) { + System.out.println(gameHistories.size() + "/" + InputView.GAME_TOTAL_ROUND); + outputTiles(gameHistories); + } +} diff --git a/src/test/java/domain/AnswerTest.java b/src/test/java/domain/AnswerTest.java new file mode 100644 index 00000000..67a86afc --- /dev/null +++ b/src/test/java/domain/AnswerTest.java @@ -0,0 +1,53 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnswerTest { + + @DisplayName("정답을 모두 맟췄을 때 모두 초록색이 나온다.") + @Test + void test01() { + Answer answer = new Answer(new Word("spill")); + + List tiles = answer.compare(new Word("spill")); + + assertThat(tiles).containsExactly(Tile.GREEN, Tile.GREEN, Tile.GREEN, Tile.GREEN, Tile.GREEN); + } + + @DisplayName("정답을 모두 빗나갔을 때 모두 노란색이 나온다.") + @Test + void test02() { + Answer answer = new Answer(new Word("spill")); + + List tiles = answer.compare(new Word("llsip")); + + assertThat(tiles).containsExactly(Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW); + } + + @DisplayName("정답을 모두 틀렸을 때 모두 회색이 나온다.") + @Test + void test03() { + Answer answer = new Answer(new Word("spill")); + + List tiles = answer.compare(new Word("mongo")); + + assertThat(tiles).containsExactly(Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY, Tile.GRAY); + } + + @DisplayName("두 번 호출 시 동일한 값이 나온다.") + @Test + void test04() { + Answer answer = new Answer(new Word("spill")); + + List tiles1 = answer.compare(new Word("llsip")); + assertThat(tiles1).containsExactly(Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW); + + List tiles2 = answer.compare(new Word("llsip")); + assertThat(tiles2).containsExactly(Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW, Tile.YELLOW); + } +} \ No newline at end of file diff --git a/src/test/java/domain/LetterTest.java b/src/test/java/domain/LetterTest.java new file mode 100644 index 00000000..26b8700c --- /dev/null +++ b/src/test/java/domain/LetterTest.java @@ -0,0 +1,23 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +class LetterTest { + + @DisplayName("Letter 을 생성한다.") + @Test + void test01() { + Letter letter = new Letter('A'); + + assertThat(letter.getLetter()).isEqualTo("A"); + } + + @DisplayName("Letter 에 숫자가 들어가면 에러가 발생한다.") + @Test + void test02() { + assertThatThrownBy(() -> new Letter('1')).isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/TileTest.java b/src/test/java/domain/TileTest.java new file mode 100644 index 00000000..ebfc40bb --- /dev/null +++ b/src/test/java/domain/TileTest.java @@ -0,0 +1,39 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class TileTest { + + @DisplayName("답안과 정답의 글자가 동일하고 워드에 들어있는 동일한 글자의 합계가 1이상이면 초록색 타일을 반환한다.") + @Test + void test01() { + Long count = 1L; + Letter answerLetter = new Letter('I'); + Letter letter = new Letter('I'); + + assertThat(Tile.getTile(count, answerLetter, letter)).isEqualTo(Tile.GREEN); + } + + @DisplayName("답안과 정답이 동일하지 않고 워드에 들어있는 동일한 글자의 합계가 1이상이면 노란색 타일을 반환한다.") + @Test + void test02() { + Long count = 1L; + Letter answerLetter = new Letter('I'); + Letter letter = new Letter('E'); + + assertThat(Tile.getTile(count, answerLetter, letter)).isEqualTo(Tile.YELLOW); + } + + @DisplayName("워드에 들어있는 동일한 글자의 합계가 0 또는 빈 값이면 회색 타일을 반환한다.") + @Test + void test03() { + Long count = 0L; + Letter answerLetter = new Letter('I'); + Letter letter = new Letter('I'); + + assertThat(Tile.getTile(count, answerLetter, letter)).isEqualTo(Tile.GRAY); + } +} diff --git a/src/test/java/domain/WordTest.java b/src/test/java/domain/WordTest.java new file mode 100644 index 00000000..2e8d9ded --- /dev/null +++ b/src/test/java/domain/WordTest.java @@ -0,0 +1,45 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WordTest { + + @DisplayName("Word 를 생성한다.") + @Test + void test01() { + Word word = new Word("SLiPP"); + + assertThat(word.getWord()).isEqualTo(getLetters("SLiPP")); + } + + @DisplayName("Word 가 5글자가 아니면 에러를 발생한다.") + @ParameterizedTest + @ValueSource(strings = { "real", "winter" }) + void test02(String word) { + assertThatThrownBy(() -> new Word(word)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("Word 가 알파벳이 아니면 에러를 발생한다.") + @ParameterizedTest + @ValueSource(strings = { "12345", "!@#$%", "1a2bc", "s!l@i", "1!a@3" }) + void test03(String word) { + assertThatThrownBy(() -> new Word(word)) + .isInstanceOf(IllegalArgumentException.class); + } + + private List getLetters(String word) { + return word.chars().mapToObj(c -> (char) c) + .map(Letter::new) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/domain/WordsTest.java b/src/test/java/domain/WordsTest.java new file mode 100644 index 00000000..ee08eef3 --- /dev/null +++ b/src/test/java/domain/WordsTest.java @@ -0,0 +1,90 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class WordsTest { + + @DisplayName("Word 를 생성한다.") + @Test + void test01() { + Words words = new Words(List.of("MySQL", "SLiPP")); + + assertThat(words.getWords()).containsExactly(new Word("MySQL"), new Word("SLiPP")); + } + + @DisplayName("주어진 Words 가 비어있으면 에러가 발생한다.") + @Test + void test02() { + assertThatThrownBy(() -> new Words(Collections.emptyList())).isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("주어진 Words 에 Word 가 없다면 에러가 발생한다.") + @ParameterizedTest + @MethodSource("generateData") + void test03(List words) { + assertThatThrownBy(() -> new Words(words)).isInstanceOf(IllegalArgumentException.class); + } + + static Stream generateData() { + return Stream.of( + Arguments.of(List.of("MySQL", "cake", "12345")), + Arguments.of(List.of("sister", "cake")), + Arguments.of(List.of("12345", "a1234"))); + } + + @DisplayName("words 정답을 가져온다.") + @ParameterizedTest + @MethodSource("generateAnswerData") + void test04(LocalDate now, String result) { + Words words = new Words(List.of("MySQL", "SLiPP", "Words")); + + Answer answer = words.getAnswer(now); + + assertThat(answer).isEqualTo(getAnswer(result)); + } + + @DisplayName("Word 를 가져온다.") + @Test + void test05() { + Words words = new Words(List.of("MySQL", "SLiPP", "Words")); + + Word word = words.getWord("MySQL"); + + assertThat(word).isEqualTo(new Word("MySQL")); + } + + @DisplayName("Word 가 존재하지 않는다면 에러가 발생한다.") + @Test + void test06() { + Words words = new Words(List.of("MySQL", "SLiPP", "Words")); + + assertThatThrownBy(() -> words.getWord("build")) + .isInstanceOf(IllegalArgumentException.class); + } + + static Stream generateAnswerData() { + return Stream.of( + Arguments.of(Words.DEFAULT_DATE.plusDays(0), "MySQL"), + Arguments.of(Words.DEFAULT_DATE.plusDays(1), "SLiPP"), + Arguments.of(Words.DEFAULT_DATE.plusDays(2), "Words"), + Arguments.of(Words.DEFAULT_DATE.plusDays(3), "MySQL"), + Arguments.of(Words.DEFAULT_DATE.plusDays(4), "SLiPP") + ); + } + + private static Answer getAnswer(String result) { + return new Answer(new Word(result)); + } +} \ No newline at end of file diff --git a/src/test/java/support/WordsGeneratorTest.java b/src/test/java/support/WordsGeneratorTest.java new file mode 100644 index 00000000..3f8b9e1f --- /dev/null +++ b/src/test/java/support/WordsGeneratorTest.java @@ -0,0 +1,19 @@ +package support; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class WordsGeneratorTest { + + @DisplayName("파일에서 Words 를 읽어온다.") + @Test + void test01() { + List words = WordsGenerator.read("src/test/resources/words.txt"); + + assertThat(words.size()).isEqualTo(5); + } +} \ 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..3195f258 --- /dev/null +++ b/src/test/resources/words.txt @@ -0,0 +1,5 @@ +cigar +rebut +sissy +humph +awake