diff --git a/README.md b/README.md index b4b1377..da277b7 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,33 @@ spill ๐ŸŸฉ๐ŸŸฉโฌœ๐ŸŸฉ๐ŸŸฉ ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ๐ŸŸฉ ``` + +## ์š”๊ตฌ์‚ฌํ•ญ +- [X] ํ•˜๋‚˜์˜ Tile์ด ์žˆ๋‹ค. => (Tile) + - [X] Tile์—๋Š” ๋ฌธ์ž๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. + - [X] ์•ŒํŒŒ๋ฒณ๋งŒ ์ž…๋ ฅ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. + - [X] Tile๊ณผ Tile์€ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋‹ค. +- [X] ์—ฌ๋Ÿฌ๊ฐœ์˜ Tile์„ ๋ฌถ๋Š”๋‹ค. => (Tiles) (์ž…๋ ฅ๋˜์–ด์„œ ์ƒ์„ฑ๋œ Tiles) + - [X] Tiles๋Š” 5๊ธ€์ž๋ฅผ ๋ฐ›๋Š”๋‹ค. + - [X] Tiles๋Š” 5๊ฐœ์˜ Tile๋กœ ๊ตฌ์„ฑ์ด ๋˜์–ด์žˆ๋‹ค. +- [X] ์ •๋‹ต์ด ์žˆ๋‹ค. => (Answer) + - [X] Answer๋Š” 5๊ธ€์ž๋ฅผ ๋ฐ›๋Š”๋‹ค. + - [X] Answer๋Š” 5๊ฐœ์˜ Tile๋กœ ๊ตฌ์„ฑ์ด ๋˜์–ด์žˆ๋‹ค. + - [X] ์ •๋‹ต๊ณผ ๋น„๊ตํ•˜์—ฌ ๊ฐ™์€ ์œ„์น˜์— ์žˆ์œผ๋ฉด GREEN์ด๋‹ค + - [X] ์ •๋‹ต์—๋Š” ์žˆ์ง€๋งŒ ์œ„์น˜๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋…ธ๋ž€์ƒ‰ + - [X] ์•„์˜ˆ ์—†์œผ๋ฉด ํšŒ์ƒ‰ + - [X] ์ •๋‹ต์— ์žˆ๋Š” ํƒ€์ผ์€ n๊ฐœ์ด์ง€๋งŒ, ์ž…๋ ฅํ•œ ํƒ€์ผ์€ n + 1๊ฐœ ์ด์ƒ์ธ ๊ฒฝ์šฐ ์ดˆ๋ก -> ๋…ธ๋ž€์ƒ‰ -> ํšŒ์ƒ‰์ˆœ์œผ๋กœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„๋‹ค +- [X] ๊ฒŒ์ž„์ด ์žˆ๋‹ค => (Game) + - [X] ๊ธฐํšŒ๋Š” ์ด 6๋ฒˆ์„ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค. + - [X] ์ •๋‹ต์„ ์ ์œผ๋ฉด ๊ฒŒ์ž„์ด ๋๋‚œ๋‹ค. + - [X] ์ •๋‹ต๊ณผ ๋‹ต์•ˆ์€ ์ •๋‹ต ์ €์žฅ์†Œ์— ์กด์žฌํ•˜๋Š” ๋‹จ์–ด์—ฌ์•ผ ํ•œ๋‹ค. + - [X] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹จ์–ด์ด๋ฉด ์žฌ์ž…๋ ฅ์„ ํ•œ๋‹ค. + - [X] ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค. + +### PRESENTATION +- [X] DefaultOutput + - [X] ์ƒˆ๋กœ์šด ๋ฌธ์ž๋ฅผ ์ ์œผ๋ฉด ๊ณผ๊ฑฐ์— ์žˆ๋˜ ๊ฒฐ๊ณผ๋„ ๊ฐ™์ด ํ‘œ์ถœ๋œ๋‹ค. + - [X] ์ง€๊ธˆ๊นŒ์ง€ ์ž…๋ ฅํ•œ Tiles๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋‹ค. +- [X] DefaultWordsRepository + - [X] ์ •๋‹ต๊ณผ ๋‹ต์•ˆ์€ `words.txt`์— ์กด์žฌํ•˜๋Š” ๋‹จ์–ด์—ฌ์•ผ ํ•œ๋‹ค. + - [X] ์ •๋‹ต์€ ๋งค์ผ ๋ฐ”๋€Œ๋ฉฐ ((ํ˜„์žฌ ๋‚ ์งœ - 2021๋…„ 6์›” 19์ผ) % ๋ฐฐ์—ด์˜ ํฌ๊ธฐ) ๋ฒˆ์งธ์˜ ๋‹จ์–ด์ด๋‹ค. \ No newline at end of file diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt new file mode 100644 index 0000000..d4c0def --- /dev/null +++ b/src/main/kotlin/Application.kt @@ -0,0 +1,7 @@ +import controller.WordleController +import presentation.DefaultInput +import presentation.DefaultOutput + +fun main() { + WordleController(DefaultInput, DefaultOutput).start() +} diff --git a/src/main/kotlin/controller/WordleController.kt b/src/main/kotlin/controller/WordleController.kt new file mode 100644 index 0000000..43e25ef --- /dev/null +++ b/src/main/kotlin/controller/WordleController.kt @@ -0,0 +1,32 @@ +package controller + +import domain.Game +import domain.Input +import domain.MatchResults +import domain.Output +import infra.WordsPool + +class WordleController( + private val input: Input, + private val output: Output +) { + + fun start() { + val game = Game(WordsPool()) + do { + var result: MatchResults + + try { + result = game.progress(input) + } catch (e: IllegalArgumentException) { + println(e.message) + continue + } + output.write(result) + + if (result.isCorrect()) { + break + } + } while (!game.checkToRetry()) + } +} diff --git a/src/main/kotlin/domain/Answer.kt b/src/main/kotlin/domain/Answer.kt new file mode 100644 index 0000000..0fe558b --- /dev/null +++ b/src/main/kotlin/domain/Answer.kt @@ -0,0 +1,75 @@ +package domain + +import domain.MatchResult.INCORRECT +import domain.MatchResult.MISSING + +class Answer(val tiles: Tiles) { + init { + require(tiles.size == REQUIRE_TILE_SIZE) { ERROR_TILE_SIZE_MSG } + } + + fun match(other: Tiles): MatchResults { + val result: Array = arrayOf(INCORRECT, INCORRECT, INCORRECT, INCORRECT, INCORRECT) + val countOfTile: MutableMap = tiles.groupingBy { it }.eachCount().toMutableMap() + + fillGreens(result, other, countOfTile) + fillYellows(result, other, countOfTile) + + return MatchResults(result.toList()) + } + + private fun fillGreens( + result: Array, + other: Tiles, + countOfTile: MutableMap + ) { + other.mapIndexed { index, tile -> + fillGreen(tile, index, result, countOfTile) + } + } + private fun fillYellows( + result: Array, + other: Tiles, + countOfTile: MutableMap + ) { + other.mapIndexed { index, tile -> + fillYellow(tile, index, result, countOfTile) + } + } + + private fun fillGreen( + tile: Tile, + index: Int, + result: Array, + countOfTile: MutableMap + ) { + if (this.tiles.equals(tile, index)) { + result[index] = MatchResult.CORRECT + + countOfTile[tile] = countOfTile[tile]!!.dec() + } + } + + private fun fillYellow( + tile: Tile, + index: Int, + result: Array, + countOfTile: MutableMap + ) { + val count = countOfTile[tile] ?: EMPTY + + if (result[index] == INCORRECT && count > EMPTY) { + result[index] = MISSING + + countOfTile[tile] = count.dec() + } + } + + companion object { + const val ERROR_TILE_SIZE_MSG = "ํƒ€์ผ์€ 5๊ฐœ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + const val REQUIRE_TILE_SIZE = 5 + const val EMPTY = 0 + + fun of(words: String): Answer = Answer(Tiles.of(words)) + } +} diff --git a/src/main/kotlin/domain/Game.kt b/src/main/kotlin/domain/Game.kt new file mode 100644 index 0000000..23f9a00 --- /dev/null +++ b/src/main/kotlin/domain/Game.kt @@ -0,0 +1,26 @@ +package domain + +class Game(private val words: Words) { + private val answer: Answer = Answer(words.getTodayWords()) + private var tryCount = 0 + + fun progress(input: Input): MatchResults { + val tiles = input.read() + checkWord(tiles) + tryCount++ + return answer.match(tiles) + } + + fun checkToRetry() = MAX_TRY_COUNT == tryCount + + private fun checkWord(tiles: Tiles) { + if (!words.exists(tiles)) { + throw IllegalArgumentException(NOT_FOUND_WORD) + } + } + + companion object { + private const val MAX_TRY_COUNT = 6 + private const val NOT_FOUND_WORD = "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹จ์–ด์ž…๋‹ˆ๋‹ค" + } +} diff --git a/src/main/kotlin/domain/Input.kt b/src/main/kotlin/domain/Input.kt new file mode 100644 index 0000000..8b5c03b --- /dev/null +++ b/src/main/kotlin/domain/Input.kt @@ -0,0 +1,5 @@ +package domain + +fun interface Input { + fun read(): Tiles +} diff --git a/src/main/kotlin/domain/MatchResult.kt b/src/main/kotlin/domain/MatchResult.kt new file mode 100644 index 0000000..3328fd8 --- /dev/null +++ b/src/main/kotlin/domain/MatchResult.kt @@ -0,0 +1,7 @@ +package domain + +enum class MatchResult { + CORRECT, + MISSING, + INCORRECT +} diff --git a/src/main/kotlin/domain/MatchResults.kt b/src/main/kotlin/domain/MatchResults.kt new file mode 100644 index 0000000..45d0470 --- /dev/null +++ b/src/main/kotlin/domain/MatchResults.kt @@ -0,0 +1,14 @@ +package domain + +data class MatchResults(val results: List) { + init { + require(results.size == REQUIRE_RESULTS_SIZE) { ERROR_RESULTS_SIZE_MSG } + } + + fun isCorrect(): Boolean = results.all { it == MatchResult.CORRECT } + + companion object { + const val ERROR_RESULTS_SIZE_MSG = "๊ฒฐ๊ณผ๋Š” 5๊ฐœ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + const val REQUIRE_RESULTS_SIZE = 5 + } +} diff --git a/src/main/kotlin/domain/Output.kt b/src/main/kotlin/domain/Output.kt new file mode 100644 index 0000000..5a86732 --- /dev/null +++ b/src/main/kotlin/domain/Output.kt @@ -0,0 +1,5 @@ +package domain + +fun interface Output { + fun write(matchResults: MatchResults) +} diff --git a/src/main/kotlin/domain/Tile.kt b/src/main/kotlin/domain/Tile.kt new file mode 100644 index 0000000..4d60863 --- /dev/null +++ b/src/main/kotlin/domain/Tile.kt @@ -0,0 +1,13 @@ +package domain + +@JvmInline +value class Tile(val character: Char) { + init { + require(character in ALLOWED_CHARACTER) { ERROR_ALLOWED_CHARACTER_MSG } + } + + companion object { + const val ERROR_ALLOWED_CHARACTER_MSG = "๋ฌธ์ž๋Š” ์•ŒํŒŒ๋ฒณ๋งŒ ์ž…๋ ฅ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค." + val ALLOWED_CHARACTER = 'a'..'z' + } +} diff --git a/src/main/kotlin/domain/Tiles.kt b/src/main/kotlin/domain/Tiles.kt new file mode 100644 index 0000000..d2a2cbe --- /dev/null +++ b/src/main/kotlin/domain/Tiles.kt @@ -0,0 +1,18 @@ +package domain + +data class Tiles(val tiles: List) : List by tiles { + init { + require(tiles.size == REQUIRE_TILE_SIZE) { ERROR_TILE_SIZE_MSG } + } + + fun equals(tile: Tile, index: Int): Boolean = this.tiles[index] == tile + + fun countOf(tile: Tile): Int = this.tiles.count { it == tile } + + companion object { + const val ERROR_TILE_SIZE_MSG = "ํƒ€์ผ์€ 5๊ฐœ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + const val REQUIRE_TILE_SIZE = 5 + + fun of(words: String): Tiles = Tiles(words.map { Tile(it) }) + } +} diff --git a/src/main/kotlin/domain/Words.kt b/src/main/kotlin/domain/Words.kt new file mode 100644 index 0000000..c5a429f --- /dev/null +++ b/src/main/kotlin/domain/Words.kt @@ -0,0 +1,7 @@ +package domain + +interface Words { + fun exists(tiles: Tiles): Boolean + + fun getTodayWords(): Tiles +} diff --git a/src/main/kotlin/infra/WordsPool.kt b/src/main/kotlin/infra/WordsPool.kt new file mode 100644 index 0000000..df91e33 --- /dev/null +++ b/src/main/kotlin/infra/WordsPool.kt @@ -0,0 +1,31 @@ +package infra + +import domain.Tiles +import domain.Words +import java.io.File +import java.time.LocalDate + +class WordsPool : Words { + + private val today: Tiles + + init { + val nowEpochDay = LocalDate.now().toEpochDay() + val daysFromStandard = (nowEpochDay - BASE_TIME_EPOCH_DAY).toInt() + this.today = WORDS[daysFromStandard % WORDS.size] + } + + override fun exists(tiles: Tiles): Boolean { + return WORDS.contains(tiles) + } + + override fun getTodayWords(): Tiles { + return today + } + + companion object { + private val BASE_TIME_EPOCH_DAY = LocalDate.of(2021, 6, 19).toEpochDay() + private val WORDS: List = + File(ClassLoader.getSystemResource("words.txt").file).bufferedReader().readLines().map { Tiles.of(it) } + } +} diff --git a/src/main/kotlin/presentation/DefaultInput.kt b/src/main/kotlin/presentation/DefaultInput.kt new file mode 100644 index 0000000..ffa63ca --- /dev/null +++ b/src/main/kotlin/presentation/DefaultInput.kt @@ -0,0 +1,14 @@ +package presentation + +import domain.Input +import domain.Tiles + +object DefaultInput : Input { + private const val INPUT_MESSAGE = "์ •๋‹ต์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”." + + override fun read(): Tiles { + println(INPUT_MESSAGE) + val text = readln() + return Tiles.of(text) + } +} diff --git a/src/main/kotlin/presentation/DefaultOutput.kt b/src/main/kotlin/presentation/DefaultOutput.kt new file mode 100644 index 0000000..44880e1 --- /dev/null +++ b/src/main/kotlin/presentation/DefaultOutput.kt @@ -0,0 +1,42 @@ +package presentation + +import domain.MatchResult +import domain.MatchResults +import domain.Output + +object DefaultOutput : Output { + private const val BLANK = "" + private const val MAX_TRY_COUNT = 6 + + private val allResults: MutableList = mutableListOf() + + override fun write(matchResults: MatchResults) { + allResults.add(matchResults) + + if (matchResults.isCorrect()) { + println("${allResults.size}/$MAX_TRY_COUNT") + } + + printAll() + } + + private fun printAll() { + allResults.forEach { + printLine(it) + } + } + + private fun printLine(matchResults: MatchResults) { + println( + matchResults.results.joinToString(BLANK) { + blockOf(it) + } + ) + } + + private fun blockOf(matchResult: MatchResult): String = when (matchResult) { + MatchResult.CORRECT -> "\uD83D\uDFE9" + MatchResult.MISSING -> "\uD83D\uDFE8" + else -> "โฌœ" + } +} diff --git a/src/test/kotlin/domain/AnswerTest.kt b/src/test/kotlin/domain/AnswerTest.kt new file mode 100644 index 0000000..237d30e --- /dev/null +++ b/src/test/kotlin/domain/AnswerTest.kt @@ -0,0 +1,82 @@ +package domain + +import domain.MatchResult.CORRECT +import domain.MatchResult.INCORRECT +import domain.MatchResult.MISSING +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.data.forAll +import io.kotest.data.headers +import io.kotest.data.row +import io.kotest.data.table +import io.kotest.matchers.collections.shouldContain +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe + +internal class AnswerTest : ShouldSpec({ + context("Answer๋Š”") { + val answer = Answer.of("hello") + + context("5๊ฐœ Tile ๋กœ") { + val tiles = Tiles.of("hello") + should("๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค") { + answer.tiles shouldBe tiles + } + } + + context("5๊ฐœ๊ฐ€ ์•„๋‹Œ Tile ์€") { + should("์‹คํŒจํ•œ๋‹ค") { + forAll( + table( + headers("word"), + row(""), + row("h"), + row("hell"), + row("hellow"), + ) + ) { word -> + shouldThrow { Answer.of(word) } + } + } + } + + context("๋‹ต์•ˆ๊ณผ ๋น„๊ตํ•˜์—ฌ") { + + should("๊ฐ™์€ ์œ„์น˜์— ์žˆ์œผ๋ฉด CORRECT์ด๋‹ค") { + val tiles = Tiles.of("hello") + val matches = answer.match(tiles) + matches.results shouldContainExactly listOf(CORRECT, CORRECT, CORRECT, CORRECT, CORRECT) + } + + should("๋‹ค๋ฅธ ์œ„์น˜์— ์žˆ์œผ๋ฉด MISSING์ด๋‹ค") { + val tiles = Tiles.of("olehl") + val matches = answer.match(tiles) + matches.results shouldContain MISSING + } + + should("์—†์„ ๊ฒฝ์šฐ INCORRECT์ด๋‹ค") { + val tiles = Tiles.of("zzzzz") + val matches = answer.match(tiles) + matches.results shouldContain INCORRECT + } + + should("๋งž๋Š” ๋ถ€๋ถ„์„ CORRECT๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ๋‚˜๋จธ์ง€ ์กด์žฌํ•˜๋Š” ๋ถ€๋ถ„์„ MISSING์œผ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค ") { + val tiles = Tiles.of("olleh") + val matches = answer.match(tiles) + matches.results shouldContainExactly listOf(MISSING, MISSING, CORRECT, MISSING, MISSING) + } + + should("์ •๋‹ต์ด ๋’ค์— ์žˆ์œผ๋ฉด ์ •๋‹ต๋ถ€ํ„ฐ CORRECT์œผ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค") { + val tiles = Tiles.of("lllll") + val matches = answer.match(tiles) + matches.results shouldContainExactly listOf(INCORRECT, INCORRECT, CORRECT, CORRECT, INCORRECT) + } + + should("๋‹ต์•ˆ์— ์žˆ๋Š” ํƒ€์ผ์ด ์ •๋‹ต ๊ฐœ์ˆ˜๋ณด๋‹ค ๋งŽ์œผ๋ฉด ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์„ ์ œ์™ธํ•œ ๋ถ€๋ถ„์ด INCORRECT์œผ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค") { + val tiles = Tiles.of("llool") + val matches = answer.match(tiles) + matches.results shouldContainExactly listOf(MISSING, MISSING, MISSING, INCORRECT, INCORRECT) + } + } + } +}) diff --git a/src/test/kotlin/domain/GameTest.kt b/src/test/kotlin/domain/GameTest.kt new file mode 100644 index 0000000..bb57242 --- /dev/null +++ b/src/test/kotlin/domain/GameTest.kt @@ -0,0 +1,76 @@ +package domain + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.shouldBe + +class GameTest : BehaviorSpec({ + val answer = Tiles.of("hello") + + Given("ํ•˜๋‚˜์˜ ๋‹ต์•ˆ์—") { + val input = TestInput("hello") + val repository = TestWordsRepository(answer, setOf(Tiles(answer))) + val game = Game(repository) + + When("์ •๋‹ต์ด ์žˆ์„ ๊ฒฝ์šฐ") { + val result = game.progress(input) + + then("๊ฒŒ์ž„์ด ์ข…๋ฃŒ ๋œ๋‹ค") { + result.isCorrect() shouldBe true + } + } + } + + Given("๋‹ต์•ˆ์„ 7๋ฒˆ ์ด์ƒ") { + val input = listOf("abcde", "abcde", "abcde", "abcde", "abcde", "abcde", "abcde").map { TestInput(it) } + val repository = TestWordsRepository(answer, setOf(Tiles(answer), Tiles.of("abcde"))) + val game = Game(repository) + + When("์ ์œผ๋ฉด") { + input.forEach { + game.progress(it) + } + + Then("๋”์ด์ƒ ์‹œ๋„ํ•  ์ˆ˜ ์—†๋‹ค") { + game.checkToRetry() shouldBe false + } + } + } + + Given("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋‹จ์–ด๋Š”") { + val input = listOf("abcde", "abcde", "abcde", "abcde", "abcde", "abcde", "abcde").map { TestInput(it) } + val repository = TestWordsRepository(answer, setOf(Tiles(answer))) + val game = Game(repository) + + Then("์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค") { + input.forAll { + shouldThrow { + game.progress(it) + } + } + } + } +}) + +class TestInput(private val word: String) : Input { + override fun read(): Tiles { + return Tiles.of(word) + } +} + +class TestWordsRepository(val answer: Tiles, val words: Set) : Words { + var tilesStack = mutableListOf() + var calledTodayWords = false + + override fun exists(tiles: Tiles): Boolean { + tilesStack.add(tiles) + + return words.contains(tiles) + } + + override fun getTodayWords(): Tiles { + calledTodayWords = true + return answer + } +} diff --git a/src/test/kotlin/domain/MatchResultsTest.kt b/src/test/kotlin/domain/MatchResultsTest.kt new file mode 100644 index 0000000..11e1dae --- /dev/null +++ b/src/test/kotlin/domain/MatchResultsTest.kt @@ -0,0 +1,52 @@ +package domain + +import domain.MatchResult.CORRECT +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import org.junit.jupiter.params.provider.ValueSource + +internal class MatchResultsTest { + @Test + fun `5๊ฐœ์˜ ๊ฒฐ๊ณผ๋กœ ์ด๋ค„์ง„๋‹ค`() { + // given + val matchResult = listOf(CORRECT, CORRECT, CORRECT, CORRECT, CORRECT) + val result = MatchResults(matchResult) + + // then + assertThat(result.results).containsExactlyElementsOf(matchResult) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 6, 7, 8]) + fun `5๊ฐœ์˜ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹ˆ๋ฉด ์‹คํŒจํ•œ๋‹ค`(size: Int) { + // given + val matchResult = (0 until size).map { CORRECT } + + // then + assertThatIllegalArgumentException().isThrownBy { + MatchResults(matchResult) + }.withMessage(MatchResults.ERROR_RESULTS_SIZE_MSG) + } + + @Test + fun `๋ชจ๋“  MatchResult๊ฐ€ ์„ฑ๊ณต์ด๋ฉด MatchResults๋„ ์„ฑ๊ณต์ด๋‹ค`() { + // given + val result = MatchResults(listOf(CORRECT, CORRECT, CORRECT, CORRECT, CORRECT)) + + // then + assertThat(result.isCorrect()).isTrue + } + + @ParameterizedTest + @EnumSource(value = MatchResult::class, names = ["MISSING", "INCORRECT"]) + fun `๋ชจ๋“  MatchResult๊ฐ€ ์„ฑ๊ณต์ด ์•„๋‹ˆ๋ฉด MatchResults๋Š” ์„ฑ๊ณต์ด ์•„๋‹ˆ๋‹ค`(result: MatchResult) { + // given + val result = MatchResults(listOf(CORRECT, CORRECT, CORRECT, CORRECT, result)) + + // then + assertThat(result.isCorrect()).isFalse + } +} diff --git a/src/test/kotlin/domain/TileTest.kt b/src/test/kotlin/domain/TileTest.kt new file mode 100644 index 0000000..115a536 --- /dev/null +++ b/src/test/kotlin/domain/TileTest.kt @@ -0,0 +1,46 @@ +package domain + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource + +internal class TileTest { + @Test + fun `ํƒ€์ผ์€ ํ•˜๋‚˜์˜ ๋ฌธ์ž๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค`() { + // given + val tile = Tile('a') + + // then + assertThat(tile.character).isEqualTo('a') + } + + @ParameterizedTest + @ValueSource(chars = [' ', '0', '1', '5', '9', 'ใ„ฑ', 'โฌœ']) + fun `ํƒ€์ผ์€ ์•ŒํŒŒ๋ฒณ๋งŒ ์ž…๋ ฅ์ด ๊ฐ€๋Šฅํ•˜๋‹ค`(character: Char) { + assertThatIllegalArgumentException() + .isThrownBy { Tile(character) } + .withMessage(Tile.ERROR_ALLOWED_CHARACTER_MSG) + } + + @Test + fun `๋‘๊ฐœ์˜ Tile์ด ์„œ๋กœ ์ผ์น˜ํ•˜๋ฉด ์ฐธ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() { + // given + val tile = Tile('a') + val anotherTile = Tile('a') + + // then + assertThat(tile).isEqualTo(anotherTile) + } + + @Test + fun `๋‘๊ฐœ์˜ Tile์ด ์„œ๋กœ ๋‹ค๋ฅด๋ฉด ๊ฑฐ์ง“์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค`() { + // given + val tile = Tile('a') + val anotherTile = Tile('b') + + // then + assertThat(tile).isNotEqualTo(anotherTile) + } +} diff --git a/src/test/kotlin/domain/TilesTest.kt b/src/test/kotlin/domain/TilesTest.kt new file mode 100644 index 0000000..4dffc79 --- /dev/null +++ b/src/test/kotlin/domain/TilesTest.kt @@ -0,0 +1,80 @@ +package domain + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatIllegalArgumentException +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource + +internal class TilesTest { + @Test + fun `Tiles๋Š” 5๊ฐœ์˜ Tile๋กœ ๊ตฌ์„ฑ์ด ๋˜์–ด์žˆ๋‹ค`() { + // given + val elements = listOf(Tile('a'), Tile('b'), Tile('c'), Tile('d'), Tile('e')) + + // when + val tiles = Tiles(elements) + + // then + assertThat(tiles.tiles).containsExactlyElementsOf(elements) + } + + @ParameterizedTest + @ValueSource(ints = [0, 1, 2, 3, 4, 6, 10]) + fun `Tiles๋Š” 5๊ฐœ์˜ Tile๋กœ ๊ตฌ์„ฑ์ด ์•ˆ๋˜๋ฉด ์‹คํŒจํ•œ๋‹ค`(size: Int) { + // given + val elements: List = (0 until size).map { Tile('a') } + + // then + assertThatIllegalArgumentException() + .isThrownBy { Tiles(elements) } + .withMessage(Tiles.ERROR_TILE_SIZE_MSG) + } + + @Test + fun `Tiles๋Š” ๋ฌธ์ž์—ด 5๊ธ€์ž๋กœ ๊ตฌ์„ฑ ํ•  ์ˆ˜ ์žˆ๋‹ค`() { + // given + val tiles = Tiles.of("hello") + + // then + assertThat(tiles.tiles) + .containsExactly(Tile('h'), Tile('e'), Tile('l'), Tile('l'), Tile('o')) + } + + @ParameterizedTest + @ValueSource(strings = ["", "h", "hell", "hellow"]) + fun `Tiles๋Š” ๋ฌธ์ž์—ด 5๊ธ€์ž๋กœ ๊ตฌ์„ฑ์ด ์•ˆ๋˜๋ฉด ์‹คํŒจํ•œ๋‹ค`(words: String) { + // then + assertThatIllegalArgumentException() + .isThrownBy { Tiles.of(words) } + .withMessage(Tiles.ERROR_TILE_SIZE_MSG) + } + + @ParameterizedTest + @CsvSource( + value = [ + "h,0,true", "e,1,true", "l,2,true", "l,3,true", "o,4,true", // ์„ฑ๊ณต์ผ€์ด์Šค + "a,0,false", "b,1,false", "c,2,false", "d,3,false", "e,4,false" // ์‹คํŒจ์ผ€์ด์Šค + ] + ) + fun `Tiles๋Š” ์œ„์น˜์™€ Tile์„ ๋ฐ›์•„์„œ ํ•ด๋‹น ์œ„์น˜์— ๊ฐ™์€ ํƒ€์ผ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค`(tile: Char, index: Int, result: Boolean) { + // given + val tile = Tile(tile) + val tiles = Tiles.of("hello") + + // then + assertThat(tiles.equals(tile, index)).isEqualTo(result) + } + + @ParameterizedTest + @CsvSource(value = ["h,1", "e,1", "l,2", "o,1", "z,0"]) + fun `Tiles์— Tile์ด ๋ช‡๊ฐœ ์žˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค`(tile: Char, count: Int) { + // given + val tiles = Tiles.of("hello") + val tile = Tile(tile) + + // then + assertThat(tiles.countOf(tile)).isEqualTo(count) + } +}