-
Notifications
You must be signed in to change notification settings - Fork 35
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
base: main
Are you sure you want to change the base?
Changes from all commits
e715878
50d1cf4
59f7a4e
8013a33
c5b1d44
54fe090
770346b
5c26467
4cc4c99
a931257
caeeff9
1cb3148
2a3c133
3e75764
f328f65
798afda
add6622
7b2a40e
44680bd
24412d9
86967c2
075025b
64e58c2
a64c965
8838156
6293b08
bda9855
9299eca
9a81038
30850a7
51480af
13225f6
b2c51db
4a4381f
8f462f7
e879694
8a0dcb9
c15c888
2aba793
1c428d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package wordle | ||
|
||
import wordle.controller.WordleGameController | ||
|
||
fun main() { | ||
WordleGameController().run() | ||
} |
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) | ||
} | ||
} |
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() | ||
} | ||
} |
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) | ||
} |
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) |
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package wordle.domain | ||
|
||
enum class LetterMatch { | ||
CORRECT, | ||
PRESENT, | ||
ABSENT, | ||
} |
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) | ||
} |
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 |
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 } | ||
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 |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. !(wordResult.isCorrectLetterMatch(index) 해당 부분에 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
묶어서 표현했습니다. |
||
|
||
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()) | ||
} |
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() | ||
} |
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tryCount.attempts != 0 이 부분에서 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 있으나 없으나 상관은 없으나 |
||
|
||
fun wordResults(): List<List<LetterMatch>> = results.map { it.matches() } | ||
} |
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) | ||
} |
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}와 일치해야 합니다."), | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
여기서 궁금한점은 중복 제거를 안하더라도 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
해당 또한, 오늘의 단어를 할 때는 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") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package wordle.view | ||
|
||
fun inputAnswerWord(): String { | ||
println("🚀 정답을 입력하세요. : ") | ||
return readln() | ||
} |
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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여태 require만 알고있었는데 덕분에 새로운거 알게되었습니다! 감사합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네네 다시 시도시에
IllegalStateException
이 더 알맞아서check
를 사용하게 되었어용!