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] 산군, 민트, 도기 미션 제출합니다. #16

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,37 @@ spill
🟩🟩⬜🟩🟩
🟩🟩🟩🟩🟩
```

## 기능 목록 정리

- `words.txt` 파일을 통해 정답 정보를 가져온다.
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.

- 6x5 격자를 통해서 5글자 단어를 6번 만에 추측한다.
- 플레이어가 답안을 제출하면 프로그램이 정답과 제출된 단어의 각 알파벳 종류와 위치를 비교해 판별한다.
- 판별 결과는 흰색의 타일이 세 가지 색(초록색/노란색/회색) 중 하나로 바뀌면서 표현된다.
- 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.
- 정답과 답안은 words.txt에 존재하는 단어여야 한다.
- 정답은 매일 바뀌며 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.

## 주요 객체
### WordPicker
- 정답을 뽑아온다.

### Answer
- 사용자의 입력과 정답을 비교하여 결과를 반환한다.
- 정답은 5글자여야 한다.

### WordleGame
- 게임을 진행한다.
- 게임은 총 6번 진행한다.

### Tile
- 타일을 가진다.

abcde
a , b, c, d, e

faaff
회색 / 노란색 / 회색 / 회색 / 회색
1 change: 1 addition & 0 deletions src/main/kotlin/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fun main() = WordleController().run()
19 changes: 19 additions & 0 deletions src/main/kotlin/WordleController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import view.InputView
import view.OutputView
import wordle.domain.WordPicker
import wordle.domain.WordleGame

class WordleController {

fun run() {
val wordleGame = WordleGame(WordPicker())

OutputView.printGameStart()

do {
val userGuess = InputView.readUserGuess()
val gameHistory = wordleGame.playGame(userGuess)
OutputView.printResult(gameHistory)
} while (!wordleGame.isOver())
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package view

import wordle.domain.UserGuess

object InputView {

fun readUserGuess(): UserGuess {
OutputView.printRequestAnswer()
return runCatching {
UserGuess(readln())
}.onFailure { OutputView.printError() }
.getOrElse { readUserGuess() }
}
}


38 changes: 38 additions & 0 deletions src/main/kotlin/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package view

import wordle.domain.Tile
import wordle.domain.Tile.GREEN
import wordle.domain.Tile.GREY
import wordle.domain.Tile.YELLOW

object OutputView {
private const val GREEN_TILE_FORMAT = "🟩"
private const val YELLOW_TILE_FORMAT = "🟨"
private const val GREY_TILE_FORMAT = "⬜"

fun printGameStart() {
println("WORDLE을 6번 만에 맞춰 보세요.")
println("시도의 결과는 타일의 색 변화로 나타납니다.")
}

fun printRequestAnswer() {
println("정답을 입력해 주세요.")
}

fun printError() = println("[ERROR] 다시 시도해주세요🙏")

fun printResult(gameHistory: List<List<Tile>>) {
gameHistory.forEach { history -> printRoundResult(history) }
println()
}

private fun printRoundResult(result: List<Tile>) {
println(result.joinToString("", "", "") { tile -> mapTile(tile) })
}

private fun mapTile(tile: Tile) = when (tile) {
GREEN -> GREEN_TILE_FORMAT
YELLOW -> YELLOW_TILE_FORMAT
GREY -> GREY_TILE_FORMAT
}
}
76 changes: 76 additions & 0 deletions src/main/kotlin/wordle/domain/Answer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package wordle.domain

import wordle.domain.Tile.*
// ktlint-disable no-wildcard-imports

class Answer(private val value: String) {

init {
require(value.length == WORD_LENGTH) { "정답은 " + WORD_LENGTH + "글자여야 합니다." }
require(!value.contains(BLANK)) { "정답에 공백이 들어갈 수 없습니다." }
}

fun judge(input: String): List<Tile> {
val indexMarker: MutableMap<Char, MutableList<Int>> = HashMap()
val result = mutableListOf(GREY, GREY, GREY, GREY, GREY)

markAnswerCharacterIndex(indexMarker)

checkGreen(input, indexMarker, result)
checkYellow(input, indexMarker, result)

return result.toList()
}

private fun markAnswerCharacterIndex(map: MutableMap<Char, MutableList<Int>>) {
value.forEachIndexed { index, char ->
if (!map.containsKey(char)) map[char] = mutableListOf()

map[char]!!.add(index)
}
}

private fun checkGreen(
input: String,
indexMarker: MutableMap<Char, MutableList<Int>>,
result: MutableList<Tile>,
) {
input.forEachIndexed { index, char ->
if (isGreenCondition(indexMarker, char, index)) {
result[index] = GREEN
indexMarker[char]!!.remove(index)
}
}
}

private fun isGreenCondition(
indexMarker: MutableMap<Char, MutableList<Int>>,
char: Char,
index: Int,
) = indexMarker.containsKey(char) && indexMarker[char]!!.contains(index)

private fun checkYellow(
input: String,
indexMarker: MutableMap<Char, MutableList<Int>>,
result: MutableList<Tile>,
) {
input.forEachIndexed { index, char ->
if (isYellowCondition(indexMarker, char, result, index)) {
result[index] = YELLOW
indexMarker[char]!!.removeAt(0)
}
}
}

private fun isYellowCondition(
indexMarker: MutableMap<Char, MutableList<Int>>,
char: Char,
result: MutableList<Tile>,
index: Int,
) = indexMarker.containsKey(char) && indexMarker[char]!!.isNotEmpty() && result[index] != GREEN

companion object {
private const val WORD_LENGTH = 5
private const val BLANK = " "
}
}
6 changes: 6 additions & 0 deletions src/main/kotlin/wordle/domain/Tile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package wordle.domain

enum class Tile {

GREEN, YELLOW, GREY
}
13 changes: 13 additions & 0 deletions src/main/kotlin/wordle/domain/UserGuess.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wordle.domain

@JvmInline
value class UserGuess(val value: String) {

init {
require((value.length) == REQUIRED_LENGTH)
}

companion object {
private const val REQUIRED_LENGTH = 5
}
}
33 changes: 33 additions & 0 deletions src/main/kotlin/wordle/domain/WordPicker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package wordle.domain

import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar

class WordPicker {

fun pick(): String {
val words = getWords()
val index = getIndexOfAnswer(words)
return words[index]
}

private fun getIndexOfAnswer(words: List<String>): Int {
val today = Calendar.getInstance()
val criteria = SimpleDateFormat("yyyy-mm-dd").parse(CRITERIA_DATE)

val result = (today.time.time - criteria.time) / (60 * 60 * 24 * 1000)

return result.toInt() % words.size
}

private fun getWords(): List<String> {
val file = ClassLoader.getSystemResource(FILE_NAME).file
return File(file).readLines()
}

companion object {
private const val CRITERIA_DATE = "2021-06-19"
private const val FILE_NAME = "words.txt"
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/wordle/domain/WordleGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package wordle.domain

class WordleGame(private val wordPicker: WordPicker) {
private val history: MutableList<List<Tile>> = mutableListOf()

private val todayAnswer: Answer by lazy { Answer(wordPicker.pick()) }

fun playGame(
userGuess: UserGuess,
): List<List<Tile>> {
val judgeResult = todayAnswer.judge(userGuess.value)

history.add(judgeResult)

return history.toList()
}

fun isOver(): Boolean {
val greenCount = history.last().count { it == Tile.GREEN }

return (greenCount == ALL_CORRECT_COUNT) or (history.size == NO_MORE_COUNT)
}

companion object {
private const val ALL_CORRECT_COUNT = 5
private const val NO_MORE_COUNT = 6
}
}
34 changes: 34 additions & 0 deletions src/test/kotlin/wordle/WordPickerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package wordle

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import wordle.domain.WordPicker
import java.io.File

class WordPickerTest {

@Test
fun 단어를_추출한다() {
// given
val wordPicker = WordPicker()

// when
val actual = wordPicker.pick()

// then
assertThat(actual).isNotEmpty
}

@Test
fun 추출한_단어는_words_텍스트_파일에_존재한다() {
// given
val wordPicker = WordPicker()
val words = File(ClassLoader.getSystemResource("words.txt").file).readLines()

// when
val word = wordPicker.pick()

// then
assertThat(word).isIn(words)
}
}
57 changes: 57 additions & 0 deletions src/test/kotlin/wordle/domain/AnswerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package wordle.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.jupiter.params.provider.ValueSource
import wordle.domain.Tile.*
import java.util.stream.Stream
// ktlint-disable no-wildcard-imports

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AnswerTest {

@Test
fun 정답은_5글자여야_한다() = assertDoesNotThrow { Answer("우크라이나") }

@ValueSource(strings = ["멕시코", "대한민국", "남아프리카공화국"])
@ParameterizedTest
fun 정답이_5글자가_아니면_예외가_발생한다(value: String) {
assertThrows<IllegalArgumentException>("정답은 5글자여야 합니다.") { Answer(value) }
}

@Test
fun 정답에_공백이_들어가면_예외가_발생한다() {
assertThrows<IllegalArgumentException>("정답에 공백이 들어갈 수 없습니다.") { Answer("sds d") }
}

@ParameterizedTest
@MethodSource("getAnswerJudgeTestCase")
fun 정답과_입력값을_비교한다(answer: String, input: String, expected: List<Tile>) {
// given
val realAnswer = Answer(answer)

// when
val judgeResult = realAnswer.judge(input)

// then
assertThat(judgeResult).isEqualTo(expected)
}

private fun getAnswerJudgeTestCase(): Stream<Arguments?>? {
return Stream.of(
Arguments.of("auple", "poppy", listOf(GREY, GREY, GREEN, GREY, GREY)),
Arguments.of("apple", "poppy", listOf(YELLOW, GREY, GREEN, GREY, GREY)),
Arguments.of("hello", "label", listOf(YELLOW, GREY, GREY, YELLOW, YELLOW)),
Arguments.of("aalll", "llbaa", listOf(YELLOW, YELLOW, GREY, YELLOW, YELLOW)),
Arguments.of("apple", "pbbpp", listOf(YELLOW, GREY, GREY, YELLOW, GREY)),
Arguments.of("aaall", "bbaaa", listOf(GREY, GREY, GREEN, YELLOW, YELLOW)),
Arguments.of("aaall", "lllaa", listOf(YELLOW, YELLOW, GREY, YELLOW, YELLOW)),
)
}
}