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] 돌쓰팀(보라돌) 미션 제출합니다. #21

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e715878
docs(README.md): 진행 요구 사항 작성
boradol Jun 16, 2024
50d1cf4
docs(README.md): 프로그래밍 요구 사항 작성
boradol Jun 16, 2024
59f7a4e
docs(README.md): 프로그래밍 요구사항 위치 변경
boradol Jun 16, 2024
8013a33
docs(README.md): 기능 요구 사항에 용어 정리와 기능 목록 항목 추가
boradol Jun 16, 2024
c5b1d44
docs(README.md): 용어정리
boradol Jun 16, 2024
54fe090
docs(README.md): 기능목록
boradol Jun 16, 2024
770346b
chore(.gitkeep): 불필요한 gitkeep 파일 제거
boradol Jun 17, 2024
5c26467
build: 코틀린 버전 1.9.0 -> 1.9.23
boradol Jun 17, 2024
4cc4c99
feat(Word): 문자열을 입력받아 단어를 만들 수 있다.
boradol Jun 17, 2024
a931257
feat(Word): 단어는 사전에 있는 단어이어야 한다.
boradol Jun 17, 2024
caeeff9
feat(ExceptionCode): Wordle의 에러 메시지를 관리한다.
boradol Jun 17, 2024
1cb3148
feat(Letter): 유효한 글자를 생성할 수 있다.
boradol Jun 17, 2024
2a3c133
feat(Letter): 일치 표시 기호가 들어간 글자로 변경할 수 있다.
boradol Jun 17, 2024
3e75764
feat(Word): 단어는 여러 개의 글자로 이루어진다.
boradol Jun 17, 2024
f328f65
refactor(DictionaryFileLoader): ClassLoader 이용하여 words.txt 파일 읽기
boradol Jun 17, 2024
798afda
refactor(Dictionary): 사전에 포함된 문자열인지 확인할 수 있다.
boradol Jun 17, 2024
add6622
feat(Dictionary): 사전 단어 목록의 인덱스로 한 문자열을 받을 수 있다.
boradol Jun 17, 2024
7b2a40e
chore : 테스트 패키지 이동 -> domain -> wordle.domain
boradol Jun 17, 2024
44680bd
feat(TodayWord): 오늘의 단어를 생성한다.
boradol Jun 17, 2024
24412d9
feat(AnswerWord): 답안 단어를 생성한다
boradol Jun 17, 2024
86967c2
feat(InputView, OutputView): 초기 입출력을 콘솔에 나타낸다.
boradol Jun 17, 2024
075025b
feat(Application, WordleGameController): 프로그램 실행 시작 구현
boradol Jun 17, 2024
64e58c2
feat(WordleGame): WordleGame 반복 수행 로직 초기 구현
boradol Jun 17, 2024
a64c965
feat(WordleGameLogic): 게임 로직을 통해 여러가지 상황들을 비교할 수 있다.
boradol Jun 17, 2024
8838156
feat(WordResult): 단어 결과는 글자 일치 여부 목록을 가지며, 단어 결과를 반환한다.
boradol Jun 17, 2024
6293b08
feat(WordComparator): 단어 비교기를 구현한다.
boradol Jun 17, 2024
bda9855
feat(TryCount): 워들 게임에서 시도 가능 횟수를 관리한다.
boradol Jun 17, 2024
9299eca
feat(WordResult): 단어 결과는 단어 길이와 일치해야 합니다.
boradol Jun 17, 2024
9a81038
feat(WordResults): 단어 결과 목록은 단어 결과들과 시도횟수를 관리한다.
boradol Jun 17, 2024
30850a7
feat(OutputView): 단어 출력
boradol Jun 17, 2024
51480af
feat(WordleGame): WordleGame을 할 수 있다.
boradol Jun 17, 2024
13225f6
refactor : PR 올리기 전 코드 수정
boradol Jun 17, 2024
b2c51db
docs(README.md): 프로그래밍 요구사항 체크하기
boradol Jun 17, 2024
4a4381f
refactor(Letter): import 수정
boradol Jun 18, 2024
8f462f7
refactor(WordleGame): 불필요한 파라미터 지우기
boradol Jun 18, 2024
e879694
refactor(WordComparator): 표시하는 문자와 답안문자를 구분하는 이름 짓기, 함수 이름 수정
boradol Jun 18, 2024
8a0dcb9
test(WordResults): 게임 계속여부와 성공여부에 따른 테스트코드 추가
boradol Jun 18, 2024
c15c888
refactor(OutputView): 하드코딩된 부분 변수로 추출 및 함수명 등 리팩토링
boradol Jun 18, 2024
2aba793
refactor(WordleGameTest): 인덴트와 변수 리팩토링
boradol Jun 18, 2024
1c428d7
refactor(Letter): private 함수 삭제
boradol Jun 26, 2024
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
251 changes: 213 additions & 38 deletions README.md

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "1.9.0"
kotlin("jvm") version "1.9.23"
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}

Expand All @@ -14,9 +14,12 @@ repositories {
mavenCentral()
}

val junitJupiterVersion = "5.10.2"
val assertJVersion = "3.25.3"

dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.10.2")
testImplementation("org.assertj", "assertj-core", "3.25.3")
testImplementation("org.junit.jupiter", "junit-jupiter", junitJupiterVersion)
testImplementation("org.assertj", "assertj-core", assertJVersion)
}

tasks {
Expand Down
Empty file removed src/main/kotlin/.gitkeep
Empty file.
7 changes: 7 additions & 0 deletions src/main/kotlin/wordle/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wordle

import wordle.controller.WordleGameController

fun main() {
WordleGameController().run()
}
40 changes: 40 additions & 0 deletions src/main/kotlin/wordle/application/WordleGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package wordle.application

import wordle.domain.TodayWord
import wordle.domain.Word
import wordle.domain.WordResults
import wordle.domain.WordleGameLogic
import wordle.view.inputAnswerWord
import wordle.view.printFail
import wordle.view.printRetry
import wordle.view.printSuccess
import wordle.view.printWordResults
import java.time.LocalDate

class WordleGame(gameStartDate: LocalDate) {
private val todayWord = TodayWord(gameStartDate)
private val wordleGameLogic = WordleGameLogic(todayWord)
private val results = WordResults()

fun play() {
while (results.isContinuousGame()) {
try {
val answerWord = Word(inputAnswerWord())
val result = wordleGameLogic.compare(answerWord)
results.addResults(result)
printWordResults(results)
} catch (e: IllegalStateException) {
printRetry(e.message)
}
}
printGameResult()
}

private fun printGameResult() {
if (results.isSuccessfulGame()) {
printSuccess(results.attemptCount)
return
}
printFail(todayWord)
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/wordle/controller/WordleGameController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package wordle.controller

import wordle.application.WordleGame
import wordle.view.printStartingGameMessage
import java.time.LocalDate

class WordleGameController {
private val gameStartDate: LocalDate = LocalDate.now()
private val wordleGame: WordleGame = WordleGame(gameStartDate)

fun run() {
printStartingGameMessage()
wordleGame.play()
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/wordle/domain/AnswerWord.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wordle.domain

typealias AnswerWord = Word

fun AnswerWord(answerWord: String): AnswerWord {
return Word(answerWord)
}
8 changes: 8 additions & 0 deletions src/main/kotlin/wordle/domain/Dictionary.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wordle.domain

import wordle.infra.contains
import wordle.infra.dictionaryWord

fun isDictionaryWord(word: String): Boolean = contains(word)

fun dictionaryElementAt(index: Int): String = dictionaryWord(index)
23 changes: 23 additions & 0 deletions src/main/kotlin/wordle/domain/Letter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package wordle.domain

import wordle.exception.WordleExceptionCode.LETTER_INVALID_CHARACTER_TYPE

data class Letter(private val value: Char) {
init {
check(isAlphabet() || isMatchMarker()) { LETTER_INVALID_CHARACTER_TYPE.message }
}

fun changeMatchMarker(): Letter = MATCH_MARKER_LETTER

fun value(): String = value.toString()

private fun isAlphabet(): Boolean = value in ALPHABET

private fun isMatchMarker(): Boolean = value == MATCH_MARKER

companion object {
private const val MATCH_MARKER = '#'
private val ALPHABET = ('a'..'z').toSet()
private val MATCH_MARKER_LETTER = Letter(MATCH_MARKER)
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/wordle/domain/LetterMatch.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package wordle.domain

enum class LetterMatch {
CORRECT,
PRESENT,
ABSENT,
}
19 changes: 19 additions & 0 deletions src/main/kotlin/wordle/domain/TodayWord.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wordle.domain

import wordle.infra.dictionaryWord
import java.time.LocalDate
import java.time.temporal.ChronoUnit

typealias TodayWord = Word

private val CRITERION_DATE: LocalDate = LocalDate.of(2021, 6, 19)

fun TodayWord(today: LocalDate): TodayWord {
return Word(extractDictionaryWord(today))
}

private fun extractDictionaryWord(date: LocalDate): String {
val calculatedIndex = ChronoUnit.DAYS.between(CRITERION_DATE, date).toInt()

return dictionaryWord(calculatedIndex)
}
16 changes: 16 additions & 0 deletions src/main/kotlin/wordle/domain/TryCount.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package wordle.domain

import wordle.exception.WordleExceptionCode.TRY_COUNT_HAS_NOT_REMAINDER

data class TryCount(private var count: Int = MAX_TRY_COUNT) {
val attempts get() = MAX_TRY_COUNT - count

fun isRemainder(): Boolean = count in 1..MAX_TRY_COUNT

fun minus() {
check(isRemainder()) { TRY_COUNT_HAS_NOT_REMAINDER.message }
this.count -= 1
}
}

const val MAX_TRY_COUNT = 6
21 changes: 21 additions & 0 deletions src/main/kotlin/wordle/domain/Word.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package wordle.domain

import wordle.exception.WordleExceptionCode.WORD_INVALID_LENGTH
import wordle.exception.WordleExceptionCode.WORD_IS_NOT_IN_DICTIONARY
import wordle.exception.WordleExceptionCode.WORD_NOT_ALLOW_SPACE

data class Word(private val word: List<Letter>) : List<Letter> by word {
fun letters(): String = word.joinToString("", transform = Letter::value)
}

const val WORD_LENGTH = 5

fun Word(word: String): Word {
check(word.isNotBlank()) { WORD_NOT_ALLOW_SPACE.message }

Choose a reason for hiding this comment

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

여태 require만 알고있었는데 덕분에 새로운거 알게되었습니다! 감사합니다!

Copy link
Author

Choose a reason for hiding this comment

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

네네 다시 시도시에 IllegalStateException이 더 알맞아서 check를 사용하게 되었어용!

check(isValidLength(word)) { WORD_INVALID_LENGTH.message }
check(isDictionaryWord(word)) { WORD_IS_NOT_IN_DICTIONARY.message }

return Word(word.toCharArray().map { Letter(it) })
}

private fun isValidLength(word: String) = word.length == WORD_LENGTH
60 changes: 60 additions & 0 deletions src/main/kotlin/wordle/domain/WordComparator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package wordle.domain

class WordComparator(
private val markerLetters: MutableList<Letter>,
private val wordResult: WordResult = WordResult(),
) {
fun matchCorrect(answerWord: Word): WordComparator =
apply {
markerLetters.forEachIndexed { index, _ -> changeCorrectMatch(index, answerWord[index]) }
}

fun matchPresent(answerWord: Word): WordComparator =
apply {
markerLetters.forEachIndexed { index, _ -> changePresentMatch(index, answerWord[index]) }
}

fun result(): WordResult = wordResult

private fun changeCorrectMatch(
index: Int,
answerLetter: Letter,
) {
if (isCorrectLetter(index, answerLetter)) {
wordResult.changeMatchType(index, LetterMatch.CORRECT)
changeMarkerLetter(index)
}
}

private fun changePresentMatch(
index: Int,
answerLetter: Letter,
) {
if (isPresentLatter(index, answerLetter)) {
wordResult.changeMatchType(index, LetterMatch.PRESENT)
changeMarkerLetterIndexOf(answerLetter)
}
}

private fun isCorrectLetter(
index: Int,
answerLetter: Letter,
) = markerLetters[index] == answerLetter

private fun isPresentLatter(
index: Int,
answerLetter: Letter,
) = !(wordResult.isCorrectLetterMatch(index) || isCorrectLetter(index, answerLetter)) && (answerLetter in markerLetters)

Choose a reason for hiding this comment

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

 !(wordResult.isCorrectLetterMatch(index)

해당 부분에 () 가 필요한 이유를 알 수 있을까요?!

Copy link
Author

Choose a reason for hiding this comment

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

!( wordResult.isCorrectLetterMatch(index) || isCorrectLetter(index, answerLetter) )

묶어서 표현했습니다.
이부분이 CorrectLetter인지 아닌지 Validation이 필요한 로직이라서요.
그럼 이 부분을 묶은것은 어떻게 생각하세요?
이것도 따로 private 함수로 빼려다가 내부 함수의 뎁스가 너무 깊어질거 같아서 그냥 이렇게 두었습니다.


private fun changeMarkerLetter(index: Int) {
markerLetters[index] = markerLetters[index].changeMatchMarker()
}

private fun changeMarkerLetterIndexOf(letter: Letter) {
changeMarkerLetter(markerLetters.indexOf(letter))
}
}

fun WordComparator(todayWord: Word): WordComparator {
return WordComparator(todayWord.map(Letter::copy).toMutableList())
}
22 changes: 22 additions & 0 deletions src/main/kotlin/wordle/domain/WordResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package wordle.domain

import wordle.exception.WordleExceptionCode.WORD_RESULT_INVALID_LENGTH

data class WordResult(private val result: MutableList<LetterMatch> = MutableList(WORD_LENGTH) { LetterMatch.ABSENT }) {
init {
check(result.size == WORD_LENGTH) { WORD_RESULT_INVALID_LENGTH.message }
}

fun changeMatchType(
index: Int,
matchType: LetterMatch,
) {
result[index] = matchType
}

fun isCorrectLetterMatch(index: Int): Boolean = result[index] == LetterMatch.CORRECT

fun isSuccessfulWordResult() = result.all { matchType -> matchType == LetterMatch.CORRECT }

fun matches(): List<LetterMatch> = result.toList()
}
19 changes: 19 additions & 0 deletions src/main/kotlin/wordle/domain/WordResults.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wordle.domain

class WordResults(
private val results: MutableList<WordResult> = mutableListOf(),
private val tryCount: TryCount = TryCount(),
) {
val attemptCount: Int get() = tryCount.attempts

fun addResults(result: WordResult) {
tryCount.minus()
results.add(result)
}

fun isContinuousGame(): Boolean = !isSuccessfulGame() && tryCount.isRemainder()

fun isSuccessfulGame(): Boolean = (tryCount.attempts != 0) && results.last().isSuccessfulWordResult()

Choose a reason for hiding this comment

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

tryCount.attempts != 0

이 부분에서 ()가 필요한 이유가 따로 있으실까요??

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 wordResults(): List<List<LetterMatch>> = results.map { it.matches() }
}
11 changes: 11 additions & 0 deletions src/main/kotlin/wordle/domain/WordleGameLogic.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package wordle.domain

class WordleGameLogic(private val todayWord: Word) {
fun compare(answerWord: Word): WordResult =
todayWord.comparator()
.matchCorrect(answerWord)
.matchPresent(answerWord)
.result()

private fun Word.comparator(): WordComparator = WordComparator(this)
}
12 changes: 12 additions & 0 deletions src/main/kotlin/wordle/exception/WordleExceptionCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package wordle.exception

import wordle.domain.WORD_LENGTH

enum class WordleExceptionCode(val message: String) {
WORD_NOT_ALLOW_SPACE("단어는 공백만 입력할 수 없습니다."),
WORD_INVALID_LENGTH("단어의 길이는 ${WORD_LENGTH}자 입니다."),
WORD_IS_NOT_IN_DICTIONARY("Wordle Game에서 유효한 단어가 아닙니다."),
LETTER_INVALID_CHARACTER_TYPE("유효하지 않은 글자 형식입니다."),
TRY_COUNT_HAS_NOT_REMAINDER("시행 횟수는 0보다 작을 수 없습니다."),
WORD_RESULT_INVALID_LENGTH("단어 결과의 글자 일치 상태 목록들은 단어 길이인 ${WORD_LENGTH}와 일치해야 합니다."),
}
25 changes: 25 additions & 0 deletions src/main/kotlin/wordle/infra/DictionaryFileLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package wordle.infra

import java.nio.charset.StandardCharsets

private const val NEW_LINE = "\n"
private const val WORDS_FILE_PATH = "words.txt"
private val classLoader: ClassLoader = Thread.currentThread().contextClassLoader
private val dictionaryWords: List<String> by lazy { loadDictionaryWords() }
private val dictionaryWordSet: Set<String> by lazy { dictionaryWords.toSet() }
val dictionaryWordsSize = dictionaryWords.size

fun contains(word: String): Boolean = dictionaryWordSet.contains(word)
Comment on lines +8 to +12

Choose a reason for hiding this comment

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

dictionaryWords(List) -> dictionaryWordSet(Set) 으로 변환을 하여 dictionaryWordSet.contains(word) 단어 포함 여부를 나타내는거 같습니다!

dictionaryWords -> dictionaryWordSet으로 변환을 해서 얻을 수 있는 이점은 중복 제거라고 생각이 됩니다(만약 아니면 말씀해주세요!)

여기서 궁금한점은 중복 제거를 안하더라도 dictionaryWords를 이용하여 단어가 포함되어있는지 비교할 수 있을거 같은데 왜 따로
dictionaryWordSet을 만들어서 단어 포함여부를 비교하는지 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

dictionaryWordSet으로 변환의 이점은
시간복잡도 면에서도 이득을 볼 수 있어 이렇게 구현해보았습니다.🥲

해당dictionaryWords에서 해당 단어가 contains인지 판별할 때
List는 O(n) 복잡도, Set O(1) 복잡도로 활용할 수 있습니다.
그래서 성능상 contains가 자주 사용될 것 같았고, 조금더 성능을 높여주면 좋을 것 같아 변환 시켜주었습니다.

또한, 오늘의 단어를 할 때는 List의 index로 접근해야할 것 같아서 일단 이렇게 두가지를 사용해보았습니다.


fun dictionaryWord(index: Int): String {
check(dictionaryWords.isNotEmpty()) { NoSuchElementException("Dictionary File is Empty") }

return dictionaryWords[index % dictionaryWordsSize]
}

private fun loadDictionaryWords(): List<String> =
classLoader.getResourceAsStream(WORDS_FILE_PATH)
?.bufferedReader(StandardCharsets.UTF_8)
?.use { it.readText().split(NEW_LINE) }
?.filter { it.isNotBlank() }
?: throw IllegalArgumentException("Dictionary File not found: $WORDS_FILE_PATH")
6 changes: 6 additions & 0 deletions src/main/kotlin/wordle/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package wordle.view

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

import wordle.domain.LetterMatch
import wordle.domain.MAX_TRY_COUNT
import wordle.domain.Word
import wordle.domain.WordResults

private const val ENTER = "\n"
private const val SPACE = ""

fun printStartingGameMessage() {
println("🎮 WORDLE을 ${MAX_TRY_COUNT}번 만에 맞춰 보세요.$ENTER📌 시도의 결과는 타일의 색 변화로 나타납니다.🥳$ENTER")
}

fun printWordResults(results: WordResults) {
println(results.wordResults().joinToString(ENTER) { printTiles(it) } + ENTER)
}

fun printRetry(message: String?) {
println("🥲 다시 시도하세요! : ${message ?: SPACE}$ENTER")
}

fun printSuccess(attemptCount: Int) {
println("🎉 성공입니다. $attemptCount / $MAX_TRY_COUNT")
}

fun printFail(todayWord: Word) {
println("👻 실패하였습니다. 오늘의 단어는 [ ${todayWord.letters()} ] 입니다.")
}

private fun printTiles(it: List<LetterMatch>) = it.joinToString(SPACE) { Tile.of(it).color }
Loading