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

๐Ÿ’ฃ Step1: ์ง€๋ขฐ ์ฐพ๊ธฐ(๊ทธ๋ฆฌ๊ธฐ) #350

Open
wants to merge 14 commits into
base: sang5c
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
1eb42a3
docs: 1๋‹จ๊ณ„ ๊ธฐ๋ณธ ์š”๊ตฌ์‚ฌํ•ญ ์ž‘์„ฑ
sang5c Jul 25, 2023
148f896
feat: ์ž‘๋™ํ•˜๋Š” ์ฝ”๋“œ ๊ตฌํ˜„
sang5c Jul 25, 2023
1403e38
docs: ๊ธฐ๋Šฅ ๋ชฉ๋ก ๋„์ถœ
sang5c Jul 25, 2023
fb2ed74
feat(Position): x, y ์ขŒํ‘œ๋Š” 0 ์ด์ƒ์ด๋‹ค
sang5c Jul 25, 2023
ac05369
feat(Mark): ๋งˆํฌ๋Š” ์ง€๋ขฐ์™€ ์ผ๋ฐ˜ ์นธ์ด ์กด์žฌํ•œ๋‹ค
sang5c Jul 25, 2023
5cdd5c2
feat(RandomPositionGenerator): ์œ„์น˜ ์ƒ์„ฑ๊ธฐ๋Š” ์ตœ๋Œ€ x, y ์ขŒํ‘œ๋ฅผ ๋ฐ›์•„ ๋žœ๋คํ•œ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•œ๋‹ค
sang5c Jul 25, 2023
4012321
refactor: ํฌ์ง€์…˜ ์ƒ์„ฑ๊ธฐ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝ
sang5c Jul 25, 2023
c9f09e7
refactor: ๊ฒŒ์ž„ํŒ ์ƒ์„ฑ ๊ธฐ๋Šฅ Board ๋‚ด๋ถ€๋กœ ์ด๋™
sang5c Jul 25, 2023
9fb724a
refactor: lint ์ ์šฉ
sang5c Jul 25, 2023
7436ceb
refactor: BoardSize ํด๋ž˜์Šค ๋ถ„๋ฆฌ
sang5c Jul 25, 2023
66b0831
refactor: BoardMeta ํด๋ž˜์Šค ์ฑ…์ž„ ๋ถ„๋ฆฌ
sang5c Jul 25, 2023
dceff02
refactor(Board): ๊ฒŒ์ž„ํŒ ์ƒ์„ฑ ์‹œ์ ์— ํฌ์ง€์…˜ ์ƒ์„ฑ๊ธฐ ์‚ฌ์šฉ
sang5c Jul 25, 2023
c15473b
docs: ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๋ชฉ๋ก ์ž‘์„ฑ
sang5c Jul 25, 2023
3bbaf71
refactor: board ํŒจํ‚ค์ง€ ๋ถ„๋ฆฌ
sang5c Jul 25, 2023
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
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
# kotlin-minesweeper
# kotlin-minesweeper

## Step 1 - ์ง€๋ขฐ ์ฐพ๊ธฐ(๊ทธ๋ฆฌ๊ธฐ)

### ๊ธฐ๋Šฅ ๋ชฉ๋ก

- [X] ์ง€๋ขฐ์ฐพ๊ธฐ ๊ฒŒ์ž„ํŒ(Board)์„ ๊ทธ๋ฆฐ๋‹ค.
- [X] ๊ฒŒ์ž„ํŒ์€ ํฌ์ง€์…˜๊ณผ ๋งˆํฌ๋ฅผ ๊ฐ–๋Š”๋‹ค.
- [X] ๊ฒŒ์ž„ํŒ์€ 2์ฐจ์› ์นธ์„ ๊ฐ–๋Š”๋‹ค.
- [X] ๊ฒŒ์ž„ํŒ์€ ์ง€๋ขฐ ์œ„์น˜๋ฅผ ๋ฐ›์•„ ์ง€๋ขฐ๋ฅผ ๋ฐฐ์น˜ํ•œ๋‹ค.
- [X] ๊ฐ€๋กœ ์„ธ๋กœ ํฌ๊ธฐ๋Š” 1์ด์ƒ์ด๋‹ค.
- [X] ์ง€๋ขฐ ๊ฐœ์ˆ˜๋Š” 1์ด์ƒ ๊ฒŒ์ž„ํŒ ํฌ๊ธฐ(๊ฐ€๋กœ*์„ธ๋กœ) ์ดํ•˜์ด๋‹ค.
- [X] ์œ„์น˜ ์ƒ์„ฑ๊ธฐ๋Š” ์ตœ๋Œ€ x, y ์ขŒํ‘œ๋ฅผ ๋ฐ›์•„ ๋žœ๋คํ•œ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
- [X] ๋งˆํฌ๋Š” ์ง€๋ขฐ์™€ ์ผ๋ฐ˜ ์นธ์ด ์กด์žฌํ•œ๋‹ค
- [X] ํฌ์ง€์…˜์€ x, y ์ขŒํ‘œ๋ฅผ ๊ฐ–๋Š”๋‹ค.
- [X] x, y ์ขŒํ‘œ๋Š” 0์ด์ƒ์ด๋‹ค

### ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ

- ์ง€๋ขฐ ์ฐพ๊ธฐ๋ฅผ ๋ณ€ํ˜•ํ•œ ํ”„๋กœ๊ทธ๋žจ์„ ๊ตฌํ˜„ํ•œ๋‹ค.
- ๋†’์ด์™€ ๋„ˆ๋น„, ์ง€๋ขฐ ๊ฐœ์ˆ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
- ์ง€๋ขฐ๋Š” ๋ˆˆ์— ์ž˜ ๋„๋Š” ๊ฒƒ์œผ๋กœ ํ‘œ๊ธฐํ•œ๋‹ค.
- ์ง€๋ขฐ๋Š” ๊ฐ€๊ธ‰์  ๋žœ๋ค์— ๊ฐ€๊น๊ฒŒ ๋ฐฐ์น˜ํ•œ๋‹ค.

### ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์š”๊ตฌ์‚ฌํ•ญ

๊ฐ์ฒด์ง€ํ–ฅ ์ƒํ™œ ์ฒด์กฐ ์›์น™์„ ์ง€ํ‚ค๋ฉด์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐํ•œ๋‹ค.

- ํ•œ ๋ฉ”์„œ๋“œ์— ์˜ค์ง ํ•œ ๋‹จ๊ณ„์˜ ๋“ค์—ฌ์“ฐ๊ธฐ๋งŒ ํ•œ๋‹ค.
- else ์˜ˆ์•ฝ์–ด๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
- ๋ชจ๋“  ์›์‹œ ๊ฐ’๊ณผ ๋ฌธ์ž์—ด์„ ํฌ์žฅํ•œ๋‹ค.
- ํ•œ ์ค„์— ์ ์„ ํ•˜๋‚˜๋งŒ ์ฐ๋Š”๋‹ค.
- ์ค„์—ฌ ์“ฐ์ง€ ์•Š๋Š”๋‹ค(์ถ•์•ฝ ๊ธˆ์ง€).
- ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ž‘๊ฒŒ ์œ ์ง€ํ•œ๋‹ค.
- 3๊ฐœ ์ด์ƒ์˜ ์ธ์Šคํ„ด์Šค ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง„ ํด๋ž˜์Šค๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
- ์ผ๊ธ‰ ์ปฌ๋ ‰์…˜์„ ์“ด๋‹ค.
- getter/setter/ํ”„๋กœํผํ‹ฐ๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
Empty file removed src/main/kotlin/.gitkeep
Empty file.
21 changes: 21 additions & 0 deletions src/main/kotlin/minesweeper/domain/Minesweeper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package minesweeper.domain

import minesweeper.domain.board.Board
import minesweeper.domain.board.BoardMeta
import minesweeper.ui.InputView
import minesweeper.ui.OutputView

class Minesweeper(
private val input: InputView,
private val output: OutputView,
) {
fun start() {
val width = input.getWidth()
val height = input.getHeight()
val mineCount = input.getMineCount()

val boardMeta = BoardMeta(width, height, mineCount)
val board = Board.create(boardMeta)
output.printStart(board)
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/minesweeper/domain/board/Board.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package minesweeper.domain.board

import minesweeper.domain.field.Mark
import minesweeper.domain.field.Position
import minesweeper.domain.generator.PositionGenerator
import minesweeper.domain.generator.RandomPositionGenerator

class Board private constructor(val markMap: Map<Position, Mark>) {
companion object {
fun create(boardMeta: BoardMeta, positionGenerator: PositionGenerator = RandomPositionGenerator()): Board {
val minePositions = positionGenerator.get(boardMeta)
val baseBoard = generateAllPositions(boardMeta.boardSize)
.associateWith { Mark.SAFE }
.toMutableMap()
baseBoard.putAll(minePositions.associateWith { Mark.MINE })
return Board(baseBoard.toMap())
}

private fun generateAllPositions(boardSize: BoardSize): List<Position> {
return (0 until boardSize.width).flatMap { w ->
(0 until boardSize.height).map { h ->
Position(w, h)
}
}
}
}
}
15 changes: 15 additions & 0 deletions src/main/kotlin/minesweeper/domain/board/BoardMeta.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package minesweeper.domain.board

class BoardMeta(val boardSize: BoardSize, val mineCount: Int) {
val width: Int
get() = boardSize.width
val height: Int
get() = boardSize.height

constructor(width: Int, height: Int, mineCount: Int) : this(BoardSize(width, height), mineCount)

init {
require(mineCount > 0) { "์ง€๋ขฐ ๊ฐœ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
require(mineCount < width * height) { "์ง€๋ขฐ ๊ฐœ์ˆ˜๋Š” ๊ฐ€๋กœ * ์„ธ๋กœ ๋ณด๋‹ค ์ž‘์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/minesweeper/domain/board/BoardSize.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package minesweeper.domain.board

class BoardSize(val width: Int, val height: Int) {
init {
require(width > 0) { "๊ฐ€๋กœ ๊ธธ์ด๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
require(height > 0) { "์„ธ๋กœ ๊ธธ์ด๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/minesweeper/domain/field/Mark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package minesweeper.domain.field

enum class Mark {
MINE,
SAFE,
;

fun isMine(): Boolean {
return this == MINE
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/minesweeper/domain/field/Position.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package minesweeper.domain.field

data class Position(val x: Int, val y: Int) {
init {
require(x >= 0) { "x๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
require(y >= 0) { "y๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package minesweeper.domain.generator

import minesweeper.domain.board.BoardMeta
import minesweeper.domain.field.Position

fun interface PositionGenerator {
fun get(boardMeta: BoardMeta): Set<Position>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package minesweeper.domain.generator

import minesweeper.domain.board.BoardMeta
import minesweeper.domain.field.Position
import kotlin.random.Random

class RandomPositionGenerator : PositionGenerator {
override fun get(boardMeta: BoardMeta): Set<Position> {
return generateSequence { Position(Random.nextInt(0, boardMeta.width), Random.nextInt(0, boardMeta.height)) }
.distinct()
.take(boardMeta.mineCount)
.toSet()
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/minesweeper/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package minesweeper

import minesweeper.domain.Minesweeper
import minesweeper.ui.InputView
import minesweeper.ui.OutputView

fun main() {
val input = InputView()
val output = OutputView()

val minesweeper = Minesweeper(input, output)
minesweeper.start()
}
18 changes: 18 additions & 0 deletions src/main/kotlin/minesweeper/ui/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package minesweeper.ui

class InputView {
fun getWidth(): Int {
println("๋„ˆ๋น„๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.")
return readln().toInt()
}

fun getHeight(): Int {
println("๋†’์ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”.")
return readln().toInt()
}

fun getMineCount(): Int {
println("์ง€๋ขฐ๋Š” ๋ช‡ ๊ฐœ์ธ๊ฐ€์š”?")
return readln().toInt()
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/minesweeper/ui/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package minesweeper.ui

import minesweeper.domain.board.Board
import minesweeper.domain.field.Mark

class OutputView {
fun printStart(board: Board) {
println("์ง€๋ขฐ์ฐพ๊ธฐ ๊ฒŒ์ž„ ์‹œ์ž‘")
printBoard(board)
}

private fun printBoard(board: Board) {
groupByLine(board).forEach { (_, marks) ->
println(marks.joinToString(" ") { toSymbol(it) })
}
}

private fun groupByLine(board: Board): Map<Int, List<Mark>> {
return board.markMap.entries.groupBy({ it.key.x }, { it.value })
}

private fun toSymbol(mark: Mark): String {
if (mark.isMine()) {
return "*"
}
return "C"
}
}
Empty file removed src/test/kotlin/.gitkeep
Empty file.
20 changes: 20 additions & 0 deletions src/test/kotlin/minesweeper/domain/board/BoardMetaTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package minesweeper.domain.board

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

class BoardMetaTest {
@Test
fun `์ง€๋ขฐ ๊ฐœ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค`() {
shouldThrow<IllegalArgumentException> {
BoardMeta(1, 1, 0)
}
}

@Test
fun `์ง€๋ขฐ ๊ฐœ์ˆ˜๊ฐ€ ๊ฐ€๋กœ ์„ธ๋กœ์˜ ๊ณฑ๋ณด๋‹ค ํฌ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค`() {
shouldThrow<IllegalArgumentException> {
BoardMeta(1, 1, 2)
}
}
}
16 changes: 16 additions & 0 deletions src/test/kotlin/minesweeper/domain/board/BoardSizeTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package minesweeper.domain.board

import io.kotest.assertions.throwables.shouldThrow
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class BoardSizeTest {

@ParameterizedTest
@CsvSource("0, 1", "1, 0", "0, 0")
fun `๊ฒŒ์ž„ํŒ์˜ ๊ฐ€๋กœ ์„ธ๋กœ๋Š” 0๋ณด๋‹ค ์ปค์•ผํ•œ๋‹ค`(width: Int, height: Int) {
shouldThrow<IllegalArgumentException> {
BoardSize(width, height)
}
}
}
31 changes: 31 additions & 0 deletions src/test/kotlin/minesweeper/domain/board/BoardTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package minesweeper.domain.board

import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import minesweeper.domain.field.Mark
import minesweeper.domain.field.Position
import minesweeper.domain.generator.PositionGenerator
import org.junit.jupiter.api.Test

class BoardTest {

@Test
fun `์‚ฌ์ด์ฆˆ์™€ ์ง€๋ขฐ ์œ„์น˜๋ฅผ ๋ฐ›์•„ ๊ฒŒ์ž„ํŒ์„ ์ƒ์„ฑํ•œ๋‹ค`() {
val boardMeta = BoardMeta(3, 3, 3)
val positions = listOf(Position(1, 1), Position(2, 2))

val board = Board.create(boardMeta, getTestGenerator(positions))

assertSoftly {
board.markMap.size shouldBe boardMeta.width * boardMeta.height
board.markMap[Position(1, 1)] shouldBe Mark.MINE
board.markMap[Position(2, 2)] shouldBe Mark.MINE
}
}

private fun getTestGenerator(positions: List<Position>): PositionGenerator {
return PositionGenerator {
positions.toSet()
}
}
}
14 changes: 14 additions & 0 deletions src/test/kotlin/minesweeper/domain/field/MarkTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package minesweeper.domain.field

import io.kotest.matchers.shouldBe
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class MarkTest {

@ParameterizedTest
@CsvSource("MINE, true", "SAFE, false")
fun `์ง€๋ขฐ์ธ์ง€ ํ™•์ธํ•œ๋‹ค`(mark: Mark, expected: Boolean) {
mark.isMine() shouldBe expected
}
}
15 changes: 15 additions & 0 deletions src/test/kotlin/minesweeper/domain/field/PositionTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package minesweeper.domain.field

import io.kotest.assertions.throwables.shouldThrow
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class PositionTest {
@ParameterizedTest
@CsvSource("-1, 0", "0, -1", "-1, -1")
fun `x ๋˜๋Š” y๊ฐ€ 0 ๋ฏธ๋งŒ์ด๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค`(x: Int, y: Int) {
shouldThrow<IllegalArgumentException> {
Position(x, y)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package minesweeper.domain.generator

import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import minesweeper.domain.board.BoardMeta
import org.junit.jupiter.api.Test

class RandomPositionGeneratorTest {

@Test
fun `๋žœ๋ค ํฌ์ง€์…˜ ์ƒ์„ฑ๊ธฐ๋Š” 0๋ถ€ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๊นŒ์ง€์˜ ๊ฐ’์„ ๊ฐ–๋Š” ํฌ์ง€์…˜์„ ์ƒ์„ฑํ•œ๋‹ค`() {
val boardMeta = BoardMeta(3, 3, 3)
val generator = RandomPositionGenerator()

val positions = generator.get(boardMeta)

assertSoftly {
positions.size shouldBe boardMeta.mineCount
positions.all {
it.x in 0 until boardMeta.width && it.y in 0 until boardMeta.height
} shouldBe true
}
}
}