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] 페퍼(최수연) 미션 제출합니다. #10

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a07ca44
docs: README.md 기능목록 추가
jojogreen91 May 11, 2022
13a13cf
feat: Answer 글자길이를 검증하는 기능 구현
jojogreen91 May 11, 2022
c1d7d33
test: AnswerTest 테스트작성
jojogreen91 May 12, 2022
778f9c1
feat: Answer 가 `words.txt` 에 존재하는 단어인지 검증하는 기능
jojogreen91 May 12, 2022
0782fbf
feat: 오늘의 정답을 선택하는 기능
jojogreen91 May 12, 2022
58cbf75
feat: 답안과 정답을 비교하는 기능 구현
jojogreen91 May 12, 2022
551f64f
feat: 입출력 기능 구현
jojogreen91 May 12, 2022
f0c74c9
feat: Wordle 게임을 실행하는 기능 구현
jojogreen91 May 12, 2022
d9a554d
feat: 어플리케이션을 실행하는 기능 구현
jojogreen91 May 12, 2022
8f1f5f0
test: Game 테스트 구현
jojogreen91 May 13, 2022
5422144
test: 정답과 비교하는 테스트 케이스 추가 구현
jojogreen91 May 13, 2022
92ee394
feat: 유저의 입력이 잘못된 경우, 재입력을 받도록 변경
SuyeonChoi May 22, 2022
7fbf22a
refactor: 유니코드를 도형 아이콘으로 변경
SuyeonChoi May 22, 2022
4dc585c
refactor: 불필요한 코드 제거 및 개선
SuyeonChoi May 22, 2022
91d964c
test: 백틱을 사용하여 테스트 함수명 변경
SuyeonChoi May 22, 2022
98a1af3
test: kotest를 적용하여 테스트 코드를 변경
SuyeonChoi May 22, 2022
aaaad52
test(AnswerTest): 글자길이가 5가 아닌 경우의 경계값 테스트 추가
SuyeonChoi May 22, 2022
066b045
test(GameTest): 경계를 테스트하도록 변경
SuyeonChoi May 22, 2022
0350518
test(AnswerTest): 테스트 케이스 추가
SuyeonChoi May 22, 2022
084a409
refactor(AnswerTest): wildcard import문 제거
SuyeonChoi May 22, 2022
3c355c2
test: DslTest 수업 및 실습 코드 추가
SuyeonChoi May 22, 2022
9dae55e
style: 코드 리포맷팅
SuyeonChoi May 22, 2022
9e1193a
refactor: 단어를 맞출 수 있는 횟수를 Game에 선언하여 사용하도록 변경
SuyeonChoi May 22, 2022
3e3219e
refactor(Words): object 클래스로 변경
SuyeonChoi May 29, 2022
5e3965e
refactor(Game): 불필요한 코드 길이 개선
SuyeonChoi May 31, 2022
4f9251e
style(AnswerTest): import 순서가 컨벤션을 지키도록 변경
SuyeonChoi May 31, 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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# 미션 - 워들

## 📝 기능 목록

### 코틀린 워들 기능목록 / 페퍼, 조조그린

- [x] 시작메시지 출력하는 기능
- [x] 답안을 입력받는 기능
- [x] 입력값은 5자리 문자열로 제한하는 기능
- [x] 입력값은 `words.txt` 에 존재하는 단어만 가능
- [x] 답안은 6번 만 입력 가능
- [x] 오늘의 정답을 선택하는 기능
- [x] 정답은 `words.txt` 의 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어
- [x] 답안이 각 문자가 정답의 문자와 어떻게 매칭되는지 찾는 기능
- [x] 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 표시
- [x] 확인된 정보에 맞게 출력하는 기능
- [x] 맞는 글자는 초록색, 위치가 틀리면 노란색, 없으면 회색으로 표시
- [x] 게임 결과를 출력하는 기능

## 🔍 진행 방식

- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
Expand Down
Empty file removed src/main/kotlin/.gitkeep
Empty file.
5 changes: 5 additions & 0 deletions src/main/kotlin/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import wordle.controller.WordleController

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

import wordle.domain.Answer
import wordle.domain.Game
import wordle.domain.Words
import wordle.view.printErrorMessage
import wordle.view.printResults
import wordle.view.printStartMessage
import wordle.view.requestInput
import java.time.LocalDate

class WordleController {

fun run() {
val game = Game(Words.pick(LocalDate.now()))
printStartMessage(game.fixedCount)

while (game.isPlaying) {
val answer = requestAnswer()
game.playRound(answer)
printResults(game.results, game.isPlaying, game.findTryCount(), game.fixedCount)
}
}

private fun requestAnswer(): Answer {
return try {
Answer(requestInput())
} catch (e: IllegalArgumentException) {
printErrorMessage(e.message)
requestAnswer()
}

Choose a reason for hiding this comment

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

올바르지 않은 단어를 입력했을 때 재입력받게 하면 유저들이 더 편하게 게임을 이용할 수 있어보이네요!
image

Copy link
Author

Choose a reason for hiding this comment

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

게임이 종료되는 상황을 피할 수 있겠군요~! try-catch문을 활용해서 반영해보았습니다.

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

class Answer(private val answer: String) {

init {
require(answer.length == WORD_SIZE) { "[ERROR] 부적절한 글자 길이입니다." }
require(Words.contains(answer)) { "[ERROR] 목록에 존재하지 않는 단어입니다." }
}

fun compareToWord(word: String): MutableList<Mark> {
val result = MutableList(WORD_SIZE) { Mark.NONE }
val wordTable = createWordTable(word)
(0 until WORD_SIZE).map { markExact(it, word, result, wordTable) }
(0 until WORD_SIZE).map { markExist(it, result, wordTable) }
return result

Choose a reason for hiding this comment

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

matchExact, matchExist 함수를 안쓰고 아래 코드로 작성하는 방법도 있어보이는데 페퍼가 보기에는 어떤가요~?

    fun compareToWord(word: String): MutableList<Mark> {
        val result = MutableList(WORD_SIZE) { Mark.NONE }
        val wordTable = createWordTable(word)
        (0 until WORD_SIZE).map { markExact(it, word, result, wordTable) }
        (0 until WORD_SIZE).map { markExist(it, result, wordTable) }
        return result
    }

Copy link
Author

Choose a reason for hiding this comment

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

반복하는 iter문에서 map을 사용할 수 있군요😲😲
코드가 더 짧아져서 훨씬 가독성있을거 같아요!! 반영하였습니다👍👍

}

private fun createWordTable(word: String): HashMap<Char, Int> {
val wordTable = HashMap<Char, Int>()
for (char in word) {
wordTable[char] = wordTable.getOrDefault(char, 0) + 1
}
Comment on lines +20 to +22

Choose a reason for hiding this comment

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

이 작업은 map으로도 똑같이 가능하더라고요! 그래서 map이랑 foreach랑 차이점이 뭘까 생각이 들어서 한번 검색해봤는데, 이러한 차이가 있었습니다. (Is there a difference between foreach and map?)

만약 HashMap이 아닌 List나 Set이었으면 map을 이용하는 것이 의미가 있었을 것 같은데, HashMap이어서 forEach가 더 좋아보이네요. 결론은 foreach 잘썼다 👍

Copy link
Author

Choose a reason for hiding this comment

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

유용한 링크도 첨부해주셔서 저도 덕분에 함께 공부했습니다😊😊 감사합니다!

return wordTable
}

private fun markExact(i: Int, word: String, result: MutableList<Mark>, wordTable: HashMap<Char, Int>) {
if (word[i] == answer[i]) {
result[i] = Mark.EXACT
wordTable.computeIfPresent(word[i]) { _, v -> v - 1 }
}
}

private fun markExist(i: Int, result: MutableList<Mark>, wordTable: HashMap<Char, Int>) {
if (isExist(i, result, wordTable, answer[i])) {
result[i] = Mark.EXIST
wordTable.computeIfPresent(answer[i]) { _, v -> v - 1 }
}
}

private fun isExist(
i: Int,
result: MutableList<Mark>,
wordTable: HashMap<Char, Int>,
charOfAnswer: Char,
) = result[i] == Mark.NONE && wordTable.containsKey(charOfAnswer) && wordTable[charOfAnswer] != 0
}
23 changes: 23 additions & 0 deletions src/main/kotlin/wordle/domain/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package wordle.domain

class Game(private val word: String) {
val fixedCount = 6
val results: Results = Results()
var isPlaying: Boolean = true
private set

fun playRound(answer: Answer) {
val result = answer.compareToWord(word)
results.add(result)
if (isOver(result)) {
isPlaying = false
}

Choose a reason for hiding this comment

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

이건 정말 쓸모없는 팁이긴 하지만, 아래처럼 쓸 수도 있습니다 ㅎㅎ

isPlaying = !isOver(result)

Copy link
Author

Choose a reason for hiding this comment

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

오! 코드 길이를 줄일 수 있는 유용한 팁인걸요?! 바로 반영하였습니다!
역시 리뷰어 kth!👏👏

}

private fun isOver(result: MutableList<Mark>) =
results.isLimit() || result.all { it == Mark.EXACT }

fun findTryCount(): Int {
return results.value.size
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/wordle/domain/Mark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wordle.domain

enum class Mark {

NONE,
EXIST,
EXACT
}
16 changes: 16 additions & 0 deletions src/main/kotlin/wordle/domain/Results.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package wordle.domain

private const val LIMIT_SIZE = 6

class Results {

val value: MutableList<List<Mark>> = mutableListOf()

fun add(result: List<Mark>) {
value.add(result)
}

fun isLimit(): Boolean {
return value.size >= LIMIT_SIZE
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/wordle/domain/Words.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package wordle.domain

import java.io.File
import java.time.LocalDate
import java.time.temporal.ChronoUnit

const val WORD_SIZE = 5

class Words {

companion object {

Choose a reason for hiding this comment

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

companion object을 활용하여 캐싱한건가요? 👍

Copy link
Author

Choose a reason for hiding this comment

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

넵 불필요한 데이터를 매번 생성하지 않게끔 활용해보았습니다〰️

Choose a reason for hiding this comment

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

현재 Words 클래스에 companion object밖에 없는데, Words 클래스 자체를 object 클래스로 만드는 건 어떤가요?

Copy link
Author

Choose a reason for hiding this comment

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

객체를 생성할 일이 없으니 object클래스를 사용하는 방식이 더 낫겠네요! 반영하였습니다~!

private val VALUE: List<String> = File("src/main/resources/words.txt").readLines()
private val BASIC_DATE = LocalDate.of(2021, 6, 19)

fun contains(word: String): Boolean {
return VALUE.contains(word)
}

fun pick(date: LocalDate): String {
val index = ChronoUnit.DAYS.between(BASIC_DATE, date) % VALUE.size

Choose a reason for hiding this comment

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

ChronoUnit.DAYS 사용 좋네요! 배워갑니다 👍
추가로, 페퍼의 코드 덕분에 ChronoUnit의 between과 Period의 between 차이를 공부해볼 수 있었어요 :)

Copy link
Author

Choose a reason for hiding this comment

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

케이님의 링크 덕분에 저도 다시한번 복습했네요😄😄

return VALUE[index.toInt()]
}
}
}
44 changes: 44 additions & 0 deletions src/main/kotlin/wordle/view/View.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package wordle.view

import wordle.domain.Mark
import wordle.domain.Results

fun printStartMessage(fixedCount: Int) {
println("WORDLE 을 $fixedCount 번 만에 맞춰 보세요.\n시도의 결과는 타일의 색 변화로 나타납니다.")
}

fun requestInput(): String {
println("정답을 입력해 주세요.")
return readln()
}

fun printResults(results: Results, isPlaying: Boolean, tryCount: Int, fixedCount: Int) {
println()
if (!isPlaying) {
printTryCount(tryCount, fixedCount)
}
results.value.forEach {
printResult(it)
}
println()
}

fun printTryCount(tryCount: Int, fixedCount: Int) {
println("$tryCount/$fixedCount\n")
}

private fun printResult(result: List<Mark>) {
val stringBuilder = StringBuilder()
result.forEach {
when (it) {
Mark.NONE -> stringBuilder.append("⬜")
Mark.EXIST -> stringBuilder.append("🟨")
Mark.EXACT -> stringBuilder.append("🟩")
}
}
println(stringBuilder.toString())
}

fun printErrorMessage(message: String?) {
println(message)
}
Empty file removed src/test/kotlin/.gitkeep
Empty file.
51 changes: 51 additions & 0 deletions src/test/kotlin/practice/DslTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package practice

import org.assertj.core.api.AssertionsForInterfaceTypes.assertThat
import org.junit.jupiter.api.Test

class DslTest {

@Test
fun `자기소개 프러퍼티 버전`() {
val person = introduce {
name = "최수연"
age = 26
}
assertThat(person.name).isEqualTo("최수연")
assertThat(person.age).isEqualTo(26)
}

@Test
fun `자기소개 함수 버전`() {
val person = introduce {
name("최수연")
age(26)
}
assertThat(person.name).isEqualTo("최수연")
assertThat(person.age).isEqualTo(26)
}
}

private fun introduce(builder: PersonBuilder.() -> Unit): Person {
// 람다에서 함수 형식을 지정가능
return PersonBuilder().apply(builder).build()
}

class PersonBuilder {
lateinit var name: String
var age: Int = 0

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

fun age(value: Int) {
age = value
}

fun build(): Person {
return Person(name, age)
}
}

data class Person(val name: String, val age: Int)
82 changes: 82 additions & 0 deletions src/test/kotlin/study/DslTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package study

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.groups.Tuple
import org.junit.jupiter.api.Test

class DslTest {
@Test
fun `페퍼 자기소개 프러퍼티 버전`() {
val profile = introduce {
name("최수연")
company("우아한테크코스")
skills {
soft("A passion for problem solving")
soft("Good communication skills")
hard("Kotlin")
}
languages {
"Korean" level 5
"English" level 3
}
}
assertThat(profile.name).isEqualTo("최수연")
assertThat(profile.company).isEqualTo("우아한테크코스")
assertThat(profile.skills.soft).contains("A passion for problem solving", "Good communication skills")
assertThat(profile.skills.hard).contains("Kotlin")
assertThat(profile.languages.languages).extracting("type", "level")
.contains(
Tuple.tuple("Korean", 5),
Tuple.tuple("English", 3)
)
}
}

fun introduce(builder: ProfileBuilder.() -> Unit): Profile {
return ProfileBuilder().apply(builder).build()
}

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

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

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

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

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

fun build(): Profile {
return Profile(name, company, skills, languages)
}
}

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

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

fun soft(value: String) {
soft.add(value)
}
}
Comment on lines +68 to +76

Choose a reason for hiding this comment

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

저는 SkillBuilder를 따로 만들어줬는데, 이 방법도 좋네요!
그 외 부분은 거의 동일하네요 ㅎㅎ


data class Languages(val languages: MutableList<Language>) {
infix fun String.level(value: Int) = languages.add(Language(this, value))
}

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