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] 제로(공재호) 미션 제출합니다. #7

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3208e76
docs: 기능 목록 작성
dongho108 May 11, 2022
a6bfd74
feat: 단어 생성 기능 추가
dongho108 May 11, 2022
5ecc453
feat: 정답 단어를 반환하는 기능 추가
dongho108 May 11, 2022
ae9220f
feat: 정답매칭기능 구현
dongho108 May 11, 2022
693fd1d
feat: 최대 6번까지 단어를 매칭하능 기능 구현
dongho108 May 11, 2022
977e755
feat: 플레이어의 입력으로 게임이 진행되도록 구현
dongho108 May 11, 2022
2bf267f
refactor: 변수명 수정
dongho108 May 11, 2022
4a32fdc
style: 변수 위치 수정
dongho108 May 11, 2022
6a91799
refactor: round를 game으로부터 받도록 수정
dongho108 May 11, 2022
960f8a4
style: kotlin 컨벤션에 맞게 수정
dongho108 May 11, 2022
3523863
refactor: 생성자 파라미터로 받도록 수정
asebn1 May 15, 2022
18b51a2
refactor: WordsResponse 네이밍 변경
asebn1 May 15, 2022
0faee3c
feat: findAnswer 사이즈 나누기 추가
asebn1 May 15, 2022
518fdc5
feat: answer을 파라미터로 받아 매치하도록 변경
asebn1 May 15, 2022
38377e6
fix: ktlintCheck에 통과하도록 코드컨밴션 적용
asebn1 May 15, 2022
535438d
feat: DSL 기능구현 및 테스트 추가
asebn1 May 18, 2022
dc4168c
refactor: count, isOver 타입 변경
asebn1 May 19, 2022
96402e7
refactor: DSL 패키지 위치 변경
asebn1 May 19, 2022
c07aeb2
refactor: sealed class 적용
asebn1 May 22, 2022
a815c59
refactor: 패턴 매치 수정
asebn1 May 22, 2022
857a983
refactor: data 클래스 제거
asebn1 May 30, 2022
4ad4f5f
refactor: 게임 결과 클래스 변경
asebn1 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@

---

🛠 기능 목록
- [x] 정답 매칭
- [x] 판별 결과는 타일로 알려준다.
- [x] 정답은 `words.txt`의 ((현재 날짜 - 2021년 6월 19일) % `words.txt`의 단어 개수) 번째의 단어이다.
- [x] 매칭 결과
- [x] 플레이어 답과 정답 사이 한 글자의 스펠과 위치가 같은 경우 `초록색 타일`로 나타낸다.
- [x] 플레이어 답과 정답 사이 한 글자의 스펠은 같지만 위치가 다른 경우 `노란색 타일`로 나타낸다.
- [x] 플레이어 답과 정답 사이 한 글자의 스펠과 위치가 모두 다른 경우 `회색 타일`로 나타낸다.
- [x] 타일
- [x] 초록색, 노란색, 회색
- [x] 단어
- [x] 단어는 5글자이다.
- [x] 단어는 영단어이다.
- [x] 게임
- [x] 최대 6번까지 답안을 제출할 수 있다.
- [x] 정답은 `words.txt`의 ((현재 날짜 - 2021년 6월 19일) % `words.txt`의 단어 개수) 번째의 단어이다.
- [x] 단어는 `words.txt`에 포함된 단어여야 한다.
- [x] 답안을 6번 제출하면 게임을 종료한다.
- [x] 6번 안에 정답을 맞추면 종료한다.
- [x] 플레이어는 답을 입력한다.
- [x] 답을 입력할때마다 매칭된 누적 타일을 반환한다.
- [x] 게임이 종료되면 시도횟수를 반환한다.

## 🚀 기능 요구 사항

선풍적인 인기를 끌었던 영어 단어 맞추기 게임이다.
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/study/Introduce.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package study

import study.person.Person
import study.person.PersonBuilder

fun introduce(builder: PersonBuilder.() -> Unit): Person {
return PersonBuilder().apply(builder).build()
}
3 changes: 3 additions & 0 deletions src/main/kotlin/study/language/Language.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package study.language

data class Language(val name: String, val level: Int)
14 changes: 14 additions & 0 deletions src/main/kotlin/study/language/LanguageBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package study.language

class LanguageBuilder {

var languages: MutableList<Language> = mutableListOf()

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

fun build(): MutableList<Language> {
return languages
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/study/person/Person.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package study.person

import study.language.Language
import study.skill.Skill

data class Person(
val name: String,
val company: String,
val skills: MutableList<Skill>,
val languages: MutableList<Language>
)
34 changes: 34 additions & 0 deletions src/main/kotlin/study/person/PersonBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package study.person

import study.language.Language
import study.language.LanguageBuilder
import study.skill.Skill
import study.skill.SkillBuilder

class PersonBuilder {

lateinit var name: String
lateinit var company: String
lateinit var skills: MutableList<Skill>
lateinit var languages: MutableList<Language>

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

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

fun skills(builder: SkillBuilder.() -> Unit) {
skills = SkillBuilder().apply(builder).build()
}

fun languages(builder: LanguageBuilder.() -> Unit) {
languages = LanguageBuilder().apply(builder).build()
}

fun build(): Person {
return Person(name, company, skills, languages)
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/study/skill/Skill.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package study.skill

sealed class Skill

data class SoftSkill(val value: String) : Skill()

data class HardSkill(val value: String) : Skill()
18 changes: 18 additions & 0 deletions src/main/kotlin/study/skill/SkillBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package study.skill

class SkillBuilder {

var skills: MutableList<Skill> = mutableListOf()

fun soft(value: String) {
skills.add(SoftSkill(value))
}

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

fun build(): MutableList<Skill> {
return skills
}
}
35 changes: 35 additions & 0 deletions src/main/kotlin/wordle/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package wordle

import wordle.domain.Game
import wordle.domain.GameResult
import wordle.domain.Word
import wordle.domain.Words
import wordle.domain.WordsReader
import wordle.view.InputView
import wordle.view.OutputView
import java.time.LocalDate

fun main() {

val wordsReader = WordsReader("words.txt")
val words = Words(wordsReader.words)
val game = Game(words, LocalDate.now())
val gameResult = GameResult(game.maxRound)

OutputView.printInitMessage(game.maxRound)
play(game, gameResult)
}

private fun play(game: Game, gameResult: GameResult) {

Choose a reason for hiding this comment

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

play 라는 책임이 도메인의 책임처럼 느껴져서 저는 Game 객체가 가지도록 했는데 제로는 어떻게 생각하시나요? 😀

Choose a reason for hiding this comment

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

수업 때 잠깐 제로랑 얘기했던 부분인데
역시 책임을 어떻게 할당할지에 대한 고민은 항상 어려운 것 같네요 😂

Copy link
Author

Choose a reason for hiding this comment

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

그쵸 ㅠㅠ 역할과 책임 분리가 가장 어려운것같아요

return try {
while (!game.isOver) {
val playerWord = Word(InputView.requestAnswer())
val matchResult = game.matchResult(playerWord)
gameResult.add(matchResult)
OutputView.printGameResult(game.isOver, game.count, game.maxRound, gameResult)
}
} catch (exception: IllegalArgumentException) {
OutputView.printErrorMessage(exception.message)
play(game, gameResult)
}
}
46 changes: 46 additions & 0 deletions src/main/kotlin/wordle/domain/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package wordle.domain

import java.time.LocalDate

class Game(val words: Words, val date: LocalDate) {

var count: Int = 0
private set
var isOver: Boolean = false
private set
val maxRound: Int
get() = MAX_ROUND

fun matchResult(playerWord: Word): Tiles {
require(words.contains(playerWord)) { "[ERROR] words.txt에 있는 단어를 입력해주세요." }
val answer = words.findAnswer(date)
val tiles = playerWord.value.withIndex()
.mapIndexedNotNull { index, value ->
matchSpell(value.value, index, answer)
}
count++
val newTiles = Tiles(tiles)
updateIsOver(newTiles)
return newTiles
}

private fun updateIsOver(newTiles: Tiles) {
if (count >= MAX_ROUND || newTiles.isAllGreen()) {
isOver = true
}
}

private fun matchSpell(spell: Char, index: Int, answer: Word): Tile {
if (answer.sameIndexAndSpell(index, spell)) {
return Tile.GREEN
}
if (answer.contains(spell)) {
return Tile.YELLOW
}
return Tile.GRAY
}

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

class GameResult(size: Int) {

var gameResult: MutableList<Tiles> = ArrayList(size)
private set

fun add(tiles: Tiles) {
this.gameResult.add(tiles)
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/wordle/domain/Tile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package wordle.domain

enum class Tile(val symbol: String) {

GRAY("⬜"),
GREEN("\uD83D\uDFE9"),
YELLOW("\uD83D\uDFE8");

fun isGreen(): Boolean {
return this == GREEN
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/wordle/domain/Tiles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package wordle.domain

data class Tiles(val tiles: List<Tile>) {

fun isAllGreen(): Boolean {
return tiles.all { it.isGreen() }
}
}
23 changes: 23 additions & 0 deletions src/main/kotlin/wordle/domain/Word.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package wordle.domain

import java.util.regex.Pattern

data class Word(val value: String) {

init {
require(isRightSize()) { "[ERROR] 5글자의 단어를 입력하세요." }
require(isAlphabet()) { "[ERROR] 영어 단어를 입력하세요." }
}

private fun isRightSize(): Boolean = value.length == SIZE

private fun isAlphabet(): Boolean = Pattern.matches("^[a-zA-Z]*$", value)

fun sameIndexAndSpell(index: Int, spell: Char): Boolean = value[index] == spell

fun contains(spell: Char): Boolean = value.contains(spell)

companion object {
private const val SIZE = 5
Copy link

@jojogreen91 jojogreen91 May 13, 2022

Choose a reason for hiding this comment

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

저도 이걸 클래스 안에서 companion object 로 설정할지 고민하다가
저 같은 경우는 이 클래스 파일에서 전역변수로 설정했는데
제로는 어떤게 더 적절하다고 생각하시나요?

...
const val WORD_SIZE = 5

class Words {...}

Copy link
Author

Choose a reason for hiding this comment

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

음... companion object의 경우 싱글턴 이며 클래스당 1개만 구현할 수 있기때문에 명시적으로 이 곳에서 관리할 것이다라는 것을 알 수 있을 것 같아요.
두 가지의 큰 차이점은 모르겠어요. 공부해봐야겠습니다!

Copy link

@jojogreen91 jojogreen91 May 18, 2022

Choose a reason for hiding this comment

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

저도 companion object 의 정체가 뭘까 좀 공부를 해봐야겠어요. 현재는 뭔가 자바의 static 처럼 쓰고 있는데 그게 맞나라는 생각도 드네요 ㅋㅋ
저도 아직 다 읽지는 않았는데 도움이 될까 싶어서 찾은 글 하나 링크로 남겨놓겠습니다 :)
https://www.bsidesoft.com/8187

Copy link
Author

Choose a reason for hiding this comment

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

해당 링크 접속이 안되네요! ㅠ_ㅠ
저도 찾아봤는데 공유해봅니다!
https://stackoverflow.com/questions/49969319/kotlin-difference-between-constant-in-companion-object-and-top-level

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

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

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

fun findAnswer(current: LocalDate): Word {
val answerIndex = ChronoUnit.DAYS.between(BASE_DATE, current)
Copy link

@jojogreen91 jojogreen91 May 13, 2022

Choose a reason for hiding this comment

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

이 부분 도움 많이 받았습니다 👍

그런데 이부분 % words.txt의 단어 개수 연산도 해줘야 하는거 아닌가요?
혹시 제가 못본걸까요😲

Copy link
Author

Choose a reason for hiding this comment

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

맞아요ㅠ 연산 추가해주어야해요! 왜인지 잊어버렸군요 ㅎㅎㅎ,,

return words[answerIndex.toInt() % words.size]
}

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

companion object {
private val BASE_DATE = LocalDate.of(2021, 6, 19)
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/wordle/domain/WordsReader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package wordle.domain

import java.io.File

class WordsReader(private val path: String) {

val words: List<Word>
get() = getWords(path)

companion object {
private fun getWords(path: String): List<Word> {
return File(ClassLoader.getSystemResource(path).file).readLines()
.map { Word(it) }
}
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/wordle/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package wordle.view

object InputView {

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

import wordle.domain.GameResult
import wordle.domain.Tiles

object OutputView {

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

fun printGameResult(isOver: Boolean, count: Int, maxRound: Int, gameResult: GameResult) {
if (isOver) {
println("$count/$maxRound")
}
println()
gameResult.gameResult
.forEach {
printTiles(it)
println()
}
}

private fun printTiles(tiles: Tiles) {
tiles.tiles
.forEach { print(it.symbol) }
}

fun printErrorMessage(message: String?) {
println(message)
}
}
40 changes: 40 additions & 0 deletions src/test/kotlin/study/Introduce.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package study

import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
import study.language.Language
import study.skill.HardSkill
import study.skill.SoftSkill

class Introduce {

@Test
fun `자기소개`() {
val person = introduce {
name("제로")
company("우아한테크코스")
skills {
soft("A passion for problem solving")
soft("common communication skills")
hard("Kotlin")
}
languages {
"Korean" level 2
"English" level 1
}
}

person.name shouldBe "제로"
person.company shouldBe "우아한테크코스"
person.skills shouldContainExactly listOf(
SoftSkill("A passion for problem solving"),
SoftSkill("common communication skills"),
HardSkill("Kotlin")
)
person.languages shouldContainExactly listOf(
Language("Korean", 2),
Language("English", 1)
)
}
}
Loading