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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
28 changes: 28 additions & 0 deletions IMPLEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
1. 6x5 격자를 통해서 5글자 단어를 6번 만에 추측한다.
- WordleGame 은 5글자 단어를 입력받을 수 있다.
- 5글자가 아니면 재입력 한다.
- words.txt 에 존재하는 단어가 아니면 재입력 한다.
- WordleGame 입력의 성공 여부를 반환한다.
- WordleGame 은 6번 실패하면 게임을 종료한다.

2. 플레이어가 답안을 제출하면 프로그램이 정답과 제출된 단어의 각 알파벳 종류와 위치를 비교해 판별한다.
- Word 는 5개의 Window 로 이루어져 있다.
- Word 는 다른 Word 와 비교할 수 있다.
- Window 는 Alphabet + Position 으로 이루어져 있다.
- Window 는 다른 Window 와 비교할 수 있다.
- Alphabet 은 입력한 영어 문자를 나타낸다.
- Alphabet 은 다른 Alphabet 과 비교할 수 있다.
- Position 은 해당 문자의 위치를 나타낸다.
- Position 은 다른 Position 과 비교할 수 있다.
Comment on lines +8 to +16

Choose a reason for hiding this comment

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

두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다. 라는 요구사항이 구현이 안된것 같습니다!


3. 판별 결과는 흰색의 타일이 세 가지 색(초록색/노란색/회색) 중 하나로 바뀌면서 표현된다.
- 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색
- Window 는 맞는 글자, 위치가 다름, 없음 을 나타내는 Match.Perfect .Wrong .Miss 를 반환한다.
- 외부 표현 계층에서 Match 값을 가지고 색을 표현한다.
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.

4. 정답과 답안은 `words.txt`에 존재하는 단어여야 한다.
- 정답과 답안의 원천을 유동적으로 바꿀 수 있도록 한다.

5. 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.
- 게임을 시작할 때 현재 날짜를 보고 새로운 단어가 단어가 된다.
51 changes: 51 additions & 0 deletions src/main/kotlin/edu/nextstep/wordle/application/wordle/Word.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package edu.nextstep.wordle.application.wordle

import edu.nextstep.wordle.application.wordle.window.Alphabet
import edu.nextstep.wordle.application.wordle.window.AlphabetFactory
import edu.nextstep.wordle.application.wordle.window.Match
import edu.nextstep.wordle.application.wordle.window.Window
import edu.nextstep.wordle.application.wordle.window.WindowResult

data class Word(
val windows: Set<Window>,

Choose a reason for hiding this comment

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

혹시 Set을 쓰신 이유가 있을까요?
Set을 사용하기 때문에 외부에서 사용할 때 sortBy 가 나타나고, Windowposition 과 같은 변수가 생긴것 같습니다.

Word 만 보면 windows를 정렬해서 써야한다는 주석이 없으면 다른 개발자가 보았을때 실수 할 수 있어보입니다!
Word만 보면 자연스레 windows가 순서대로 나와야 좋을것 같습니다. List를 사용하면 위와 같은 문제는 없어질것 같습니다!

) {
init {
if (this.windows.size != WORD_SIZE) {

Choose a reason for hiding this comment

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

Exception을 직접 만들어서 처리하면 좋을것 같습니다.
테스트코드에서 보면 메세지를 가지고 의도한 위치에서 Exception이 터진것을 확인하고 있는데, 이렇게 되면 메세지가 바뀔경우 테스트도 깨지게 됩니다.

throw IllegalArgumentException("${this.windows.size}: 단어의 사이즈는 ${WORD_SIZE}여야 합니다.")
}
}

fun match(input: Word): List<WindowResult> {
var results = listOf<WindowResult>()

for (inputWindow in input.windows) {
val windowResult = getWindowResult(inputWindow)
results = results + windowResult
}

return results
}
Comment on lines +18 to +27

Choose a reason for hiding this comment

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

Suggested change
fun match(input: Word): List<WindowResult> {
var results = listOf<WindowResult>()
for (inputWindow in input.windows) {
val windowResult = getWindowResult(inputWindow)
results = results + windowResult
}
return results
}
fun match(input: Word): List<WindowResult> {
return input.windows.map(::getWindowResult)
}

해당 코드로도 충분해보입니다!

Choose a reason for hiding this comment

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

아래의 조건이 빠진것 같습니다.
두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.

예시는 다음과 같습니다.

답: genre
입력 값: error

현재 해당 로직이 구현이 안되어서
🟨🟨🟨⬜🟨
이렇게 표출이 됩니다. 하지만 답에서 r은 1개이므로,
🟨🟨⬜⬜⬜
다음과 같이 표출이 되어야합니다.

우선순위는 PERFECT -> WRONG -> MISS 순으로 처리가 되어야합니다.

Copy link
Author

Choose a reason for hiding this comment

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

아 제가 요구사항을 제대로 파악하지 못하고 제가 하던 게임에서의 룰을 그대로 사용했군요


private fun getWindowResult(inputWindow: Window): WindowResult {
var windowResult = WindowResult(position = inputWindow.position, match = Match.MISS)

for (targetWindow in this.windows) {
val match = targetWindow.match(inputWindow)
windowResult = windowResult.update(match)
}

return windowResult
}

companion object {
private const val WORD_SIZE: Int = 5

fun create(input: String): Word {
val alphabetFactory = AlphabetFactory.instance
val windows = input.mapIndexed { index, alphabet ->
Window(alphabet = alphabetFactory.findBy(alphabet.toString()), position = index)
}.toSet()
return Word(windows)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package edu.nextstep.wordle.application.wordle

import edu.nextstep.wordle.application.wordle.window.Match
import edu.nextstep.wordle.application.wordle.window.WindowResult

data class WordResult(
val round: Int,
val windowResults: List<WindowResult>,
) {
fun isSuccess(): Boolean {
return this.windowResults.map { it.match }
.all { it == Match.PERFECT }
}
Comment on lines +10 to +13

Choose a reason for hiding this comment

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

Suggested change
fun isSuccess(): Boolean {
return this.windowResults.map { it.match }
.all { it == Match.PERFECT }
}
fun isSuccess(): Boolean {
return this.windowResults.all { it.match == Match.PERFECT }
}

해당 코드는 간단하니까 하나로 합쳐도 충분하고, 가독성도 좋아보입니다.

Copy link
Author

Choose a reason for hiding this comment

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

네 이게 더 좋을것 같네요!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.nextstep.wordle.application.wordle

data class WordResults(
val wordResult: List<WordResult>,
) {
fun isSuccess(): Boolean {
if (wordResult.isEmpty()) {
return false
}
return requireNotNull(wordResult.maxByOrNull { it.round })
.isSuccess()
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/edu/nextstep/wordle/application/wordle/Wordle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package edu.nextstep.wordle.application.wordle

import edu.nextstep.wordle.application.wordle.dictionary.WordFinder

data class Wordle(
val target: Word,
val wordResult: List<WordResult> = emptyList(),

Choose a reason for hiding this comment

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

이 부분을 WordResults를 가지도록 하면 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

지금과 같은 경우에는 한가지 경우밖에 없지만 저는 일급컬렉션을 멤버변수로 들고있는것 보다 사용되는 위치에 따라 해당 상황에 맞는 일급컬렉션을 그때 랩핑해서 사용하는게 좋다고 생각하고있습니다.

val wordFinder: WordFinder,
) {
val round = wordResult.size + 1

fun input(word: Word): WordleAnswer {
if (wordFinder.notContain(word)) {
return WordleAnswer.Retry(this, "사전에 없는 단어(${word.rawWord()})입니다.")

Choose a reason for hiding this comment

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

UI와 소통하기 위해 Retry가 있는것 같습니다! Exception 으로 처리하는게 더 깔끔할것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

이 부분은 이펙티브 코틀린을 읽고 한번 시도해본 부분인데요!
UI와 소통하기 위해 retry를 넣었다기보다는
아이템 7에서 예외를 정보를 전달하는 방법으로 사용하는것이 cost가 크다고 하여 sealed 클래스로 처리했습니다.

}
Comment on lines +13 to +15

Choose a reason for hiding this comment

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

Exception을 터트리는건 어떨까요?


val result = this.target.match(word)
val wordResult = this.wordResult + WordResult(round = this.round, windowResults = result)

return WordleAnswer.Right(this.copy(wordResult = wordResult))
}

private fun WordFinder.notContain(word: Word): Boolean = !this.contain(word)

private fun Word.rawWord(): String {
return windows.sortedBy { it.position }.joinToString(separator = "") { it.alphabet.alphabet }
}
Comment on lines +25 to +27

Choose a reason for hiding this comment

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

Retry 객체를 생성하기 위한 메세지를 만드는것으로 보입니다.
메세지 만들기 위해 Word.windows가 public으로 풀린것이 조금 아쉽습니다! 다른 방법은 없을까요?


fun isSuccess(): Boolean {
return WordResults(this.wordResult)
.isSuccess()
}

fun isEnd(): Boolean {
return round > MAX_ROUND || isSuccess()
}

companion object {
private const val MAX_ROUND: Int = 6
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package edu.nextstep.wordle.application.wordle

sealed class WordleAnswer(
val wordle: Wordle,
) {
class Right(wordle: Wordle) : WordleAnswer(wordle)
class Retry(wordle: Wordle, val message: String) : WordleAnswer(wordle)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package edu.nextstep.wordle.application.wordle.dictionary

import edu.nextstep.wordle.application.wordle.Word

class MemoryWordFinder(
val words: List<Word>,
) : WordFinder {
override fun contain(input: Word): Boolean {
return words.contains(input)

Choose a reason for hiding this comment

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

List에서 해당 Word가 있는지 찾는것은 많은 비용이 들것 같은데, List보단 Set으로 검색하는건 어떨까요

Copy link
Author

Choose a reason for hiding this comment

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

성능에 대한 부분을 간과했네요! 감사합니다~

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.nextstep.wordle.application.wordle.dictionary

import edu.nextstep.wordle.application.wordle.Word
import java.time.LocalDate
import java.time.format.DateTimeFormatter

/**
* 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.
*/
class MemoryWordProvider(val words: List<Word>) : WordProvider {
override fun provide(date: LocalDate): Word {
val index = (date.format(DateTimeFormatter.ofPattern("yyyyMMdd")).toInt() - 20210619) % words.size

Choose a reason for hiding this comment

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

공식이 조금 이해한 바와는 다른것 같습니다! 오늘 날짜 기준으로 20220417-20210619 가 아닌, 2021년6월19일 부터의 날짜를 계산하는게 맞지 않을까 싶습니다. (302일 정도네요.)

Copy link
Author

Choose a reason for hiding this comment

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

아 그렇군요 이것도 제가 잘못 이해한것 같네요 ㅎㅎ

return words[index]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.nextstep.wordle.application.wordle.dictionary

import edu.nextstep.wordle.application.wordle.Word

fun interface WordFinder {
fun contain(input: Word): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package edu.nextstep.wordle.application.wordle.dictionary

import edu.nextstep.wordle.application.wordle.Word
import java.time.LocalDate

interface WordProvider {

Choose a reason for hiding this comment

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

혹시 WordFinderfun interface로 구현하고, 여기는 그렇지 않은 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

WordProvider를 굳이 fun interface로 구현해둘 필요성을 못느꼈습니다.

fun provide(date: LocalDate = LocalDate.now()): Word
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package edu.nextstep.wordle.application.wordle.window

class Alphabet(
value: String,
) {
val alphabet: String = value.lowercase()
Comment on lines +3 to +6

Choose a reason for hiding this comment

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

data classvalue class로도 가능해 보이는데, lowercase() 때문에 하지 못한것으로 보입니다.
그래서 equals, hashcode를 직접 구현해주신것 같구요!

아래와 같은 코드는 어떨까요?

1안

Suggested change
class Alphabet(
value: String,
) {
val alphabet: String = value.lowercase()
data class Alphabet private constructor(
val alphabet: String
) {
constructor(alphabet: Char): this(alphabet.lowercase())

2안

Suggested change
class Alphabet(
value: String,
) {
val alphabet: String = value.lowercase()
data class Alphabet(
private var _alphabet: String
) {
val alphabet: String
get() = _alphabet
init {
_alphabet = _alphabet.lowercase()
}


init {
if (!alphabets.contains(alphabet)) {
throw IllegalArgumentException("$alphabet 알파벳 입력만 허용합니다.")
}
Comment on lines +9 to +11

Choose a reason for hiding this comment

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

해당 부분도 Exception을 직접 만들어줘도 좋을거같아요.

Copy link
Author

Choose a reason for hiding this comment

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

네 도메인내의 특정 비지니스에 속하니까 커스텀 익셉션을 만들어주는게 좋을것 같네요

}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Alphabet

if (alphabet != other.alphabet) return false

return true
}

override fun hashCode(): Int {
return alphabet.hashCode()
}

companion object {
private val alphabets = ('A'..'Z').map { it.lowercase() }.toSet()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package edu.nextstep.wordle.application.wordle.window

class AlphabetFactory(
private val set: Set<Alphabet>,
) {
fun findBy(alphabet: String): Alphabet {
val target = Alphabet(alphabet)
return set.first { it == target }
Comment on lines +7 to +8

Choose a reason for hiding this comment

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

Set을 사용하고 있지만, first를 사용하여 Iterable에 구현된 반복을 하고 있네요! Map<String, Alphabet>은 어떨까요?

}

companion object {
val instance = create()

private fun create(): AlphabetFactory {
val alphabets: Set<Alphabet> = ('A'..'Z').map { Alphabet(it.lowercase()) }
.toSet()

return AlphabetFactory(alphabets)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.nextstep.wordle.application.wordle.window

enum class Match {
PERFECT,
WRONG,
MISS,
;

fun updatable(other: Match): Boolean {
return when (this) {
PERFECT -> false
WRONG -> other == PERFECT
MISS -> other != MISS
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package edu.nextstep.wordle.application.wordle.window

data class Window(
val alphabet: Alphabet,
val position: Int,
) {
fun match(other: Window): Match {
val alphabetMatch = this.alphabet == other.alphabet
val positionMatch = this.position == other.position

return if (alphabetMatch && positionMatch) {
Match.PERFECT
} else if (alphabetMatch) {
Match.WRONG
} else {
Match.MISS
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package edu.nextstep.wordle.application.wordle.window

data class WindowResult(
val position: Int,
val match: Match,
) {
fun update(match: Match): WindowResult {
val updateMatch = if (this.match.updatable(match)) {
match
} else {
this.match
}

return WindowResult(this.position, updateMatch)
}
}
31 changes: 31 additions & 0 deletions src/main/kotlin/edu/nextstep/wordle/config/WordleConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package edu.nextstep.wordle.config

import edu.nextstep.wordle.application.wordle.Word
import edu.nextstep.wordle.application.wordle.dictionary.MemoryWordFinder
import edu.nextstep.wordle.application.wordle.dictionary.MemoryWordProvider
import edu.nextstep.wordle.application.wordle.dictionary.WordFinder
import edu.nextstep.wordle.application.wordle.dictionary.WordProvider
import java.nio.file.Files
import java.nio.file.Paths
import kotlin.streams.toList

class WordleConfig {

fun memoryWordFinder(): WordFinder = MemoryWordFinder(words)

fun memoryWordProvider(): WordProvider = MemoryWordProvider(words)

fun wordleGame(): WordleGame = WordleGame(memoryWordProvider(), memoryWordFinder())

companion object {
private const val BASE_DIRECTORY = ""
private const val CLASS_PATH = "src/main/resources"
private const val DICTIONARY_FILE_NAME = "words.txt"
private val words: List<Word> =
Files.newInputStream(Paths.get(BASE_DIRECTORY, CLASS_PATH, DICTIONARY_FILE_NAME))
.bufferedReader()
.lines()
.map { Word.create(it) }
.toList()
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/edu/nextstep/wordle/config/WordleGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package edu.nextstep.wordle.config

import edu.nextstep.wordle.application.wordle.Word
import edu.nextstep.wordle.application.wordle.WordResults
import edu.nextstep.wordle.application.wordle.Wordle
import edu.nextstep.wordle.application.wordle.WordleAnswer
import edu.nextstep.wordle.application.wordle.dictionary.WordFinder
import edu.nextstep.wordle.application.wordle.dictionary.WordProvider
import edu.nextstep.wordle.presentation.inputWord
import edu.nextstep.wordle.presentation.showAnswer
import edu.nextstep.wordle.presentation.showTiles
import edu.nextstep.wordle.presentation.start

class WordleGame(
private val wordProvider: WordProvider,
private val wordFinder: WordFinder,
) {
fun run() {
val targetWord = wordProvider.provide()
var wordle = Wordle(target = targetWord, wordFinder = wordFinder)
start()

Choose a reason for hiding this comment

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

start()수를 보면 Game.start()같은 느낌이 있습니다.
showStartMessage() 같은 함수명을 주어서 게임의 시작과는 관련이 없어보이도록 하는게 좋을것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

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

에고 뷰단 짤때 거의다 끝나는 마음에 성급하게 네이밍을 했네요


while (wordle.untilEnd()) {
val input: Word = inputWord()
val answer: WordleAnswer = wordle.input(input)
wordle = doAnswer(answer)
showTiles(wordle.wordResult)
}

showAnswer(WordResults(wordle.wordResult))
}

private fun Wordle.untilEnd() = !this.isEnd()

private fun doAnswer(answer: WordleAnswer): Wordle {
when (answer) {
is WordleAnswer.Retry -> println(answer.message)
is WordleAnswer.Right -> Unit
}
return answer.wordle
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/edu/nextstep/wordle/main/WordleApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.nextstep.wordle.main

import edu.nextstep.wordle.config.WordleConfig

fun main() {
WordleConfig().wordleGame().run()
}
Loading