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] 파랑(이하은) 미션 제출합니다. #13

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
44 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
6c29542
chore: 패키지 구조 변경
summerlunaa May 13, 2022
a77bf9e
chore: 사용하지 않는 import 정리
summerlunaa May 14, 2022
0a62335
test: shouldHaveMessage 메서드 적용
summerlunaa May 15, 2022
200333b
fix: 등록된 단어가 아닌 경우 count가 증가되지 않도록 수정
summerlunaa May 15, 2022
7812438
refactor: match 결과 구하는 로직 수정
summerlunaa May 15, 2022
80fd53d
docs: 기능 요구사항 체크
summerlunaa May 15, 2022
2d19c45
refactor: 변수 상수화
summerlunaa May 15, 2022
125d27d
chore: 중괄호 삭제
summerlunaa May 15, 2022
668cbdd
refactor: 상수 6을 Game에서 가져와서 사용하는 것으로 변경
summerlunaa May 15, 2022
6a295e8
feat: Dsl study 구현
summerlunaa May 18, 2022
ed3ab9f
feat: Dsl study 구현
summerlunaa May 18, 2022
d12e369
fix: 결과를 가져오는 과정에서 에러가 발새하는 경우 count가 증가되지 않도록 수정
summerlunaa May 20, 2022
f45ae33
refactor: 메서드명 수정
summerlunaa May 20, 2022
7832770
conflict 해결
summerlunaa May 20, 2022
be18256
refactor: sealed class 사용
summerlunaa May 20, 2022
102e8d7
refactor: blocking properties 언더바 및 getter 추가
summerlunaa May 21, 2022
23bb4e4
refactor: maxGameCount 추가
summerlunaa May 21, 2022
6a28632
refactor: Game, Words에서 생성시 date를 입력받을 수 있도록 수정
summerlunaa May 21, 2022
961eac9
refactor: 로직 변경
summerlunaa May 21, 2022
8df1ac1
refactor: main 및 view 로직 변경
summerlunaa May 22, 2022
d7e35f8
refactor: pattern 상수화
summerlunaa May 22, 2022
ace29e4
refactor: mutableMap으로 변경
summerlunaa May 23, 2022
e210cfe
refactor: count 접근 제어자 변경 및 setter private으로 지정
summerlunaa May 23, 2022
9e9c283
refactor: initAnswerMap 리턴타입 변경
summerlunaa May 23, 2022
fd86a0e
refactor: joinToString 적용
summerlunaa May 23, 2022
ac5ff8f
refactor: kotlin Path 사용
summerlunaa May 23, 2022
ad32c27
refactor: MutableList를 사용하지 않도록 로직 변경 및 확장함수 적용
summerlunaa May 23, 2022
2370f77
refactor: 게임 종료 여부를 판단하는 로직 변경
summerlunaa May 30, 2022
a2d57b4
fix: 잘못된 값이 들어온 경우 입력을 다시 받도록 수정
summerlunaa May 30, 2022
f35a779
refactor: 메서드에 통일성 부여
summerlunaa May 30, 2022
7696783
refactor: mutableMap을 사용하지 않도록 로직 변경
summerlunaa May 30, 2022
fce9fe0
test: AnnotationSpec 사용
summerlunaa May 31, 2022
5693dcb
test: FunSpec 사용
summerlunaa Jun 2, 2022
6e78472
refactor: 답안에 대한 타일 결과를 구하는 로직 변경
summerlunaa Jun 2, 2022
3cdc51e
chore: 불필요한 파일 삭제
summerlunaa Jun 2, 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] 답안은 5글자여야 한다.
- 답안은 `words.txt` 안에 존재하는 단어여야 한다.
- [ ] 정답과 답안을 비교하여 결과를 타일로 표현한다.
- 맞는 글자는 초록색으로 표현한다.
- 위치가 틀리면 노란색으로 표현한다.
- 정답에 존재하지 않는 글자면 회색으로 표현한다.
- 두 개의 동일한 문자를 입력하고 그중 하나가 회색으로 표시되면 해당 문자 중 하나만 최종 단어에 나타난다.
- [x] 정답은 매일 바뀐다.
- [x] `words.txt` 안의 ((현재 날짜 - 2021년 6월 19일) % 배열의 크기) 번째의 단어이다.
- [ ] 정답을 맞췄을 경우 타일 표현 전에 몇 번째 만에 맞췄는지 출력한다.
Copy link

Choose a reason for hiding this comment

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

기능 요구 사항을 잘 정리해주셨는데 아직 [x] 업데이트가 아직 안된 것 같아요!


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

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

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

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

Choose a reason for hiding this comment

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

doGame보다 playGame은 어떠세요?ㅎㅎ 파랑은 어떻게 생각하시나요!?

Copy link
Author

Choose a reason for hiding this comment

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

play game이 더 자연스러운 것 같아서 변경했습니다👍

OutputView.printCount(game.count)
OutputView.printResults(game.results)
Copy link
Owner

Choose a reason for hiding this comment

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

View에 대한 호출을 최소화해 보면 어떨까요?

}

private fun doGame(game: Game) {
val answer = Word(InputView.inputAnswer())
game.match(answer)
if (game.isGameOver(answer)) {
return
}
OutputView.printResults(game.results)
doGame(game)
}
22 changes: 22 additions & 0 deletions src/main/kotlin/wordle/domain/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package wordle.domain

class Game(private val words: Words) {

constructor(words: List<Word>) : this(Words(words))

var count: Int = 0
private set

var results: MutableList<List<Tile>> = ArrayList()
Copy link
Owner

Choose a reason for hiding this comment

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

뒷받침하는 프로퍼티(backing property)를 사용해 보세요.

private set
Copy link

Choose a reason for hiding this comment

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

private set👍 👍 좋은방법같아요!

Copy link
Author

Choose a reason for hiding this comment

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

그때 제로가 질문해서 배웠습니다👍


fun isGameOver(answer: Word): Boolean {
return count == 6 || words.isCorrect(answer)
Copy link

Choose a reason for hiding this comment

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

OutputView에서 WORDLE을 6번 만에 맞춰 보세요.6과 도메인 Game의 6의 의존 값을 각각 가지고 있는 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

그렇네요 Game에서 public 상수로 선언해서 OutputView에서 가져다 쓰는 것으로 변경했습니다~

Copy link

Choose a reason for hiding this comment

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

깰끔하네여 ㅎ

}

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

YELLOW("🟨"), GREEN("🟩"), GRAY("⬜")
}
13 changes: 13 additions & 0 deletions src/main/kotlin/wordle/domain/Word.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wordle.domain

data class Word(val value: String) {

init {
require(value.length == 5) { "단어의 길이는 5글자여야 합니다." }
require(Regex("[a-zA-Z]*").matches(value)) { "단어에 영어가 아닌 글자나 공백이 포함될 수 없습니다." }
Copy link
Owner

Choose a reason for hiding this comment

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

String을 패턴 매칭을 위해 Regex으로 바꾸는 것은 꽤나 비싼 행위입니다. 미리 Regex으로 만들어 사용하는 것은 어떨까요? 아래의 글을 참고하세요.
https://stackoverflow.com/questions/2469244/whats-the-difference-between-string-matches-and-matcher-matches/2469275

Copy link
Author

Choose a reason for hiding this comment

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

Regex는 실행될 때마다 리컴파일되는 군요.. pattern을 상수로 만들어두고 pattern.matcher를 사용하는 것으로 변경했습니다!

}

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

Choose a reason for hiding this comment

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

깔끔 하네요ㅎㅎ 💯

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

import java.time.LocalDate
import java.time.temporal.ChronoUnit

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

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

Choose a reason for hiding this comment

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

HashMap을 사용한 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

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

문자를 비교해서 같은 경우 문자를 소진하기 때문에 문자의 개수를 저장할 필요가 있었습니다. 그렇기 때문에 문자 - 문자의 개수를 key, value로 저장하기 위해 HashMap을 사용했습니다! 이 부분이 궁금했던 거 맞나요?

Copy link

@asebn1 asebn1 May 19, 2022

Choose a reason for hiding this comment

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

이해했습니다!! 역시 S....

Copy link
Owner

Choose a reason for hiding this comment

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

코틀린은 읽기 전용 컬렉션과 변경 가능한 컬렉션을 구별해 제공합니다.

Suggested change
private var answerMap: MutableMap<Char, Int> = HashMap()
private var answerMap: MutableMap<Char, Int> = mutableMapOf()


private fun findAnswer(): Word {
val date: LocalDate = LocalDate.now()
val standardDate: LocalDate = LocalDate.of(2021, 6, 19)
val days: Int = ChronoUnit.DAYS.between(standardDate, date).toInt()
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()
val result: ArrayList<Tile> = ArrayList()
repeat(5) { result.add(Tile.GRAY) }

repeat(5) { i -> result[i] = findTileBySameCheck(word, i) }
Copy link

Choose a reason for hiding this comment

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

Suggested change
repeat(5) { i -> result[i] = findTileBySameCheck(word, i) }
repeat(5) { result[it] = findTileBySameCheck(word, it) }

이렇게도 될거같아요!

repeat(5) { i -> result[i] = findTileByContainCheck(result, word, i) }
return result
}

private fun initAnswerMap(): MutableMap<Char, Int> {
val answerMap: MutableMap<Char, Int> = HashMap()
answer.value.forEach {
answerMap.putIfAbsent(it, 0)
answerMap.computeIfPresent(it) { _, v -> v + 1 }
}
return answerMap
}

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

private fun findTileByContainCheck(result: List<Tile>, word: Word, index: Int): Tile {
if (result[index] == Tile.GREEN) {
return Tile.GREEN
}
if (answerMap.keys.contains(word.value[index])) {
calculateAnswerMap(word.value[index])
return Tile.YELLOW
}
return Tile.GRAY
}

private fun calculateAnswerMap(key: Char) {
answerMap.computeIfPresent(key) { _, v -> v - 1 }
if (answerMap[key] == 0) {
answerMap.remove(key)
}
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/wordle/util/WordsReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wordle.util

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

object WordsReader {

fun getWords(): List<Word> {
val path = "src/main/resources/words.txt"
val reader = FileReader(path)
return reader.readLines().map { Word(it) }
}
}
Copy link

Choose a reason for hiding this comment

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

WordsReader 좋네요!! 근데 해당 클래스가 Util 패키지에 속하는 것이 맞는지 궁금해요. 저도 잘 몰라서 질문드립니다 😄

Copy link
Author

Choose a reason for hiding this comment

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

txt 파일에서 문자열을 읽어와 Word list로 변환해주는 역할만 하고 있어서 util 클래스라고 생각했습니다!

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

import wordle.domain.Tile

object OutputView {

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

fun printResults(results: List<List<Tile>>) {
println()
results.forEach { printResult(it) }
println()
}

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

fun printCount(count: Int) {
println()
println("$count/6")
}
}
25 changes: 25 additions & 0 deletions src/test/kotlin/wordle/domain/GameTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package wordle.domain

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class GameTest {

@Test
fun `등록된 단어가 아닌 경우 예외가 발생한다`() {
val game = Game(listOf(Word("apple"), Word("hello"), Word("spicy")))

val exception = shouldThrow<IllegalArgumentException> {
game.match(Word("spell"))
}
exception.message shouldBe "등록된 단어가 아닙니다."
}

@Test
fun `게임 종료여부를 확인한다`() {
val game = Game(listOf(Word("apple"), Word("hello"), Word("spicy")))
repeat(6) { game.match(Word("apple")) }
Copy link
Owner

Choose a reason for hiding this comment

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

매번 이렇게 match()를 사용해야 할까요? 어떻게 하면 더 손쉬운 테스트를 할 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

Game을 생성할 때 maxGameCount를 받아 match를 사용하지 않고도 테스트할 수 있도록 수정했습니다!

game.isGameOver(Word("apple")) shouldBe true
}
}
35 changes: 35 additions & 0 deletions src/test/kotlin/wordle/domain/WordTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package wordle.domain

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.inspectors.forAll
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class WordTest {
Copy link

Choose a reason for hiding this comment

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

코테스트 배워갑니다. 👍 👍 👍 👍 👍 👍


@Test
fun `5글자가 아닌 word를 생성하면 예외가 발생한다`() {
val exception = shouldThrow<IllegalArgumentException> {
Word("test")
}
exception.message shouldBe "단어의 길이는 5글자여야 합니다."
}

@Test
fun `영어가 아닌 글자가 포함된 word를 생성하면 예외가 발생한다`() {
listOf("ap pl", "ap피le").forAll {
val exception = shouldThrow<IllegalArgumentException> {
Word(it)
}
exception.message shouldBe "단어에 영어가 아닌 글자나 공백이 포함될 수 없습니다."
}
}

@Test
fun `해당 글자가 단어의 특정 인덱스와 일치하는지 확인한다`() {
val word = Word("style")
val other = Word("soooo")
word.isSameChar(other, 0) shouldBe true
word.isSameChar(other, 1) shouldBe false
}
}
36 changes: 36 additions & 0 deletions src/test/kotlin/wordle/domain/WordsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package wordle.domain

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class WordsTest {

@Test
fun `단어가 리스트 안에 존재하는지 확인해준다`() {
val apple = Word("apple")
val happy = Word("happy")
val doggy = Word("doggy")
val words = Words(listOf(apple, happy))

words.contains(apple) shouldBe true
words.contains(doggy) shouldBe false
}

@Test
fun `단어가 정답인지 확인해준다`() {
val words = Words(listOf(Word("apple")))

words.isCorrect(Word("apple")) shouldBe true
words.isCorrect(Word("happy")) shouldBe false
}

@Test
fun `단어를 정답과 비교하여 결과 타일을 반환한다`() {
val words = Words(listOf(Word("apple")))
val word = Word("hello")

val checkList = words.check(word)

checkList shouldBe listOf(Tile.GRAY, Tile.YELLOW, Tile.GRAY, Tile.GREEN, Tile.GRAY)
}
}
13 changes: 13 additions & 0 deletions src/test/kotlin/wordle/util/WordsReaderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package wordle.util

import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class WordsReaderTest {

@Test
fun `파일을 읽어서 List를 반환해준다`() {
val words = WordsReader.getWords()
words.size shouldBe 2309
}
}