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] 로마(김범수) 미션 제출합니다. #12

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5577af4
docs: 기능 요구 사항 추가
kbsat May 12, 2022
913234c
feat: GameWord 생성 기능 구현
summerlunaa May 12, 2022
9adf4b7
refactor: 클래스 이름 수정
kbsat May 13, 2022
490274e
test: 테스트에 forAll 사용
kbsat May 13, 2022
6fd8eb7
feat: Words 구현
kbsat May 13, 2022
41725cb
feat: WordsReader 구현
kbsat May 13, 2022
eb2e781
feat: 단어를 비교하여 결과를 계산하는 로직 구현
kbsat May 13, 2022
50de938
feat: Game 기능 구현
kbsat May 13, 2022
6ca90e7
feat: View 구현 및 Application 구현
kbsat May 13, 2022
9a24bc9
refactor: 패키지 구조 변경
kbsat May 14, 2022
88c3d73
docs: 기능 요구사항 체크
kbsat May 14, 2022
c44d184
refactor: println 두개로 나누어 표현
kbsat May 14, 2022
316ce9c
refactor: Word를 소문자로 자동으로 바꾸도록 변경
kbsat May 15, 2022
787cc36
refactor: 매직넘버 상수화
kbsat May 15, 2022
6c159c8
style: enum 표기방식 수정
kbsat May 15, 2022
14570c8
style: 클래스 내 블록 순서 수정
kbsat May 15, 2022
c671c8f
fix: count 증가 로직 순서 변경
kbsat May 16, 2022
e0c0485
style: ktlintCheck 수행
kbsat May 16, 2022
0223a01
refactor: 총 라운드 수를 외부에서 주입받는 형식의 printStartMessage로 변경 및 it 사용
kbsat May 16, 2022
ba60252
test: shouldHaveMessage 활용
kbsat May 16, 2022
0fe17d0
test: DslTest 작성
kbsat May 18, 2022
fa76e29
style: ktlint 적용
kbsat May 25, 2022
5f11353
refactor: Words 리팩토링 ( findAnswer, answerMap 선언 )
kbsat May 25, 2022
a06696b
refactor: Words 리팩토링
kbsat May 25, 2022
8f373bd
refactor: printCount round 입력추가
kbsat May 29, 2022
50c25d1
refactor: Word 확장함수 적용
kbsat May 29, 2022
1d22723
refactor: Regex를 미리 생성해놓고 사용하는 느낌으로 변경
kbsat May 29, 2022
6df3404
refactor: 재귀문 제거하고 while문으로 대체
kbsat May 30, 2022
37d95d8
refactor: 예외 발생시 다시 입력받을 수 있도록 수정
kbsat May 30, 2022
ef7eab0
refactor: Tile 출력과 관련된 정보를 View로 이동
kbsat May 30, 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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@

## 🚀 기능 요구 사항

- [x] 시작 문구를 띄워준다.
- [x] 답안을 입력받는다.
- [x] 답안은 5글자여야 한다.
- 답안은 `words.txt` 안에 존재하는 단어여야 한다.
- [x] 정답과 답안을 비교하여 결과를 타일로 표현한다.
- 맞는 글자는 초록색으로 표현한다.
- 위치가 틀리면 노란색으로 표현한다.
- 정답에 존재하지 않는 글자면 회색으로 표현한다.
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.
- [x] 정답은 매일 바뀐다.
- [x] `words.txt` 안의 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.
- [x] 정답을 맞췄을 경우 타일 표현 전에 몇 번째 만에 맞췄는지 출력한다.

Copy link

@dongho108 dongho108 May 15, 2022

Choose a reason for hiding this comment

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

./gradlew ktlintCheck 이 실패하네요! 한번 확인해보시면 좋을 것 같아요

(여기서 틀렸다는게 아니라 코멘트만 여기에 남긴거에요!)

Copy link
Author

Choose a reason for hiding this comment

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

오 감사합니다! 수정하고 깜빡한 것 같아요 😥

선풍적인 인기를 끌었던 영어 단어 맞추기 게임이다.

- 6x5 격자를 통해서 5글자 단어를 6번 만에 추측한다.
Expand Down
32 changes: 32 additions & 0 deletions src/main/kotlin/wordle/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package wordle

import wordle.domain.Game
import wordle.domain.Word
import wordle.utils.WordsReader
import wordle.view.InputView
import wordle.view.OutputView

fun main() {
val game = Game(WordsReader.getWords())
OutputView.printStartMessage(game)
doGame(game)
}

private fun doGame(game: Game) {
try {
submitAnswer(game)
} catch (exception: IllegalArgumentException) {
OutputView.printErrorMessage(exception)
doGame(game)
}
}

private fun submitAnswer(game: Game) {
var isOver = false
while (!isOver) {
val answer = Word(InputView.inputAnswer())
game.match(answer)
isOver = game.isGameOver(answer)
OutputView.printResults(game, isOver)
}
}
30 changes: 30 additions & 0 deletions src/main/kotlin/wordle/domain/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package wordle.domain

private const val TOTAL_ROUNDS_NUM = 6

class Game(
private val words: Words,
val totalCount: Int = TOTAL_ROUNDS_NUM
) {

var count: Int = 0
private set

var results: MutableList<List<Tile>> = ArrayList()
private set

constructor(
words: List<Word>,
totalCount: Int = TOTAL_ROUNDS_NUM
) : this(Words(words), totalCount)

fun isGameOver(answer: Word): Boolean {
return count >= totalCount || words.isCorrect(answer)
}

fun match(answer: Word) {
require(words.contains(answer)) { "등록된 단어가 아닙니다." }
results.add(words.check(answer))
count++
}
}
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() {

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

private const val WORD_SIZE = 5
private val ALPHABET_REGEX = Regex("[a-zA-Z]*")

class Word(_value: String) {

val value: String = _value.lowercase()

init {
require(value.isRightSize()) { "단어의 길이는 5글자여야 합니다." }
require(value.isAlphabet()) { "단어에 영어가 아닌 글자나 공백이 포함될 수 없습니다." }
}

fun isSameChar(other: Word, index: Int): Boolean {
return this.value[index] == other.value[index]
}

private fun String.isRightSize(): Boolean {
return this.length == WORD_SIZE
}

private fun String.isAlphabet(): Boolean {
return ALPHABET_REGEX.matches(this)
}

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

other as Word

if (value != other.value) return false

return true
}

override fun hashCode(): Int {
return value.hashCode()
}
Comment on lines +27 to +40

Choose a reason for hiding this comment

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

data클래스로 만들면 equals hashcode가 필요 없지 않을까요?

Copy link
Author

Choose a reason for hiding this comment

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

해당 클래스에서 lowerCase로 바꿔주는 작업을 위해서 주생성자에서 인자의 형식으로 값을 받습니다.
그렇기 때문에 프로퍼티가 선언이 되어있지 않는 주생성자는 data class로 바꿀 수 없었습니다. 그래서 직접 정의해주는 방식을 선택했습니다~!

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

import java.time.LocalDate

class Words(private val values: List<Word>) {

private val answer: Word = findAnswer()
private var answerMap: MutableMap<Char, Int> = mutableMapOf()

private fun findAnswer(): Word {
val days = TODAY.compareTo(STANDARD_DATE)
return values[days % values.size]
}

fun contains(word: Word): Boolean {
return values.contains(word)
}

fun isCorrect(word: Word): Boolean {
return word == answer
}

fun check(word: Word): List<Tile> {
answerMap = initAnswerMap()
return DEFAULT_TILES.markGreen(word).markYellow(word)
}

private fun initAnswerMap(): MutableMap<Char, Int> {
return answer.value
.groupBy { it }
.mapValues { it.value.count() }
.toMutableMap()
}

private fun List<Tile>.markGreen(word: Word): List<Tile> {
return List(size) { index -> grayOrGreen(word, index) }
}

private fun grayOrGreen(word: Word, index: Int): Tile {
return if (answer.isSameChar(word, index)) {
calculateAnswerMap(word.value[index])
Tile.GREEN
} else {
Tile.GRAY
}
}

private fun List<Tile>.markYellow(word: Word): List<Tile> {
return mapIndexed { index, tile -> existOrYellow(tile, word, index) }
}

private fun existOrYellow(tile: Tile, word: Word, index: Int): Tile {
return if (tile != Tile.GREEN && answerMap.containsKey(word.value[index])) {
calculateAnswerMap(word.value[index])
Tile.YELLOW
} else {
tile
}
}

private fun calculateAnswerMap(key: Char) {
answerMap.computeIfPresent(key) { _, v -> v - 1 }
if (answerMap[key] == 0) {
answerMap.remove(key)
}
}

companion object {
private val TODAY = LocalDate.now()
private val STANDARD_DATE = LocalDate.of(2021, 6, 19)
private val DEFAULT_TILES = List(5) { Tile.GRAY }
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/wordle/utils/WordsReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package wordle.utils

import wordle.domain.Word
import java.io.FileReader

private const val WORDS_PATH = "src/main/resources/words.txt"

object WordsReader {

fun getWords(): List<Word> {
val reader = FileReader(WORDS_PATH)
return reader.readLines().map { Word(it) }
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/wordle/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package wordle.view

object InputView {

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

import wordle.domain.Game
import wordle.domain.Tile

object OutputView {

fun printStartMessage(game: Game) = println(
"""
|WORDLE을 ${game.totalCount}번 만에 맞춰 보세요.
|시도의 결과는 타일의 색 변화로 나타납니다.
""".trimMargin()
)

fun printResults(game: Game, isGameOver: Boolean) {
if (isGameOver) {
printCount(game.count, game.totalCount)
}
println()
game.results.forEach { printResult(it) }
println()
}

private fun printResult(result: List<Tile>) {
result.forEach { printTile(it) }
println()
}

private fun printCount(count: Int, totalRound: Int) = println(
"""
|
|$count/$totalRound
""".trimMargin()
)

fun printErrorMessage(exception: RuntimeException) {
println(exception.message)
}

private fun printTile(tile: Tile) {
when (tile) {
Tile.GREEN -> print("🟩")
Tile.YELLOW -> print("🟨")
Tile.GRAY -> print("⬜")
}
}
}
106 changes: 106 additions & 0 deletions src/test/kotlin/study/DslTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package study

import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

/*
introduce {
name("박재성")
company("우아한형제들")
skills {
soft ("A passion for problem solving")
soft ("Good communication skills")
hard ("Kotlin")
}
languages {
"Korean" level 5
"English" level 3
}
}
*/
class DslTest {

@Test
fun `자기소개 함수 버전`() {
val person = introduce {
name("김범수")
company("우아한형제들")
skills {
soft("코틀린")
soft("자바")
hard("운동")
}
languages {
"Korean" level 100
"English" level 10
"Japanese" level 0
}
}

assertSoftly(person) {
it.name shouldBe "김범수"
it.company shouldBe "우아한형제들"
it.skills.soft shouldHaveSize 2
it.skills.hard shouldHaveSize 1
it.languages.values shouldHaveSize 3
it.languages.values shouldContainExactly listOf(
Language("Korean", 100),
Language("English", 10),
Language("Japanese", 0)
)
}
}
}

fun introduce(builder: PersonBuilder.() -> Unit): Person {
return PersonBuilder().apply(builder).build()
}

class PersonBuilder {
lateinit var name: String
lateinit var company: String
lateinit var skills: Skills
lateinit var languages: Languages

fun name(value: String) {
name = value
}

fun company(value: String) {
company = value
}

fun skills(builder: Skills.() -> Unit) {
skills = Skills(mutableListOf(), mutableListOf()).apply(builder)
}

fun languages(builder: Languages.() -> Unit) {
languages = Languages(mutableListOf()).apply(builder)
}

fun build(): Person = Person(name, company, skills, languages)
}

data class Person(val name: String, val company: String, val skills: Skills, val languages: Languages)

class Skills(val soft: MutableList<String>, val hard: MutableList<String>) {
fun soft(value: String) {
soft.add(value)
}

fun hard(value: String) {
hard.add(value)
}
}

class Languages(val values: MutableList<Language>) {

infix fun String.level(level: Int) {
values.add(Language(this, level))
}
}

data class Language(val value: String, val level: Int)
Loading