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

[Wordle] 차리(이찬주) 미션 제출합니다. #11

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
38e9dde
docs: 기능 요구사항 목록 작성
kth990303 May 10, 2022
80f767b
feat: 글자 하나를 나타내는 `Letter` 구현
kth990303 May 10, 2022
da55951
feat: 유효한 단어를 나타내는 `Word` 구현
kth990303 May 10, 2022
6377d1d
feat: `Word`에서 다른 단어와 비교하는 기능 구현
kth990303 May 11, 2022
ba0bba1
feat: 예측 단어와 정답 단어 비교 기능 구현
kth990303 May 11, 2022
17aca8c
refactor: 접근제어자 수정
kth990303 May 11, 2022
35e26f7
docs: 누락된 요구사항 체크
kth990303 May 11, 2022
80cdae7
feat: 정답 단어를 날짜에 따라 뽑는 기능 구현
kth990303 May 11, 2022
d284131
feat: 단어가 소문자로 이루어졌는지 검증하는 로직 추가 구현
kth990303 May 11, 2022
052464f
feat: 게임을 실행하는 애플리케이션 구현
kth990303 May 11, 2022
b80b2e3
refactor: EOF 컨벤션 변경
kth990303 May 12, 2022
ce0abfe
style: `ktlintCheck`에 맞게 컨벤션 변경
kth990303 May 12, 2022
d2b992b
style: 메소드 순서 변경
cjlee38 May 13, 2022
0d96a6d
test: DSL 테스트 추가
cjlee38 May 18, 2022
df8a5b0
fix: 워들 로직 오류 수정
cjlee38 May 19, 2022
80b7921
refactor: 검증 로직 확장함수로 변경
cjlee38 May 19, 2022
53f36be
feat: Game 객체 추가 및 게임진행 책임 위임
cjlee38 May 19, 2022
7feb21f
refactor: 접근제어자 수정
cjlee38 May 19, 2022
c77d604
style: reformat code
cjlee38 May 19, 2022
21c766d
refactor: 날짜를 생성자 주입을 통해 받을 수 있도록 변경
cjlee38 May 21, 2022
4aa60e3
fix: 인덱스 상관없이 일치한 단어를 소진하도록 변경
cjlee38 May 22, 2022
86c784f
test: 누락한 테스트 수정
cjlee38 May 22, 2022
04c9896
refactor: 도메인에서 view 값 제거
cjlee38 May 22, 2022
0a3af01
style: ktlint 적용
cjlee38 May 22, 2022
d5216d2
style: 한줄 메소드는 식으로 변경
cjlee38 May 22, 2022
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
21 changes: 21 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## 기능 요구사항
### 입출력
- [x] 게임 방법 안내를 출력한다.
- [x] `예측 단어`를 입력받는다.
- [x] `예측 단어`가 유효한 단어가 아니면 안내 문구를 출력한 후 재입력받는다.
- [x] 최대 6번 입력받을 수 있다.
- [x] `예측 결과`를 출력한다.
- [x] 맞는 글자는 초록색🟩, 위치만 틀리면 노란색🟨, 없으면 회색⬜으로 출력한다.
- [x] 게임을 종료한다.
- [x] 6번 이내에 `정답 단어`를 맞추지 못할 경우 정답 단어를 출력한 후에 종료한다.

### 도메인
- [x] `유효한 단어`를 불러온다.
- [x] `유효한 단어`란, `words.txt`에 존재하는 단어이다.
- [x] `정답 단어`를 불러온다.
- [x] 정답은 유효한 단어에서 뽑는다.
- [x] 정답은 매일 바뀌며 `((현재 날짜 - 2021년 6월 19일) % 배열의 크기)` 번째의 단어이다.
- [x] `예측 단어`와 `정답 단어`를 비교한다.
- [x] 맞는 글자가 있는지 먼저 비교한다.
- [x] 위치만 틀린 글자가 있는지 그 다음으로 비교한다.
- [x] 나머지는 모두 틀린 글자로 간주한다.
46 changes: 46 additions & 0 deletions src/main/kotlin/wordle/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package wordle

import wordle.domain.Answer
import wordle.domain.Color
import wordle.domain.Color.GREEN
import wordle.domain.Word
import wordle.domain.WordPicker
import wordle.view.InputView
import wordle.view.OutputView

fun main() {
val wordPicker = WordPicker()
val answer = Answer(wordPicker.pickTodayAnswer())
OutputView.printIntroduction()

if (playWordle(answer)) {
return
}
OutputView.printAnswer(answer)
}

private fun playWordle(answer: Answer): Boolean {
repeat(6) {

Choose a reason for hiding this comment

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

6은 워들 게임에서 의미있는 숫자이지 않을까요?🤔

Copy link
Author

Choose a reason for hiding this comment

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

Game 클래스를 만들어서 내부 Companion object에서 관리하도록 처리하였습니다~

val guessWord = guessAnswer()
val result = answer.compare(guessWord)
OutputView.printResult(result)
if (isAllGreen(result)) {
OutputView.printCount(it + 1)
return true
}
}
return false
}

fun guessAnswer(): Word {
return try {
Word(InputView.inputGuess())
} catch (e: IllegalArgumentException) {
OutputView.printError(e.message)
guessAnswer()
}
}

private fun isAllGreen(result: List<Color>): Boolean {
return result.all { it == GREEN }
}
39 changes: 39 additions & 0 deletions src/main/kotlin/wordle/domain/Answer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package wordle.domain

private const val RANGE_START = 0
private const val RANGE_END = 4

class Answer(val word: Word) {

fun compare(word: Word): List<Color> {
val exactIndices = compareExact(word)
val anyIndices = compareAny(word)
return merge(exactIndices, anyIndices)

Choose a reason for hiding this comment

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

단어를 비교하는 로직을 이해하기 쉽네요. 한 수 배우고 갑니다😲😲

}

private fun compareExact(word: Word): List<Int> {
return (RANGE_START..RANGE_END).filter { this.word.compareByIndex(word, it) }
}

private fun compareAny(word: Word): List<Int> {
return (RANGE_START..RANGE_END).filter { isAnyMatch(word, it) }
}

private fun merge(greenIndices: List<Int>, yellowIndices: List<Int>): List<Color> {
return (RANGE_START..RANGE_END).map { defineColor(it, greenIndices, yellowIndices) }
}

private fun defineColor(index: Int, greenIndices: List<Int>, yellowIndices: List<Int>): Color {
if (greenIndices.contains(index)) {
return Color.GREEN
}
if (yellowIndices.contains(index)) {
return Color.YELLOW
}
return Color.GRAY
}

private fun isAnyMatch(word: Word, outerIndex: Int): Boolean {
return (RANGE_START..RANGE_END).any { this.word.compareByIndex(word, it, outerIndex) }
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/wordle/domain/Color.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wordle.domain

enum class Color(val representation: String) {
GREEN("🟩"),
YELLOW("🟨"),
GRAY("⬜")

Choose a reason for hiding this comment

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

UI영역에서는 초록색, 노란색, 회색으로 결과를 보여주는 부분을 도메인 로직에서 의존하는거 같아요. 당장은 아니더라도 한번 고민해보시면 좋을거 같습니다😊

Copy link
Author

Choose a reason for hiding this comment

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

도메인쪽에서 뷰 로직에 영향을 받는게 좋아보이지는 않네요. view쪽에서 when절로 처리하였습니다~

}
34 changes: 34 additions & 0 deletions src/main/kotlin/wordle/domain/Word.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package wordle.domain

import java.io.File

private const val WORD_LENGTH = 5

data class Word(val word: String) {
init {
require(word.length == WORD_LENGTH) { "단어는 5글자여야 합니다." }
require(isLowerCase(word)) { "단어는 소문자로 이루어져야 합니다." }
require(contains(word)) { "유효하지 않은 단어입니다." }
}

private fun isLowerCase(value: String): Boolean {
return value.all { it.isLowerCase() }
}

fun compareByIndex(other: Word, myIndex: Int, otherIndex: Int = myIndex): Boolean {
return word[myIndex] == other.word[otherIndex]
}

companion object {
private val CACHE: List<String> = File("src/main/resources/words.txt")
.readLines()
Comment on lines +21 to +22

Choose a reason for hiding this comment

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

캐싱 사용💯💯


fun contains(word: String): Boolean {
return CACHE.contains(word)
}

fun findWordByDay(day: Int): Word {
return Word(CACHE[day % CACHE.size])
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/wordle/domain/WordPicker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package wordle.domain

import java.time.LocalDate
import java.time.Period

private const val YEAR = 2021
private const val MONTH = 6
private const val DAY_OF_MONTH = 19

class WordPicker(private val today: LocalDate = LocalDate.now()) {
fun pickTodayAnswer(): Word {
val fixed = LocalDate.of(YEAR, MONTH, DAY_OF_MONTH)
val day = Period.between(fixed, today).days
return Word.findWordByDay(day)
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/wordle/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wordle.view

object InputView {
fun inputGuess(): String {
println("정답을 입력해 주세요.")
return readln()
}
}
34 changes: 34 additions & 0 deletions src/main/kotlin/wordle/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package wordle.view

import wordle.domain.Answer
import wordle.domain.Color

object OutputView {
private val results: MutableList<List<Color>> = mutableListOf()

fun printIntroduction() {
println("WORDLE을 6번 만에 맞춰 보세요.")

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.

그렇네요. Game 클래스 내부의 companion object에서 상수로 관리하도록 처리하였습니다~

println("시도의 결과는 타일의 색 변화로 나타납니다.")
}

fun printResult(newResult: List<Color>) {
results.add(newResult)
for (result in results) {
result.forEach { print(it.representation) }
println()
}
println()
}

fun printAnswer(answer: Answer) {
println("아쉽습니다! 정답은 ${answer.word.word}입니다.")
}

fun printCount(tryCount: Int) {
println("$tryCount/6")

Choose a reason for hiding this comment

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

여기서도 6이 사용되고 있네요!

Copy link
Author

Choose a reason for hiding this comment

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

반영하였습니다~

}

fun printError(message: String?) {
println(message)
}
}
85 changes: 85 additions & 0 deletions src/test/kotlin/wordle/domain/AnswerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package wordle.domain

import io.kotest.matchers.collections.shouldContainAll
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class AnswerTest {

@Test
@DisplayName("완전히 일치하는 글자들과 틀린 글자가 존재하는 예측을 비교한다.")
fun compareWithGreenAndGray() {
val answer = Answer(Word("cigar"))

answer.compare(Word("clear")) shouldContainAll listOf(
Color.GREEN,
Color.GRAY,
Color.GRAY,
Color.GREEN,
Color.GREEN
)
}

@Test
@DisplayName("완전히 일치하는 글자들만 존재하는 예측을 비교한다. - 정답인 경우")
fun compareWithAllGreens() {
val answer = Answer(Word("cigar"))

answer.compare(Word("cigar")) shouldContainAll
listOf(Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN, Color.GREEN)
}

@Test
@DisplayName("모든 종류의 글자들이 존재하는 예측을 비교한다.")
fun compareWithAllColors() {
val answer = Answer(Word("spill"))

answer.compare(Word("hello")) shouldContainAll
listOf(Color.GRAY, Color.GRAY, Color.YELLOW, Color.GREEN, Color.GRAY)
}

@Test
@DisplayName("위치만 일치하는 글자와 틀린 글자들이 존재하는 예측을 비교한다.")
fun compareWithYellowAndGray1() {
val answer = Answer(Word("front"))

answer.compare(Word("totem")) shouldContainAll
listOf(Color.YELLOW, Color.YELLOW, Color.GRAY, Color.GRAY, Color.GRAY)
}

@Test
@DisplayName("위치만 일치하는 글자들과 틀린 글자들이 존재하는 예측을 비교한다.")
fun compareWithYellowAndGray2() {
val answer = Answer(Word("totem"))

answer.compare(Word("start")) shouldContainAll
listOf(Color.GRAY, Color.YELLOW, Color.GRAY, Color.GRAY, Color.YELLOW)
}

@Test
@DisplayName("전부 틀린 글자들이 존재하는 예측을 비교한다.")
fun compareWithAllGrays() {
val answer = Answer(Word("parry"))

answer.compare(Word("biome")) shouldContainAll
listOf(Color.GRAY, Color.GRAY, Color.GRAY, Color.GRAY, Color.GRAY)
}

@Test
@DisplayName("전부 위치만 일치하는 글자들이 존재하는 예측을 비교한다.")
fun compareWithAllYellows() {
val answer = Answer(Word("parse"))

answer.compare(Word("spear")) shouldContainAll
listOf(Color.YELLOW, Color.YELLOW, Color.YELLOW, Color.YELLOW, Color.YELLOW)
}

@Test
@DisplayName("test.")

Choose a reason for hiding this comment

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

DisplayName으로도 무엇을 테스트하고자 하는지 잘 모르겠어요🥲

Copy link
Author

Choose a reason for hiding this comment

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

이것저것 테스트해보다가 실수로 넣은것같네요.. 이 부분도 수정하였습니다~

fun test1() {
val answer = Answer(Word("witch"))

answer.compare(Word("timid")) shouldContainAll
listOf(Color.YELLOW, Color.GREEN, Color.GRAY, Color.GRAY, Color.GRAY)
}
}
19 changes: 19 additions & 0 deletions src/test/kotlin/wordle/domain/WordPickerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wordle.domain

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import java.time.LocalDate

class WordPickerTest {

@Test
@DisplayName("오늘의 단어를 가져온다.")
fun pickTodayAnswer() {
val wordPicker = WordPicker(LocalDate.of(2021, 6, 20))

val todayAnswer = wordPicker.pickTodayAnswer()

todayAnswer shouldBe Word("rebut")
}
}
52 changes: 52 additions & 0 deletions src/test/kotlin/wordle/domain/WordTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package wordle.domain

import io.kotest.assertions.throwables.shouldNotThrow
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class WordTest {

@Test
@DisplayName("단어는 5글자여야 한다.")
fun constructor() {
shouldThrow<IllegalArgumentException> { (Word("word")) }
}

@Test
@DisplayName("단어는 소문자로 이루어져야 한다.")
fun constructorWithLowercaseWord() {
shouldThrow<IllegalArgumentException> { Word("CIGAR") }
}

@Test
@DisplayName("유효한 단어여야 한다.")
fun constructorWithValidWord() {
shouldNotThrow<IllegalArgumentException> { Word("cigar") }
}

@Test
@DisplayName("유효하지 않은 단어일 경우 예외를 발생시킨다.")
fun constructorWithInvalidWord() {
shouldThrow<IllegalArgumentException> { Word("abcde") }
}

@Test
@DisplayName("다른 단어와 같은 인덱스로 비교한다")
fun compareBySameIndex() {
val wordA = Word("cigar")
val wordB = Word("clear")

wordA.compareByIndex(wordB, 0) shouldBe true
}

@Test
@DisplayName("다른 단어와 다른 인덱스로 비교한다")
fun compareByDifferentIndex() {
val wordA = Word("cigar")
val wordB = Word("clear")

wordA.compareByIndex(wordB, 0, 1) shouldBe false
}
}