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 11 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()
}
22 changes: 22 additions & 0 deletions src/main/kotlin/wordle/controller/WordleController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package wordle.controller

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

class WordleController {

fun run() {
printStartMessage()
val game = Game(Words.pick(LocalDate.now()))
while (game.isPlaying) {
val answer = Answer(printInputMessage())
game.playRound(answer)
printResults(game.results, game.isPlaying, game.findTryCount())
}

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문을 활용해서 반영해보았습니다.

}
}
58 changes: 58 additions & 0 deletions src/main/kotlin/wordle/domain/Answer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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)
matchExact(word, result, wordTable)
matchExist(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 matchExact(word: String, result: MutableList<Mark>, wordTable: HashMap<Char, Int>) {
for (i in 0 until WORD_SIZE) {

Choose a reason for hiding this comment

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

이렇게 map으로 처리하는 방법도 있겠네요! 꼭 이 방법이 더 좋다는건 아니고, 또 다른 방법도 존재한다는 내용의 코멘트입니다.

Suggested change
for (i in 0 until WORD_SIZE) {
(0 until WORD_SIZE).map { markExact(it, word, result, wordTable) }

markExact(i, word, result, 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 matchExist(result: MutableList<Mark>, wordTable: HashMap<Char, Int>) {
for (i in 0 until WORD_SIZE) {
markExist(i, result, wordTable)
}
}

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 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 { mark -> mark == Mark.EXACT }

Choose a reason for hiding this comment

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

it 키워드를 사용할 수 있겠네요~

Suggested change
results.isLimit() || result.all { mark -> mark == Mark.EXACT }
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
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/wordle/domain/Words.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package wordle.domain

import java.nio.file.Files
import java.nio.file.Paths
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> = Files.readAllLines(Paths.get("src/main/resources/words.txt"))

Choose a reason for hiding this comment

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

불필요한 코드 및 import문을 제거할 수 있겠네요 😄

Suggested change
private val VALUE: List<String> = Files.readAllLines(Paths.get("src/main/resources/words.txt"))
private val VALUE: List<String> = File("src/main/resources/words.txt").readLines()

Copy link
Author

Choose a reason for hiding this comment

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

이런 방법이!!! 감사합니다 리뷰어님🙇‍♀️

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()]
}
}
}
41 changes: 41 additions & 0 deletions src/main/kotlin/wordle/view/View.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package wordle.view

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

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

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

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

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

private fun printResult(result: List<Mark>) {
val stringBuilder = StringBuilder()
result.forEach {
when (it) {
Mark.NONE -> stringBuilder.append("⬜")
Mark.EXIST -> stringBuilder.append("\uD83D\uDFE8")
Mark.EXACT -> stringBuilder.append("\uD83D\uDFE9")

Choose a reason for hiding this comment

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

NONE은 도형을, EXACT, EXIST는 유니코드를 사용한 이유는 무엇인가요?
+) StringBuilder를 사용한 이유는 무엇인가요?

Copy link
Author

Choose a reason for hiding this comment

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

예시 출력을 그대로 붙여넣었더니 통일성 있지 않게 삽입되었네요🥲 통일성을 위해 모두 도형으로 변경하였습니다⬜🟨🟩

매번 String을 생성하여 출력하는 것보단, 가변적인 StringBuilder()를 사용하여 필요한 문자열을 모두 더한 뒤, 한번에 출력하는 것이 효율적이라고 판단하여 사용해보았습니다!

}
}
println(stringBuilder.toString())
}

Empty file removed src/test/kotlin/.gitkeep
Empty file.
38 changes: 38 additions & 0 deletions src/test/kotlin/wordle/domain/AnswerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package wordle.domain

import io.kotest.matchers.throwable.shouldHaveMessage
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import wordle.domain.Mark.*

Choose a reason for hiding this comment

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

./gradlew ktlintCheck가 실패하네요!

import문에 와일드카드를 사용하지 않는 방법이 좋아보입니다 😄

Copy link
Author

Choose a reason for hiding this comment

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

반영하였습니다!


internal class AnswerTest {

@Test
fun 글자길이가_5가_아닌_경우_예외발생() {
assertThrows<IllegalArgumentException> { Answer("abcdef") }

Choose a reason for hiding this comment

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

forAll을 이용하여 4글자인 경우도 테스트해보면 좋을 것 같아요!

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.

assertThrow문 사용하셨군요! 👍 저는 kotest의 shouldThrow를 사용했는데 둘 다 좋네요 ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

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

이전에는 아직 kotest가 익숙하지 않았어요.
이번에 차리와 케이 코드를 보다가 kotest에 관심이 생겨서 변경해보았습니다! 확인해주시면 감사하겠습니다🙏

.shouldHaveMessage("[ERROR] 부적절한 글자 길이입니다.")
}

@Test
fun 주어진_단어목록에_존재하지_않는_경우_예외발생() {
assertThrows<IllegalArgumentException> { Answer("abcde") }
.shouldHaveMessage("[ERROR] 목록에 존재하지 않는 단어입니다.")
}

@Test
fun 답안과_정답을_비교_CASE_중복되는_문자_중_하나만_일치_할_때() {
val answer = Answer("groom")

assertThat(answer.compareToWord("goose"))
.isEqualTo(listOf(EXACT, NONE, EXACT, EXIST, NONE))
}

@Test
fun 답안과_정답을_비교_CASE_중복되는_문자가_존재하지만_정답의_개수가_더_많을_때() {
val answer = Answer("eerie")

assertThat(answer.compareToWord("sheen"))
.isEqualTo(listOf(EXIST, EXIST, NONE, NONE, NONE))
}

Choose a reason for hiding this comment

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

아래의 경우도 테스트해보면 어떨까요?

  • EXACT만 존재하는 경우
  • NONE만 존재하는 경우
  • EXIST만 존재하는 경우 (ex. parse, spear)
  • 중복되는 문자가 존재하면서, 입력한 단어에 그 문자가 더 많은 경우

그 외에 다양한 경우들이 존재할 것 같아요.

Copy link
Author

Choose a reason for hiding this comment

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

테스트가 많이 부족했던거 같아요! 워들 세부 규칙의 예시를 참고하여 추가적인 테스트를 만들어보았습니다🙂

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

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll

internal class GameTest {

@Test
fun 게임을_한_라운드씩_진행() {
val game = Game("fetus")
repeat(3) { game.playRound(Answer("apple")) }

assertAll(
{assertThat(game.findTryCount()).isEqualTo(3)},
{assertThat(game.isPlaying).isTrue()}

Choose a reason for hiding this comment

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

ktlintCheck가 실패하네요 😢
중괄호 사이에 공백이 있는 게 좋아보입니다.

image

Copy link
Author

Choose a reason for hiding this comment

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

ktlintCheck이 통과되도록 수정하였습니다👍
마지막에 확인하는 습관을 잊지 않아야겠어요!

)
}

@Test
fun 게임을_한_라운드씩_진행하다_6라운드에_도달하면_게임종료() {
val game = Game("fetus")
repeat(6) { game.playRound(Answer("apple")) }

assertAll(
{assertThat(game.findTryCount()).isEqualTo(6)},
{assertThat(game.isPlaying).isFalse()}
)
}

@Test
fun 게임을_한_라운드씩_진행하다_정답을_맞추면_게임종료() {
val game = Game("fetus")
repeat(3) { game.playRound(Answer("fetus")) }

assertAll(
{assertThat(game.findTryCount()).isEqualTo(3)},
{assertThat(game.isPlaying).isFalse()}
)
}

@Test
fun 몇_번째_시도인지_계산() {
val game = Game("fetus")
repeat(6) { game.playRound(Answer("apple")) }

assertThat(game.findTryCount()).isEqualTo(6)
}
}
15 changes: 15 additions & 0 deletions src/test/kotlin/wordle/domain/WordsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package wordle.domain

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.time.LocalDate

internal class WordsTest {

@Test
fun 오늘의_단어를_선택() {
val date = LocalDate.of(2022, 5, 12)

assertThat(Words.pick(date)).isEqualTo("fetus")
}
}