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

Step2 #420

Open
wants to merge 3 commits into
base: wjdtlr0920
Choose a base branch
from
Open

Step2 #420

Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
+ 코드리뷰 요청 2단계 [[동영상]](https://www.youtube.com/watch?v=HnTdFJd0PtU) [[문서]](https://github.com/next-step/nextstep-docs/blob/master/codereview/review-step2.md)
+ 코드리뷰 요청 3단계 [[동영상]](https://www.youtube.com/watch?v=fzrT3eoecUw) [[문서]](https://github.com/next-step/nextstep-docs/blob/master/codereview/review-step3.md)

### 문자열 덧셈 계산기
새로운 언어를 배워 써먹을 만큼 숙련도를 높이려면 많이 노력해야 한다. 코틀린을 처음 배웠는데 정확한 코틀린 문법이 기억나지 않는 경우 유용하게 써먹을 수 있다.

#### 기능 요구 사항
지뢰 찾기를 변형한 프로그램을 구현한다.
- 높이와 너비, 지뢰 개수를 입력받을 수 있다.
Expand All @@ -39,3 +36,29 @@
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 일급 컬렉션을 쓴다.
- getter/setter/프로퍼티를 쓰지 않는다.


## STEP2 (2단계 - 지뢰 찾기(지뢰 개수))

#### 기능 요구 사항
지뢰 찾기를 변형한 프로그램을 구현한다.
- 높이와 너비, 지뢰 개수를 입력받을 수 있다.
- 지뢰는 눈에 잘 띄는 것으로 표기한다.
- 지뢰는 가급적 랜덤에 가깝게 배치한다.
- 각 사각형에 표시될 숫자는 자신을 제외한 주변 8개 사각형에 포함된 지뢰의 개수다.

#### 기능 목록
- [x] 높이는 최소 2이상, 최대 1000이하 여야 한다.
- [x] 넓이는 최소 2이상, 최대 1000이하 여야 한다.
- [x] 지뢰의 수는 1개 이상이여야 한다.
- [x] 보드의 모든 셀이 지뢰여서는 안된다.

#### 프로그래밍 요구 사항
- 객체 지향 5원칙을 지키면서 프로그래밍한다.
- 객체지향 5원칙(SOLID)

- SRP (단일책임의 원칙: Single Responsibility Principle): 작성된 클래스는 하나의 기능만 가지며 클래스가 제공하는 모든 서비스는 그 하나의 책임(변화의 축: axis of change)을 수행하는 데 집중되어 있어야 한다
- OCP (개방폐쇄의 원칙: Open Close Principle): 소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
- LSP (리스코브 치환의 원칙: The Liskov Substitution Principle): 서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다. 즉, 서브 타입은 언제나 기반 타입과 호환될 수 있어야 한다.
- ISP (인터페이스 분리의 원칙: Interface Segregation Principle): 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
- DIP (의존성역전의 원칙: Dependency Inversion Principle): 구조적 디자인에서 발생하던 하위 레벨 모듈의 변경이 상위 레벨 모듈의 변경을 요구하는 위계관계를 끊는 의미의 역전 원칙이다.
9 changes: 8 additions & 1 deletion src/main/kotlin/minesweeper/controller/Controller.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package minesweeper.controller

import minesweeper.domain.model.Board
import minesweeper.domain.model.Height
import minesweeper.domain.model.MineCount
import minesweeper.domain.model.Width
import minesweeper.view.InputView
import minesweeper.view.ResultView

object Controller {
fun start() {
val board = InputView.getBoard()
val height = Height.from(InputView.getHeight())
val width = Width.from(InputView.getWidth())
val mineCount = MineCount.from(InputView.getMineCount())
val board = Board.create(width, height, mineCount)
ResultView.drawBoard(board)
}
}
25 changes: 24 additions & 1 deletion src/main/kotlin/minesweeper/domain/model/Board.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package minesweeper.domain.model

import minesweeper.domain.model.cell.Cell
import minesweeper.domain.model.cell.CellState

class Board private constructor(
private val cells: List<List<Cell>>
) : List<List<Cell>> by cells {
Expand All @@ -14,11 +17,31 @@ class Board private constructor(

companion object {
fun create(width: Width, height: Height, mineCount: MineCount): Board {
val cells = List(width.toInt() * height.toInt()) { index -> Cell.create(index < mineCount.toInt()) }
val mineCellStates = List(mineCount.toInt()) { CellState.MINE }
val noneCellStates = List((width.toInt() * height.toInt()) - mineCount.toInt()) { CellState.NONE }
val cells = (mineCellStates + noneCellStates)
.shuffled()
.chunked(width.toInt())
.toCellList()

return Board(cells)
}
}
}

private fun List<List<CellState>>.toCellList(): List<List<Cell>> {
return this.mapIndexed { rowIndex, rowCells ->
rowCells.mapIndexed { columnIndex, cellStatus ->
var countOfMinesNearby = 0
if (rowIndex != 0 && columnIndex != 0 && this[rowIndex - 1][columnIndex - 1].isMine()) countOfMinesNearby++
if (rowIndex != 0 && this[rowIndex - 1][columnIndex].isMine()) countOfMinesNearby++
if (rowIndex != 0 && columnIndex != lastIndex && this[rowIndex - 1][columnIndex + 1].isMine()) countOfMinesNearby++
if (columnIndex != 0 && this[rowIndex][columnIndex - 1].isMine()) countOfMinesNearby++
if (columnIndex != lastIndex && this[rowIndex][columnIndex + 1].isMine()) countOfMinesNearby++
if (rowIndex != lastIndex && columnIndex != 0 && this[rowIndex + 1][columnIndex - 1].isMine()) countOfMinesNearby++
if (rowIndex != lastIndex && this[rowIndex + 1][columnIndex].isMine()) countOfMinesNearby++
if (rowIndex != lastIndex && columnIndex != lastIndex && this[rowIndex + 1][columnIndex + 1].isMine()) countOfMinesNearby++
Cell.create(cellStatus, countOfMinesNearby)
}
}
}
14 changes: 0 additions & 14 deletions src/main/kotlin/minesweeper/domain/model/Cell.kt

This file was deleted.

2 changes: 1 addition & 1 deletion src/main/kotlin/minesweeper/domain/model/Height.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ value class Height private constructor(private val value: Int) {
fun toInt(): Int = value

companion object {
private const val MIN_HEIGHT = 1
private const val MIN_HEIGHT = 2
private const val MAX_HEIGHT = 1000

fun from(height: Int): Height = Height(height)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/minesweeper/domain/model/Width.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ value class Width private constructor(private val value: Int) {
fun toInt(): Int = value

companion object {
private const val MIN_WIDTH = 1
private const val MIN_WIDTH = 2
private const val MAX_WIDTH = 1000

fun from(width: Int): Width = Width(width)
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/minesweeper/domain/model/cell/Cell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package minesweeper.domain.model.cell

class Cell(private val cellState: CellState, private val countOfMinesNearby: Int) {

fun isMine(): Boolean = cellState == CellState.MINE

fun getCountOfMinesNearby(): Int = countOfMinesNearby

companion object {
fun create(cellState: CellState, countOfMinesNearby: Int): Cell {
return Cell(cellState, countOfMinesNearby)
}
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/minesweeper/domain/model/cell/CellState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package minesweeper.domain.model.cell

enum class CellState {
NONE,
MINE;

fun isMine(): Boolean = this == MINE
}
24 changes: 6 additions & 18 deletions src/main/kotlin/minesweeper/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,8 @@
package minesweeper.view

import minesweeper.domain.model.Board
import minesweeper.domain.model.Height
import minesweeper.domain.model.MineCount
import minesweeper.domain.model.Width

object InputView {

fun getBoard(): Board {
val height = getHeight()
val width = getWidth()
val mineCount = getMineCount()
return Board.create(width, height, mineCount)
}

private fun getHeight(): Height {
fun getHeight(): Int {
println("높이를 입력하세요.")
val height = readln()
println()
Expand All @@ -23,10 +11,10 @@ object InputView {
"높이는 숫자여야 합니다."
}

return Height.from(height.toInt())
return height.toInt()
}

private fun getWidth(): Width {
fun getWidth(): Int {
println("넓이를 입력하세요.")
val width = readln()
println()
Expand All @@ -35,10 +23,10 @@ object InputView {
"넓이는 숫자여야 합니다."
}

return Width.from(width.toInt())
return width.toInt()
}

private fun getMineCount(): MineCount {
fun getMineCount(): Int {
println("지뢰는 몇 개인가요?")
val mineCount = readln()
println()
Expand All @@ -47,6 +35,6 @@ object InputView {
"지뢰 수는 숫자여야 합니다."
}

return MineCount.from(mineCount.toInt())
return mineCount.toInt()
}
}
8 changes: 4 additions & 4 deletions src/main/kotlin/minesweeper/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package minesweeper.view

import minesweeper.domain.model.Board
import minesweeper.domain.model.Cell
import minesweeper.domain.model.cell.Cell

object ResultView {
fun drawBoard(board: Board) {
board.forEach { cells ->
drawCells(cells)
board.forEach { rowCells ->
drawCells(rowCells)
println()
}
}
Expand All @@ -20,6 +20,6 @@ object ResultView {
print("*")
return
}
print("C")
print("${cell.getCountOfMinesNearby()}")
}
}
17 changes: 10 additions & 7 deletions src/test/kotlin/minesweeper/domain/model/BoardTest.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package minesweeper.domain.model

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.core.spec.style.BehaviorSpec


fun Board(width: Int, height: Int, mineCount: Int): Board {
private fun Board(width: Int, height: Int, mineCount: Int): Board {
return Board.create(Width.from(width), Height.from(height), MineCount.from(mineCount))
}
class BoardTest : StringSpec({
class BoardTest : BehaviorSpec({

"보드의 모든 셀이 지뢰여서는 안된다." {
shouldThrow<IllegalArgumentException> {
Board(10, 10, 100)
given("보드가 주어지고") {
`when`("높이가 10, 넓이가 10, 지뢰수가 100이면") {
then("보드의 모든 셀이 지뢰여서 IllegalArgumentException 예외가 발생해야 한다.") {
shouldThrow<IllegalArgumentException> {
Board(10, 10, 100)
}
}
}
}
})
50 changes: 37 additions & 13 deletions src/test/kotlin/minesweeper/domain/model/HeightTest.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
package minesweeper.domain.model

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.core.spec.style.BehaviorSpec

class HeightTest : StringSpec({
"높이는 최소 2이상, 최대 1000이하 여야 한다" {
shouldThrow<IllegalArgumentException> {
Height.from(0)
class HeightTest : BehaviorSpec({

given("최소 2이상, 최대 1000이하를 가질 수 있는 높이가 주어지고") {
`when`("높이에 2를 입력하면") {
then("2를 가진 높이가 생성되어야 한다.") {
Height.from(2)
}
}
shouldThrow<IllegalArgumentException> {
Height.from(1)
`when`("높이에 500를 입력하면") {
then("500를 가진 높이가 생성되어야 한다.") {
Height.from(500)
}
}
shouldThrow<IllegalArgumentException> {
Height.from(1001)
`when`("높이에 1000를 입력하면") {
then("1000를 가진 높이가 생성되어야 한다.") {
Height.from(1000)
}
}

Height.from(2).toInt() shouldBe 2
Height.from(500).toInt() shouldBe 500
Height.from(1000).toInt() shouldBe 1000
`when`("높이에 0을 입력하면") {
then("높이의 최소값 보다 낮아 IllegalArgumentException 예외가 발생해야 한다.") {
shouldThrow<IllegalArgumentException> {
Height.from(0)
}
}
}
`when`("높이에 1을 입력하면") {
then("높이의 최소값 보다 낮아 IllegalArgumentException 예외가 발생해야 한다.") {
shouldThrow<IllegalArgumentException> {
Height.from(1)
}
}
}
`when`("높이에 1001을 입력하면") {
then("높이의 최대값 보다 높아 IllegalArgumentException 예외가 발생해야 한다.") {
shouldThrow<IllegalArgumentException> {
Height.from(1001)
}
}
}
}
})
39 changes: 30 additions & 9 deletions src/test/kotlin/minesweeper/domain/model/MineCountTest.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
package minesweeper.domain.model

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe

class MineCountTest : StringSpec({
"지뢰의 수는 1개 이상 이여야 한다." {
shouldThrow<IllegalArgumentException> {
MineCount.from(0)
class MineCountTest : BehaviorSpec({

given("최소 1이상을 가질 수 있는 지뢰 수가 주어지고") {
`when`("지뢰 수에 1을 입력하면") {
then("1을 가진 지뢰 수가 생성되어야 한다.") {
MineCount.from(1).toInt() shouldBe 1
}
}
`when`("지뢰 수에 10을 입력하면") {
then("10을 가진 지뢰 수가 생성되어야 한다.") {
MineCount.from(10).toInt() shouldBe 10
}
}
`when`("지뢰 수에 100을 입력하면") {
then("100을 가진 지뢰 수가 생성되어야 한다.") {
MineCount.from(100).toInt() shouldBe 100
}
}
`when`("지뢰 수에 1000을 입력하면") {
then("1000을 가진 지뢰 수가 생성되어야 한다.") {
MineCount.from(1000).toInt() shouldBe 1000
}
}

MineCount.from(1).toInt() shouldBe 1
MineCount.from(10).toInt() shouldBe 10
MineCount.from(100).toInt() shouldBe 100
MineCount.from(1000).toInt() shouldBe 1000
`when`("지뢰 수에 0을 입력하면") {
then("최소 지뢰수 보다 낮아 IllegalArgumentException 예외가 발생해야 한다.") {
shouldThrow<IllegalArgumentException> {
MineCount.from(0)
}
}
}
}
})
Loading