From fe3ce072e8e586c40aa0dd26bc3676f8e588565f Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 14:15:15 +0900 Subject: [PATCH 01/18] =?UTF-8?q?docs:=20=EC=98=A4=EB=AA=A9=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/README.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..0ca7357fd --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +## 오목 + +### Domain +- [ ] 흑돌이 먼저 시작한다. +- [ ] 오목알은 자신의 위치를 알고 있다. + - [ ] `x, y` 위치는 `1부터 15`로 제한된다. +- [ ] 오목판의 크기는 `15 x 15`이다. +- [ ] 사용자는 오목알을 놓는다. +- [ ] 사용자는 게임에서 이겼는지 확인한다. +- [ ] 사용자는 특정 위치에 돌이 있는지 확인한다. +- [ ] 사용자는 마지막 돌의 위치를 알고 있다. +- [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. + +### Input +- [ ] 오목알을 놓을 위치를 입력받는다. + +### Output +- [ ] 오목알의 위치를 입력받기 전에 오목판을 출력한다. From ca4182c3b08411dd22029aa6097378fbd5d5edf9 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 15:06:35 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=201=EB=B6=80=ED=84=B0=2015=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EB=A5=BC=20=EA=B0=80=EC=A7=84=20=EC=98=A4?= =?UTF-8?q?=EB=AA=A9=EC=95=8C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 5 +++-- src/main/kotlin/Position.kt | 13 +++++++++++++ src/main/kotlin/Stone.kt | 8 ++++++++ src/test/kotlin/PositionTest.kt | 29 +++++++++++++++++++++++++++++ src/test/kotlin/StoneTest.kt | 10 ++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/Position.kt create mode 100644 src/main/kotlin/Stone.kt create mode 100644 src/test/kotlin/PositionTest.kt create mode 100644 src/test/kotlin/StoneTest.kt diff --git a/docs/README.md b/docs/README.md index 0ca7357fd..82dee0e34 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,8 +2,9 @@ ### Domain - [ ] 흑돌이 먼저 시작한다. -- [ ] 오목알은 자신의 위치를 알고 있다. - - [ ] `x, y` 위치는 `1부터 15`로 제한된다. +- [x] 오목알은 자신의 위치를 알고 있다. + - [x] `x, y` 위치는 `1부터 15`로 제한된다. +- [ ] 중복되는 위치의 오목알을 가질 수 없다. - [ ] 오목판의 크기는 `15 x 15`이다. - [ ] 사용자는 오목알을 놓는다. - [ ] 사용자는 게임에서 이겼는지 확인한다. diff --git a/src/main/kotlin/Position.kt b/src/main/kotlin/Position.kt new file mode 100644 index 000000000..404df88a8 --- /dev/null +++ b/src/main/kotlin/Position.kt @@ -0,0 +1,13 @@ +data class Position(val x: Int, val y: Int) { + init { + require(x in MIN_BOUND..MAX_BOUND) { X_OUT_OF_RANGE_ERROR_MESSAGE } + require(y in MIN_BOUND..MAX_BOUND) { Y_OUT_OF_RANGE_ERROR_MESSAGE } + } + + companion object { + const val MIN_BOUND = 1 + const val MAX_BOUND = 15 + private const val X_OUT_OF_RANGE_ERROR_MESSAGE = "x의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" + private const val Y_OUT_OF_RANGE_ERROR_MESSAGE = "y의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" + } +} diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/Stone.kt new file mode 100644 index 000000000..271553e15 --- /dev/null +++ b/src/main/kotlin/Stone.kt @@ -0,0 +1,8 @@ +class Stone private constructor(val position: Position) { + companion object { + private val POSITION_RANGE = (Position.MIN_BOUND..Position.MAX_BOUND) + private val STONES = POSITION_RANGE.map { x -> POSITION_RANGE.map { y -> Stone(Position(x, y)) } } + + fun of(x: Int, y: Int) = STONES[x][y] + } +} diff --git a/src/test/kotlin/PositionTest.kt b/src/test/kotlin/PositionTest.kt new file mode 100644 index 000000000..5b890a283 --- /dev/null +++ b/src/test/kotlin/PositionTest.kt @@ -0,0 +1,29 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import org.junit.jupiter.api.assertThrows + +class PositionTest { + @Test + fun `오목알의 위치는 범위가 1부터 15인 x, y를 가지고 있다`() { + val position = Position(10, 10) + assertAll({ + assertThat(position.x).isEqualTo(10) + assertThat(position.y).isEqualTo(10) + }) + } + + @Test + fun `x의 범위가 1부터 15 사이가 아니면 에러가 발생한다`() { + assertThrows { + Position(16, 10) + } + } + + @Test + fun `y의 범위가 1부터 15 사이가 아니면 에러가 발생한다`() { + assertThrows { + Position(10, 16) + } + } +} diff --git a/src/test/kotlin/StoneTest.kt b/src/test/kotlin/StoneTest.kt new file mode 100644 index 000000000..6a52f944f --- /dev/null +++ b/src/test/kotlin/StoneTest.kt @@ -0,0 +1,10 @@ + + +class StoneTest { + // @Test + // fun `오목알은 자신의 위치를 알고 있다`() { + // val stone = Stone("H", 10) + // assertThat(stone.x).isEqualTo("H") + // assertThat(stone.y).isEqualTo(10) + // } +} From e13e7da5ff389ad0011497116aeac7a5b3cccf87 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 15:09:08 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EC=98=A4?= =?UTF-8?q?=EB=AA=A9=EC=95=8C=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=EB=AA=A9=EC=95=8C=20=EC=9D=BC=EA=B8=89=20?= =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 2 +- src/main/kotlin/Stones.kt | 11 +++++++++++ src/test/kotlin/StonesTest.kt | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/Stones.kt create mode 100644 src/test/kotlin/StonesTest.kt diff --git a/docs/README.md b/docs/README.md index 82dee0e34..c2435bdd3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ - [ ] 흑돌이 먼저 시작한다. - [x] 오목알은 자신의 위치를 알고 있다. - [x] `x, y` 위치는 `1부터 15`로 제한된다. -- [ ] 중복되는 위치의 오목알을 가질 수 없다. +- [x] 중복되는 위치의 오목알을 가질 수 없다. - [ ] 오목판의 크기는 `15 x 15`이다. - [ ] 사용자는 오목알을 놓는다. - [ ] 사용자는 게임에서 이겼는지 확인한다. diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/Stones.kt new file mode 100644 index 000000000..b4c64fd22 --- /dev/null +++ b/src/main/kotlin/Stones.kt @@ -0,0 +1,11 @@ +class Stones(private val stones: List) { + constructor(vararg stones: Stone) : this(stones.toList()) + + init { + require(stones.distinct().size == stones.size) { DUPLICATED_ERROR_MESSAGE } + } + + companion object { + private const val DUPLICATED_ERROR_MESSAGE = "중복되는 위치의 오목알을 가질 수 없습니다." + } +} diff --git a/src/test/kotlin/StonesTest.kt b/src/test/kotlin/StonesTest.kt new file mode 100644 index 000000000..f2017a383 --- /dev/null +++ b/src/test/kotlin/StonesTest.kt @@ -0,0 +1,19 @@ +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class StonesTest { + @Test + fun `각각 다른 위치의 오목알을 가질 수 있다`() { + assertDoesNotThrow { + Stones(Stone.of(10, 10), Stone.of(11, 11)) + } + } + + @Test + fun `중복되는 위치의 오목알을 가지면 에러가 발생한다`() { + assertThrows { + Stones(Stone.of(10, 10), Stone.of(10, 10)) + } + } +} From fa5e2b9413f496a286a0d0e2b9ce469db528d23b Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 15:10:45 +0900 Subject: [PATCH 04/18] =?UTF-8?q?docs:=20.gitkeep=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/.gitkeep | 0 src/test/kotlin/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/kotlin/.gitkeep delete mode 100644 src/test/kotlin/.gitkeep diff --git a/src/main/kotlin/.gitkeep b/src/main/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/test/kotlin/.gitkeep b/src/test/kotlin/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 0c398fdd97e9108b15581d6d2b55050ed1352980 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 17:36:16 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=AA=A9=EC=95=8C?= =?UTF-8?q?=EC=9D=84=20=EB=86=93=EC=9D=80=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EA=B0=80=20=EA=B2=8C=EC=9E=84=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EA=B2=BC=EB=8A=94=EC=A7=80=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 5 +++-- src/main/kotlin/Position.kt | 9 ++++---- src/main/kotlin/Stone.kt | 5 +++-- src/main/kotlin/Stones.kt | 42 ++++++++++++++++++++++++++++++++++- src/test/kotlin/Fixtures.kt | 10 +++++++++ src/test/kotlin/StonesTest.kt | 37 ++++++++++++++++++++++++++++-- 6 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 src/test/kotlin/Fixtures.kt diff --git a/docs/README.md b/docs/README.md index c2435bdd3..40d6b666a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,8 +7,9 @@ - [x] 중복되는 위치의 오목알을 가질 수 없다. - [ ] 오목판의 크기는 `15 x 15`이다. - [ ] 사용자는 오목알을 놓는다. -- [ ] 사용자는 게임에서 이겼는지 확인한다. -- [ ] 사용자는 특정 위치에 돌이 있는지 확인한다. +- [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. +- [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. +- [ ] 특정 위치에 돌을 놓을 수 있는지 판단한다. - [ ] 사용자는 마지막 돌의 위치를 알고 있다. - [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. diff --git a/src/main/kotlin/Position.kt b/src/main/kotlin/Position.kt index 404df88a8..53b96d558 100644 --- a/src/main/kotlin/Position.kt +++ b/src/main/kotlin/Position.kt @@ -1,12 +1,13 @@ data class Position(val x: Int, val y: Int) { init { - require(x in MIN_BOUND..MAX_BOUND) { X_OUT_OF_RANGE_ERROR_MESSAGE } - require(y in MIN_BOUND..MAX_BOUND) { Y_OUT_OF_RANGE_ERROR_MESSAGE } + require(x in POSITION_RANGE) { X_OUT_OF_RANGE_ERROR_MESSAGE } + require(y in POSITION_RANGE) { Y_OUT_OF_RANGE_ERROR_MESSAGE } } companion object { - const val MIN_BOUND = 1 - const val MAX_BOUND = 15 + private const val MIN_BOUND = 1 + private const val MAX_BOUND = 15 + val POSITION_RANGE = (MIN_BOUND..MAX_BOUND) private const val X_OUT_OF_RANGE_ERROR_MESSAGE = "x의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" private const val Y_OUT_OF_RANGE_ERROR_MESSAGE = "y의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" } diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/Stone.kt index 271553e15..f7f28349c 100644 --- a/src/main/kotlin/Stone.kt +++ b/src/main/kotlin/Stone.kt @@ -1,8 +1,9 @@ +import Position.Companion.POSITION_RANGE + class Stone private constructor(val position: Position) { companion object { - private val POSITION_RANGE = (Position.MIN_BOUND..Position.MAX_BOUND) private val STONES = POSITION_RANGE.map { x -> POSITION_RANGE.map { y -> Stone(Position(x, y)) } } - fun of(x: Int, y: Int) = STONES[x][y] + fun of(x: Int, y: Int) = STONES[x - 1][y - 1] } } diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/Stones.kt index b4c64fd22..4a85461b0 100644 --- a/src/main/kotlin/Stones.kt +++ b/src/main/kotlin/Stones.kt @@ -1,11 +1,51 @@ -class Stones(private val stones: List) { +data class Stones(private val stones: List) { constructor(vararg stones: Stone) : this(stones.toList()) init { require(stones.distinct().size == stones.size) { DUPLICATED_ERROR_MESSAGE } } + fun add(newStone: Stone): Stones = Stones(stones + newStone) + + fun hasStone(stone: Stone): Boolean = stones.contains(stone) + + fun checkWin(startStone: Stone): Boolean { + val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) + + for (moveDirection in directions) { + if (checkStraight(startStone.position, moveDirection, FORWARD_WEIGHT)) return true + if (checkStraight(startStone.position, moveDirection, BACK_WEIGHT)) return true + } + return false + } + + private fun checkStraight( + startPosition: Position, + direction: Pair, + weight: Int = FORWARD_WEIGHT + ): Boolean { + val (startX, startY) = Pair(startPosition.x, startPosition.y) + var count = 1 + var (currentX, currentY) = Pair(startX + direction.first * weight, startY + direction.second * weight) + while (currentX in Position.POSITION_RANGE && currentY in Position.POSITION_RANGE && hasStone(Stone.of(currentX, currentY))) { + count++ + currentX += direction.first * weight + currentY += direction.second * weight + } + if (count >= MINIMUM_WIN_CONDITION) return true + return false + } + companion object { private const val DUPLICATED_ERROR_MESSAGE = "중복되는 위치의 오목알을 가질 수 없습니다." + private const val MINIMUM_WIN_CONDITION = 5 + + private val RIGHT_DIRECTION = Pair(1, 0) + private val TOP_DIRECTION = Pair(0, 1) + private val RIGHT_TOP_DIRECTION = Pair(1, 1) + private val LEFT_BOTTOM_DIRECTION = Pair(-1, -1) + + private const val FORWARD_WEIGHT = 1 + private const val BACK_WEIGHT = 1 } } diff --git a/src/test/kotlin/Fixtures.kt b/src/test/kotlin/Fixtures.kt new file mode 100644 index 000000000..158e5d154 --- /dev/null +++ b/src/test/kotlin/Fixtures.kt @@ -0,0 +1,10 @@ +val ONE_ONE_STONE = Stone.of(1, 1) +val ONE_TWO_STONE = Stone.of(1, 2) +val ONE_THREE_STONE = Stone.of(1, 3) +val ONE_FOUR_STONE = Stone.of(1, 4) +val ONE_FIVE_STONE = Stone.of(1, 5) +val ONE_SIX_STONE = Stone.of(1, 6) +val ONE_SEVEN_STONE = Stone.of(1, 7) +val ONE_EIGHT_STONE = Stone.of(1, 8) +val ONE_NINE_STONE = Stone.of(1, 9) +val ONE_TEN_STONE = Stone.of(1, 10) diff --git a/src/test/kotlin/StonesTest.kt b/src/test/kotlin/StonesTest.kt index f2017a383..7d55ea0de 100644 --- a/src/test/kotlin/StonesTest.kt +++ b/src/test/kotlin/StonesTest.kt @@ -1,3 +1,4 @@ +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows @@ -6,14 +7,46 @@ class StonesTest { @Test fun `각각 다른 위치의 오목알을 가질 수 있다`() { assertDoesNotThrow { - Stones(Stone.of(10, 10), Stone.of(11, 11)) + Stones(ONE_ONE_STONE, ONE_TWO_STONE) } } @Test fun `중복되는 위치의 오목알을 가지면 에러가 발생한다`() { assertThrows { - Stones(Stone.of(10, 10), Stone.of(10, 10)) + Stones(ONE_ONE_STONE, ONE_ONE_STONE) } } + + @Test + fun `오목알을 놓을 수 있다`() { + val stones = Stones(ONE_ONE_STONE, ONE_TWO_STONE) + val newStones = stones.add(ONE_THREE_STONE) + + assertThat(newStones).isEqualTo(Stones(ONE_ONE_STONE, ONE_TWO_STONE, ONE_THREE_STONE)) + } + + @Test + fun `오목알이 포함되어 있는지 판단한다`() { + val stones = Stones(ONE_ONE_STONE, ONE_TWO_STONE, ONE_THREE_STONE) + val expected = stones.hasStone(ONE_ONE_STONE) + + assertThat(expected).isTrue + } + + @Test + fun `오목알이 5개 이상 연이어 있으면 참을 반환한다`() { + val stones = Stones(ONE_ONE_STONE, ONE_TWO_STONE, ONE_THREE_STONE, ONE_FOUR_STONE, ONE_FIVE_STONE) + val expected = stones.checkWin(ONE_ONE_STONE) + + assertThat(expected).isTrue + } + + @Test + fun `오목알이 5개 미만 연이어 있으면 거짓을 반환한다`() { + val stones = Stones(ONE_ONE_STONE, ONE_TWO_STONE, ONE_THREE_STONE) + val expected = stones.checkWin(ONE_ONE_STONE) + + assertThat(expected).isFalse + } } From 190d0073827ff0585e64ae44c160fd15a70c92ba Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Tue, 14 Mar 2023 19:25:28 +0900 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=98=A4=EB=AA=A9=EC=95=8C=EC=9D=84=20=EB=86=93?= =?UTF-8?q?=EC=95=98=EC=9D=84=20=EB=95=8C=20=EC=83=81=ED=83=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 7 +++++-- src/main/kotlin/Stone.kt | 2 +- src/main/kotlin/Stones.kt | 2 +- src/main/kotlin/state/PlayerState.kt | 10 +++++++++ src/main/kotlin/state/PlayingState.kt | 12 +++++++++++ src/main/kotlin/state/WinState.kt | 8 +++++++ src/test/kotlin/PlayingStateTest.kt | 30 +++++++++++++++++++++++++++ 7 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/state/PlayerState.kt create mode 100644 src/main/kotlin/state/PlayingState.kt create mode 100644 src/main/kotlin/state/WinState.kt create mode 100644 src/test/kotlin/PlayingStateTest.kt diff --git a/docs/README.md b/docs/README.md index 40d6b666a..b649d43fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,10 +6,13 @@ - [x] `x, y` 위치는 `1부터 15`로 제한된다. - [x] 중복되는 위치의 오목알을 가질 수 없다. - [ ] 오목판의 크기는 `15 x 15`이다. -- [ ] 사용자는 오목알을 놓는다. +- [x] 사용자는 오목알을 놓는다. + - [x] 오목알을 놓았을 때 5개 이상 연이어 있으면 승리한다. + - [x] 오목알을 놓았을 때 5개 미만 연이어 있으면 게임을 계속 진행한다. + - [ ] 특정 위치에 돌을 놓을 수 있는지 판단한다. + - [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. - [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. -- [ ] 특정 위치에 돌을 놓을 수 있는지 판단한다. - [ ] 사용자는 마지막 돌의 위치를 알고 있다. - [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/Stone.kt index f7f28349c..a2a3443cd 100644 --- a/src/main/kotlin/Stone.kt +++ b/src/main/kotlin/Stone.kt @@ -1,6 +1,6 @@ import Position.Companion.POSITION_RANGE -class Stone private constructor(val position: Position) { +data class Stone private constructor(val position: Position) { companion object { private val STONES = POSITION_RANGE.map { x -> POSITION_RANGE.map { y -> Stone(Position(x, y)) } } diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/Stones.kt index 4a85461b0..4b8845efd 100644 --- a/src/main/kotlin/Stones.kt +++ b/src/main/kotlin/Stones.kt @@ -46,6 +46,6 @@ data class Stones(private val stones: List) { private val LEFT_BOTTOM_DIRECTION = Pair(-1, -1) private const val FORWARD_WEIGHT = 1 - private const val BACK_WEIGHT = 1 + private const val BACK_WEIGHT = -1 } } diff --git a/src/main/kotlin/state/PlayerState.kt b/src/main/kotlin/state/PlayerState.kt new file mode 100644 index 000000000..cf19cc266 --- /dev/null +++ b/src/main/kotlin/state/PlayerState.kt @@ -0,0 +1,10 @@ +package state + +import Stone +import Stones + +abstract class PlayerState(protected val stones: Stones) { + abstract fun add(newStone: Stone): PlayerState + + fun hasStone(stone: Stone): Boolean = stones.hasStone(stone) +} diff --git a/src/main/kotlin/state/PlayingState.kt b/src/main/kotlin/state/PlayingState.kt new file mode 100644 index 000000000..4ee7ceb44 --- /dev/null +++ b/src/main/kotlin/state/PlayingState.kt @@ -0,0 +1,12 @@ +package state + +import Stone +import Stones + +class PlayingState(stones: Stones) : PlayerState(stones) { + override fun add(newStone: Stone): PlayerState { + val newStones = stones.add(newStone) + if (newStones.checkWin(newStone)) return WinState(newStones) + return PlayingState(newStones) + } +} diff --git a/src/main/kotlin/state/WinState.kt b/src/main/kotlin/state/WinState.kt new file mode 100644 index 000000000..a4309788f --- /dev/null +++ b/src/main/kotlin/state/WinState.kt @@ -0,0 +1,8 @@ +package state + +import Stone +import Stones + +class WinState(stones: Stones) : PlayerState(stones) { + override fun add(newStone: Stone): PlayerState = this +} diff --git a/src/test/kotlin/PlayingStateTest.kt b/src/test/kotlin/PlayingStateTest.kt new file mode 100644 index 000000000..2c94ea051 --- /dev/null +++ b/src/test/kotlin/PlayingStateTest.kt @@ -0,0 +1,30 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import state.PlayingState +import state.WinState + +class PlayingStateTest { + + @Test + fun `오목알을 놓으면 목록에 추가한다`() { + val playingState = PlayingState(Stones(ONE_ONE_STONE, ONE_TWO_STONE)).add(ONE_THREE_STONE) + val expected = playingState.hasStone(ONE_THREE_STONE) + assertThat(expected).isTrue + } + + @Test + fun `돌을 놓았을 때 오목알 연이어져 있는 오목알이 5개 미만이면 게임을 계속한다`() { + val playingState = PlayingState(Stones(ONE_ONE_STONE, ONE_TWO_STONE)) + val expected = playingState.add(ONE_THREE_STONE) + + assertThat(expected).isInstanceOf(PlayingState::class.java) + } + + @Test + fun `돌을 놓았을 때 오목알 5개 이상이 연이어져 있으면 승리한다`() { + val playingState = PlayingState(Stones(ONE_ONE_STONE, ONE_TWO_STONE, ONE_THREE_STONE, ONE_FOUR_STONE)) + val expected = playingState.add(ONE_FIVE_STONE) + + assertThat(expected).isInstanceOf(WinState::class.java) + } +} From 9d0c27601debdf1bf00e07c3b9e98f4904a687d0 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 14:24:24 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=AA=A9=ED=8C=90?= =?UTF-8?q?=EC=97=90=20=EB=8F=8C=EC=9D=84=20=EC=98=AC=EB=A0=A4=EB=86=93?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 6 +++--- src/main/kotlin/BoardState.kt | 3 +++ src/main/kotlin/Position.kt | 10 +++++----- src/main/kotlin/Stone.kt | 4 ++-- src/main/kotlin/Stones.kt | 4 +++- src/main/kotlin/player/BlackPlayer.kt | 11 +++++++++++ src/main/kotlin/player/Board.kt | 24 ++++++++++++++++++++++++ src/main/kotlin/player/Player.kt | 14 ++++++++++++++ src/main/kotlin/player/Players.kt | 13 +++++++++++++ src/main/kotlin/player/Turn.kt | 5 +++++ src/main/kotlin/player/WhitePlayer.kt | 11 +++++++++++ src/main/kotlin/state/PlayerState.kt | 5 ++++- src/main/kotlin/state/PlayingState.kt | 2 +- src/test/kotlin/BlackPlayerTest.kt | 23 +++++++++++++++++++++++ src/test/kotlin/BoardTest.kt | 26 ++++++++++++++++++++++++++ src/test/kotlin/PositionTest.kt | 4 ++-- src/test/kotlin/WhitePlayerTest.kt | 23 +++++++++++++++++++++++ 17 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/BoardState.kt create mode 100644 src/main/kotlin/player/BlackPlayer.kt create mode 100644 src/main/kotlin/player/Board.kt create mode 100644 src/main/kotlin/player/Player.kt create mode 100644 src/main/kotlin/player/Players.kt create mode 100644 src/main/kotlin/player/Turn.kt create mode 100644 src/main/kotlin/player/WhitePlayer.kt create mode 100644 src/test/kotlin/BlackPlayerTest.kt create mode 100644 src/test/kotlin/BoardTest.kt create mode 100644 src/test/kotlin/WhitePlayerTest.kt diff --git a/docs/README.md b/docs/README.md index b649d43fb..c1f47b41b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,12 +5,12 @@ - [x] 오목알은 자신의 위치를 알고 있다. - [x] `x, y` 위치는 `1부터 15`로 제한된다. - [x] 중복되는 위치의 오목알을 가질 수 없다. -- [ ] 오목판의 크기는 `15 x 15`이다. +- [x] 오목판의 크기는 `15 x 15`이다. - [x] 사용자는 오목알을 놓는다. - [x] 오목알을 놓았을 때 5개 이상 연이어 있으면 승리한다. - [x] 오목알을 놓았을 때 5개 미만 연이어 있으면 게임을 계속 진행한다. - - [ ] 특정 위치에 돌을 놓을 수 있는지 판단한다. - + - [x] 특정 위치에 돌을 놓을 수 있는지 판단한다. + - [x] 플레이어는 흑과 백으로 이루어져 있다. - [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. - [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. - [ ] 사용자는 마지막 돌의 위치를 알고 있다. diff --git a/src/main/kotlin/BoardState.kt b/src/main/kotlin/BoardState.kt new file mode 100644 index 000000000..451e21867 --- /dev/null +++ b/src/main/kotlin/BoardState.kt @@ -0,0 +1,3 @@ +enum class BoardState { + EMPTY, BLACK, WHITE +} diff --git a/src/main/kotlin/Position.kt b/src/main/kotlin/Position.kt index 53b96d558..786c47bca 100644 --- a/src/main/kotlin/Position.kt +++ b/src/main/kotlin/Position.kt @@ -1,14 +1,14 @@ -data class Position(val x: Int, val y: Int) { +data class Position(val row: Int, val col: Int) { init { - require(x in POSITION_RANGE) { X_OUT_OF_RANGE_ERROR_MESSAGE } - require(y in POSITION_RANGE) { Y_OUT_OF_RANGE_ERROR_MESSAGE } + require(row in POSITION_RANGE) { ROW_OUT_OF_RANGE_ERROR_MESSAGE } + require(col in POSITION_RANGE) { COLUMN_OUT_OF_RANGE_ERROR_MESSAGE } } companion object { private const val MIN_BOUND = 1 private const val MAX_BOUND = 15 val POSITION_RANGE = (MIN_BOUND..MAX_BOUND) - private const val X_OUT_OF_RANGE_ERROR_MESSAGE = "x의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" - private const val Y_OUT_OF_RANGE_ERROR_MESSAGE = "y의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" + private const val ROW_OUT_OF_RANGE_ERROR_MESSAGE = "행의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" + private const val COLUMN_OUT_OF_RANGE_ERROR_MESSAGE = "열의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" } } diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/Stone.kt index a2a3443cd..0318df0d2 100644 --- a/src/main/kotlin/Stone.kt +++ b/src/main/kotlin/Stone.kt @@ -2,8 +2,8 @@ import Position.Companion.POSITION_RANGE data class Stone private constructor(val position: Position) { companion object { - private val STONES = POSITION_RANGE.map { x -> POSITION_RANGE.map { y -> Stone(Position(x, y)) } } + private val STONES = POSITION_RANGE.map { row -> POSITION_RANGE.map { col -> Stone(Position(row, col)) } } - fun of(x: Int, y: Int) = STONES[x - 1][y - 1] + fun of(row: Int, col: Int) = STONES[row - 1][col - 1] } } diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/Stones.kt index 4b8845efd..43d989f10 100644 --- a/src/main/kotlin/Stones.kt +++ b/src/main/kotlin/Stones.kt @@ -5,6 +5,8 @@ data class Stones(private val stones: List) { require(stones.distinct().size == stones.size) { DUPLICATED_ERROR_MESSAGE } } + fun getPositions(): List = stones.map { it.position.copy() } + fun add(newStone: Stone): Stones = Stones(stones + newStone) fun hasStone(stone: Stone): Boolean = stones.contains(stone) @@ -24,7 +26,7 @@ data class Stones(private val stones: List) { direction: Pair, weight: Int = FORWARD_WEIGHT ): Boolean { - val (startX, startY) = Pair(startPosition.x, startPosition.y) + val (startX, startY) = Pair(startPosition.row, startPosition.col) var count = 1 var (currentX, currentY) = Pair(startX + direction.first * weight, startY + direction.second * weight) while (currentX in Position.POSITION_RANGE && currentY in Position.POSITION_RANGE && hasStone(Stone.of(currentX, currentY))) { diff --git a/src/main/kotlin/player/BlackPlayer.kt b/src/main/kotlin/player/BlackPlayer.kt new file mode 100644 index 000000000..c62162f86 --- /dev/null +++ b/src/main/kotlin/player/BlackPlayer.kt @@ -0,0 +1,11 @@ +package player + +import BoardState +import Stone +import state.PlayerState +import state.PlayingState + +class BlackPlayer(state: PlayerState = PlayingState()) : Player(state) { + override val boardState: BoardState = BoardState.BLACK + override fun putStone(stone: Stone): Player = BlackPlayer(state.add(stone)) +} diff --git a/src/main/kotlin/player/Board.kt b/src/main/kotlin/player/Board.kt new file mode 100644 index 000000000..6e64230d4 --- /dev/null +++ b/src/main/kotlin/player/Board.kt @@ -0,0 +1,24 @@ +package player + +import BoardState +import Position.Companion.POSITION_RANGE +import Stone + +class Board(blackPlayer: Player, whitePlayer: Player) { + private val players: Players = Players(blackPlayer, whitePlayer) + + private fun canPlace(stone: Stone): Boolean = allPlayers().none { it.isPlaced(stone) } + + private fun allPlayers(): List = players.toList() + + fun putStone(turn: Turn, stone: Stone): Boolean { + if (canPlace(stone)) { + players.putStone(turn, stone) + return true + } + return false + } + + private fun makeEmptyBoard(): List> = + List(POSITION_RANGE.max()) { MutableList(POSITION_RANGE.max()) { BoardState.EMPTY } } +} diff --git a/src/main/kotlin/player/Player.kt b/src/main/kotlin/player/Player.kt new file mode 100644 index 000000000..637ad85c6 --- /dev/null +++ b/src/main/kotlin/player/Player.kt @@ -0,0 +1,14 @@ +package player + +import BoardState +import Position +import Stone +import state.PlayerState + +abstract class Player(protected val state: PlayerState) { + abstract val boardState: BoardState + + fun isPlaced(stone: Stone): Boolean = state.hasStone(stone) + fun getPlaced(): List = state.getPlaced() + abstract fun putStone(stone: Stone): Player +} diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/player/Players.kt new file mode 100644 index 000000000..717fdba4b --- /dev/null +++ b/src/main/kotlin/player/Players.kt @@ -0,0 +1,13 @@ +package player + +import Stone + +class Players private constructor(private val players: Map) { + constructor(black: Player, white: Player) : this(mapOf(Turn.BLACK to black, Turn.WHITE to white)) + + fun toList(): List = players.values.toList() + + fun putStone(turn: Turn, stone: Stone) { + players[turn]?.putStone(stone) + } +} diff --git a/src/main/kotlin/player/Turn.kt b/src/main/kotlin/player/Turn.kt new file mode 100644 index 000000000..0e022684d --- /dev/null +++ b/src/main/kotlin/player/Turn.kt @@ -0,0 +1,5 @@ +package player + +enum class Turn { + BLACK, WHITE +} diff --git a/src/main/kotlin/player/WhitePlayer.kt b/src/main/kotlin/player/WhitePlayer.kt new file mode 100644 index 000000000..1e9b51b29 --- /dev/null +++ b/src/main/kotlin/player/WhitePlayer.kt @@ -0,0 +1,11 @@ +package player + +import BoardState +import Stone +import state.PlayerState +import state.PlayingState + +class WhitePlayer(state: PlayerState = PlayingState()) : Player(state) { + override val boardState: BoardState = BoardState.WHITE + override fun putStone(stone: Stone): Player = WhitePlayer(state.add(stone)) +} diff --git a/src/main/kotlin/state/PlayerState.kt b/src/main/kotlin/state/PlayerState.kt index cf19cc266..00c8f6079 100644 --- a/src/main/kotlin/state/PlayerState.kt +++ b/src/main/kotlin/state/PlayerState.kt @@ -1,10 +1,13 @@ package state +import Position import Stone import Stones -abstract class PlayerState(protected val stones: Stones) { +abstract class PlayerState(protected val stones: Stones = Stones()) { abstract fun add(newStone: Stone): PlayerState fun hasStone(stone: Stone): Boolean = stones.hasStone(stone) + + fun getPlaced(): List = stones.getPositions() } diff --git a/src/main/kotlin/state/PlayingState.kt b/src/main/kotlin/state/PlayingState.kt index 4ee7ceb44..de2219d03 100644 --- a/src/main/kotlin/state/PlayingState.kt +++ b/src/main/kotlin/state/PlayingState.kt @@ -3,7 +3,7 @@ package state import Stone import Stones -class PlayingState(stones: Stones) : PlayerState(stones) { +class PlayingState(stones: Stones = Stones()) : PlayerState(stones) { override fun add(newStone: Stone): PlayerState { val newStones = stones.add(newStone) if (newStones.checkWin(newStone)) return WinState(newStones) diff --git a/src/test/kotlin/BlackPlayerTest.kt b/src/test/kotlin/BlackPlayerTest.kt new file mode 100644 index 000000000..f7e9226d1 --- /dev/null +++ b/src/test/kotlin/BlackPlayerTest.kt @@ -0,0 +1,23 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import player.BlackPlayer +import player.Player +import state.PlayingState + +class BlackPlayerTest { + @Test + fun `특정 위치에 흑의 오목알이 없으면 참을 반환한다`() { + val player: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) + val expected = player.isPlaced(ONE_ONE_STONE) + + assertThat(expected).isTrue + } + + @Test + fun `특정 위치에 흑의 오목알이 없으면 거짓을 반환한다`() { + val player: Player = BlackPlayer() + val expected = player.isPlaced(ONE_ONE_STONE) + + assertThat(expected).isFalse + } +} diff --git a/src/test/kotlin/BoardTest.kt b/src/test/kotlin/BoardTest.kt new file mode 100644 index 000000000..4aaee253c --- /dev/null +++ b/src/test/kotlin/BoardTest.kt @@ -0,0 +1,26 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import player.BlackPlayer +import player.Board +import player.Player +import player.Turn +import player.WhitePlayer +import state.PlayingState + +class BoardTest { + @Test + fun `특정 위치에 돌을 놓으면 참을 반환한다`() { + val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) + val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) + val board = Board(blackPlayer, whitePlayer) + assertThat(board.putStone(Turn.BLACK, ONE_THREE_STONE)).isTrue + } + + @Test + fun `특정 위치에 돌을 놓지 못하면 거짓을 반환한다`() { + val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) + val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) + val board = Board(blackPlayer, whitePlayer) + assertThat(board.putStone(Turn.BLACK, ONE_TWO_STONE)).isFalse + } +} diff --git a/src/test/kotlin/PositionTest.kt b/src/test/kotlin/PositionTest.kt index 5b890a283..c6439897a 100644 --- a/src/test/kotlin/PositionTest.kt +++ b/src/test/kotlin/PositionTest.kt @@ -8,8 +8,8 @@ class PositionTest { fun `오목알의 위치는 범위가 1부터 15인 x, y를 가지고 있다`() { val position = Position(10, 10) assertAll({ - assertThat(position.x).isEqualTo(10) - assertThat(position.y).isEqualTo(10) + assertThat(position.row).isEqualTo(10) + assertThat(position.col).isEqualTo(10) }) } diff --git a/src/test/kotlin/WhitePlayerTest.kt b/src/test/kotlin/WhitePlayerTest.kt new file mode 100644 index 000000000..abeb4dfcb --- /dev/null +++ b/src/test/kotlin/WhitePlayerTest.kt @@ -0,0 +1,23 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import player.Player +import player.WhitePlayer +import state.PlayingState + +class WhitePlayerTest { + @Test + fun `특정 위치에 백의 오목알이 없으면 참을 반환한다`() { + val player: Player = WhitePlayer(PlayingState(Stones(ONE_ONE_STONE))) + val expected = player.isPlaced(ONE_ONE_STONE) + + assertThat(expected).isTrue + } + + @Test + fun `특정 위치에 백의 오목알이 없으면 거짓을 반환한다`() { + val player: Player = WhitePlayer() + val expected = player.isPlaced(ONE_ONE_STONE) + + assertThat(expected).isFalse + } +} From 606cfcfde9518b85f9d6bdf2c8c0358eae36eb88 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 14:57:13 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EC=9D=98=20=EC=98=A4=EB=AA=A9=EC=95=8C=EC=9D=84=20?= =?UTF-8?q?=EB=86=93=EB=8A=94=20=ED=84=B4=EC=9D=84=20=EB=B0=94=EA=BE=B8?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/player/Turn.kt | 7 ++++++- src/test/kotlin/BoardTest.kt | 8 ++++---- src/test/kotlin/TurnTest.kt | 13 +++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/test/kotlin/TurnTest.kt diff --git a/src/main/kotlin/player/Turn.kt b/src/main/kotlin/player/Turn.kt index 0e022684d..7319f9292 100644 --- a/src/main/kotlin/player/Turn.kt +++ b/src/main/kotlin/player/Turn.kt @@ -1,5 +1,10 @@ package player enum class Turn { - BLACK, WHITE + BLACK, WHITE; + + fun next(): Turn = when (this) { + BLACK -> WHITE + WHITE -> BLACK + } } diff --git a/src/test/kotlin/BoardTest.kt b/src/test/kotlin/BoardTest.kt index 4aaee253c..3b4a32eea 100644 --- a/src/test/kotlin/BoardTest.kt +++ b/src/test/kotlin/BoardTest.kt @@ -9,18 +9,18 @@ import state.PlayingState class BoardTest { @Test - fun `특정 위치에 돌을 놓으면 참을 반환한다`() { + fun `특정 위치에 돌을 놓으면 플레이어를 반환한다`() { val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) val board = Board(blackPlayer, whitePlayer) - assertThat(board.putStone(Turn.BLACK, ONE_THREE_STONE)).isTrue + assertThat(board.putStone(Turn.BLACK, ONE_THREE_STONE)).isInstanceOf(Player::class.java) } @Test - fun `특정 위치에 돌을 놓지 못하면 거짓을 반환한다`() { + fun `특정 위치에 돌을 놓지 못하면 null을 반환한다`() { val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) val board = Board(blackPlayer, whitePlayer) - assertThat(board.putStone(Turn.BLACK, ONE_TWO_STONE)).isFalse + assertThat(board.putStone(Turn.BLACK, ONE_TWO_STONE)).isNull() } } diff --git a/src/test/kotlin/TurnTest.kt b/src/test/kotlin/TurnTest.kt new file mode 100644 index 000000000..817c8a04e --- /dev/null +++ b/src/test/kotlin/TurnTest.kt @@ -0,0 +1,13 @@ +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import player.Turn + +class TurnTest { + @CsvSource("WHITE, BLACK", "BLACK, WHITE") + @ParameterizedTest + fun `상대방 차례를 반환한다`(myTurn: Turn, expected: Turn) { + val actual = myTurn.next() + assertThat(actual).isEqualTo(expected) + } +} From f96746e307833bb259081e480a52b159d3b180ef Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 14:57:38 +0900 Subject: [PATCH 09/18] =?UTF-8?q?refactor:=20=EB=8F=8C=EC=9D=84=20?= =?UTF-8?q?=EB=86=93=EC=9C=BC=EB=A9=B4=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B0=80=EC=A7=84=20=ED=94=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 1 + src/main/kotlin/player/Board.kt | 11 +++++------ src/main/kotlin/player/Players.kt | 4 +--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/README.md b/docs/README.md index c1f47b41b..b0e984007 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ - [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. - [ ] 사용자는 마지막 돌의 위치를 알고 있다. - [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. +- [x] 오목알을 놓으면 상대방의 차례가 된다. ### Input - [ ] 오목알을 놓을 위치를 입력받는다. diff --git a/src/main/kotlin/player/Board.kt b/src/main/kotlin/player/Board.kt index 6e64230d4..ff63002fb 100644 --- a/src/main/kotlin/player/Board.kt +++ b/src/main/kotlin/player/Board.kt @@ -4,19 +4,18 @@ import BoardState import Position.Companion.POSITION_RANGE import Stone -class Board(blackPlayer: Player, whitePlayer: Player) { - private val players: Players = Players(blackPlayer, whitePlayer) +class Board(private val players: Players) { + constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) private fun canPlace(stone: Stone): Boolean = allPlayers().none { it.isPlaced(stone) } private fun allPlayers(): List = players.toList() - fun putStone(turn: Turn, stone: Stone): Boolean { + fun putStone(turn: Turn, stone: Stone): Player? { if (canPlace(stone)) { - players.putStone(turn, stone) - return true + return players.putStone(turn, stone) } - return false + return null } private fun makeEmptyBoard(): List> = diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/player/Players.kt index 717fdba4b..4216f53d0 100644 --- a/src/main/kotlin/player/Players.kt +++ b/src/main/kotlin/player/Players.kt @@ -7,7 +7,5 @@ class Players private constructor(private val players: Map) { fun toList(): List = players.values.toList() - fun putStone(turn: Turn, stone: Stone) { - players[turn]?.putStone(stone) - } + fun putStone(turn: Turn, stone: Stone): Player? = players[turn]?.putStone(stone) } From e5e8ce4a1751cff287eafc1e8fd08f642ec76765 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 15:06:25 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=EA=B0=80=20=EB=86=93=EC=9D=80=20=EB=A7=88=EC=A7=80?= =?UTF-8?q?=EB=A7=89=20=EB=8F=8C=EC=9D=84=20=EB=A6=AC=ED=84=B4=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 2 +- src/main/kotlin/{player => }/Board.kt | 6 ++---- src/main/kotlin/Stones.kt | 2 ++ src/main/kotlin/{player => }/Turn.kt | 2 -- src/main/kotlin/player/Player.kt | 6 +++++- src/main/kotlin/player/Players.kt | 1 + src/main/kotlin/state/PlayerState.kt | 1 + src/test/kotlin/BlackPlayerTest.kt | 6 ++++++ src/test/kotlin/BoardTest.kt | 2 -- src/test/kotlin/TurnTest.kt | 1 - src/test/kotlin/WhitePlayerTest.kt | 6 ++++++ 11 files changed, 24 insertions(+), 11 deletions(-) rename src/main/kotlin/{player => }/Board.kt (93%) rename src/main/kotlin/{player => }/Turn.kt (88%) diff --git a/docs/README.md b/docs/README.md index b0e984007..df0b11414 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ - [x] 플레이어는 흑과 백으로 이루어져 있다. - [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. - [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. -- [ ] 사용자는 마지막 돌의 위치를 알고 있다. +- [x] 사용자는 마지막 돌의 위치를 알고 있다. - [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. - [x] 오목알을 놓으면 상대방의 차례가 된다. diff --git a/src/main/kotlin/player/Board.kt b/src/main/kotlin/Board.kt similarity index 93% rename from src/main/kotlin/player/Board.kt rename to src/main/kotlin/Board.kt index ff63002fb..8b1281019 100644 --- a/src/main/kotlin/player/Board.kt +++ b/src/main/kotlin/Board.kt @@ -1,8 +1,6 @@ -package player - -import BoardState import Position.Companion.POSITION_RANGE -import Stone +import player.Player +import player.Players class Board(private val players: Players) { constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/Stones.kt index 43d989f10..718387a2b 100644 --- a/src/main/kotlin/Stones.kt +++ b/src/main/kotlin/Stones.kt @@ -1,4 +1,6 @@ data class Stones(private val stones: List) { + val lastStone: Stone + get() = stones.last().copy() constructor(vararg stones: Stone) : this(stones.toList()) init { diff --git a/src/main/kotlin/player/Turn.kt b/src/main/kotlin/Turn.kt similarity index 88% rename from src/main/kotlin/player/Turn.kt rename to src/main/kotlin/Turn.kt index 7319f9292..1f8155bbd 100644 --- a/src/main/kotlin/player/Turn.kt +++ b/src/main/kotlin/Turn.kt @@ -1,5 +1,3 @@ -package player - enum class Turn { BLACK, WHITE; diff --git a/src/main/kotlin/player/Player.kt b/src/main/kotlin/player/Player.kt index 637ad85c6..c7f30a2a7 100644 --- a/src/main/kotlin/player/Player.kt +++ b/src/main/kotlin/player/Player.kt @@ -8,7 +8,11 @@ import state.PlayerState abstract class Player(protected val state: PlayerState) { abstract val boardState: BoardState + abstract fun putStone(stone: Stone): Player + fun isPlaced(stone: Stone): Boolean = state.hasStone(stone) + fun getPlaced(): List = state.getPlaced() - abstract fun putStone(stone: Stone): Player + + fun getLastStone(): Stone = state.getLastStone() } diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/player/Players.kt index 4216f53d0..9a6479264 100644 --- a/src/main/kotlin/player/Players.kt +++ b/src/main/kotlin/player/Players.kt @@ -1,6 +1,7 @@ package player import Stone +import Turn class Players private constructor(private val players: Map) { constructor(black: Player, white: Player) : this(mapOf(Turn.BLACK to black, Turn.WHITE to white)) diff --git a/src/main/kotlin/state/PlayerState.kt b/src/main/kotlin/state/PlayerState.kt index 00c8f6079..7cf1418a0 100644 --- a/src/main/kotlin/state/PlayerState.kt +++ b/src/main/kotlin/state/PlayerState.kt @@ -10,4 +10,5 @@ abstract class PlayerState(protected val stones: Stones = Stones()) { fun hasStone(stone: Stone): Boolean = stones.hasStone(stone) fun getPlaced(): List = stones.getPositions() + fun getLastStone(): Stone = stones.lastStone } diff --git a/src/test/kotlin/BlackPlayerTest.kt b/src/test/kotlin/BlackPlayerTest.kt index f7e9226d1..e15269617 100644 --- a/src/test/kotlin/BlackPlayerTest.kt +++ b/src/test/kotlin/BlackPlayerTest.kt @@ -20,4 +20,10 @@ class BlackPlayerTest { assertThat(expected).isFalse } + + @Test + fun `마지막 놓은 돌을 반환한다`() { + val player: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) + assertThat(player.getLastStone()).isEqualTo(ONE_ONE_STONE) + } } diff --git a/src/test/kotlin/BoardTest.kt b/src/test/kotlin/BoardTest.kt index 3b4a32eea..36ef6bcfa 100644 --- a/src/test/kotlin/BoardTest.kt +++ b/src/test/kotlin/BoardTest.kt @@ -1,9 +1,7 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import player.BlackPlayer -import player.Board import player.Player -import player.Turn import player.WhitePlayer import state.PlayingState diff --git a/src/test/kotlin/TurnTest.kt b/src/test/kotlin/TurnTest.kt index 817c8a04e..903774851 100644 --- a/src/test/kotlin/TurnTest.kt +++ b/src/test/kotlin/TurnTest.kt @@ -1,7 +1,6 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource -import player.Turn class TurnTest { @CsvSource("WHITE, BLACK", "BLACK, WHITE") diff --git a/src/test/kotlin/WhitePlayerTest.kt b/src/test/kotlin/WhitePlayerTest.kt index abeb4dfcb..fe6eca79e 100644 --- a/src/test/kotlin/WhitePlayerTest.kt +++ b/src/test/kotlin/WhitePlayerTest.kt @@ -20,4 +20,10 @@ class WhitePlayerTest { assertThat(expected).isFalse } + + @Test + fun `마지막 놓은 돌을 반환한다`() { + val player: Player = WhitePlayer(PlayingState(Stones(ONE_ONE_STONE))) + assertThat(player.getLastStone()).isEqualTo(ONE_ONE_STONE) + } } From c472a31b87eee4ad7f741e736851314f0cf80ac2 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 15:10:25 +0900 Subject: [PATCH 11/18] =?UTF-8?q?refactor:=20=EC=98=A4=EB=AA=A9=EC=95=8C?= =?UTF-8?q?=EC=9D=84=20=EB=86=93=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20Players=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/Board.kt | 6 +----- src/main/kotlin/player/Players.kt | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/Board.kt b/src/main/kotlin/Board.kt index 8b1281019..7cca8f0ba 100644 --- a/src/main/kotlin/Board.kt +++ b/src/main/kotlin/Board.kt @@ -5,12 +5,8 @@ import player.Players class Board(private val players: Players) { constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) - private fun canPlace(stone: Stone): Boolean = allPlayers().none { it.isPlaced(stone) } - - private fun allPlayers(): List = players.toList() - fun putStone(turn: Turn, stone: Stone): Player? { - if (canPlace(stone)) { + if (players.canPlace(stone)) { return players.putStone(turn, stone) } return null diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/player/Players.kt index 9a6479264..23247335a 100644 --- a/src/main/kotlin/player/Players.kt +++ b/src/main/kotlin/player/Players.kt @@ -6,7 +6,7 @@ import Turn class Players private constructor(private val players: Map) { constructor(black: Player, white: Player) : this(mapOf(Turn.BLACK to black, Turn.WHITE to white)) - fun toList(): List = players.values.toList() - fun putStone(turn: Turn, stone: Stone): Player? = players[turn]?.putStone(stone) + + fun canPlace(stone: Stone): Boolean = players.values.none { it.isPlaced(stone) } } From f4f1b159768e558f3c0a542f6cb33f85a019cd1f Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 16:47:37 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=AA=A9=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EC=A7=84=ED=96=89=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 7 +++-- src/main/kotlin/Board.kt | 15 ++++++---- src/main/kotlin/BoardState.kt | 3 -- src/main/kotlin/Stone.kt | 3 +- src/main/kotlin/{Turn.kt => StoneColor.kt} | 4 +-- src/main/kotlin/game/Omok.kt | 30 +++++++++++++++++++ src/main/kotlin/listener/OmokEventListener.kt | 13 ++++++++ src/main/kotlin/player/BlackPlayer.kt | 2 -- src/main/kotlin/player/Player.kt | 12 +++++--- src/main/kotlin/player/Players.kt | 24 +++++++++++---- src/main/kotlin/player/WhitePlayer.kt | 2 -- src/main/kotlin/state/PlayerState.kt | 1 + src/test/kotlin/BoardTest.kt | 4 +-- src/test/kotlin/OmokTest.kt | 9 ++++++ .../kotlin/{TurnTest.kt => StoneColorTest.kt} | 6 ++-- 15 files changed, 104 insertions(+), 31 deletions(-) delete mode 100644 src/main/kotlin/BoardState.kt rename src/main/kotlin/{Turn.kt => StoneColor.kt} (52%) create mode 100644 src/main/kotlin/game/Omok.kt create mode 100644 src/main/kotlin/listener/OmokEventListener.kt create mode 100644 src/test/kotlin/OmokTest.kt rename src/test/kotlin/{TurnTest.kt => StoneColorTest.kt} (63%) diff --git a/docs/README.md b/docs/README.md index df0b11414..435f8182c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,10 @@ ## 오목 ### Domain -- [ ] 흑돌이 먼저 시작한다. +- [ ] 한 플레이어라도 승리할 때까지 차례를 번갈아가면서 돌을 놓는다. + - [x] 흑, 백 플레이어를 가지고 있다 + - [x] 흑돌이 먼저 시작한다. + - [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. - [x] 오목알은 자신의 위치를 알고 있다. - [x] `x, y` 위치는 `1부터 15`로 제한된다. - [x] 중복되는 위치의 오목알을 가질 수 없다. @@ -14,7 +17,6 @@ - [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. - [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. - [x] 사용자는 마지막 돌의 위치를 알고 있다. -- [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. - [x] 오목알을 놓으면 상대방의 차례가 된다. ### Input @@ -22,3 +24,4 @@ ### Output - [ ] 오목알의 위치를 입력받기 전에 오목판을 출력한다. +- [ ] 마지막 돌의 위치를 출력한다. diff --git a/src/main/kotlin/Board.kt b/src/main/kotlin/Board.kt index 7cca8f0ba..64bd804d1 100644 --- a/src/main/kotlin/Board.kt +++ b/src/main/kotlin/Board.kt @@ -1,17 +1,22 @@ -import Position.Companion.POSITION_RANGE import player.Player import player.Players class Board(private val players: Players) { constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) - fun putStone(turn: Turn, stone: Stone): Player? { + fun putStone(stoneColor: StoneColor, stone: Stone): Board? { if (players.canPlace(stone)) { - return players.putStone(turn, stone) + return Board(players.putStone(stoneColor, stone)) } return null } - private fun makeEmptyBoard(): List> = - List(POSITION_RANGE.max()) { MutableList(POSITION_RANGE.max()) { BoardState.EMPTY } } + fun getPlayers(): Players = players.copy() + + fun isRunning(): Boolean = players.isRunning + + fun getWinner(): Player = players.getWinner() + + // private fun makeEmptyBoard(): List> = + // List(POSITION_RANGE.max()) { MutableList(POSITION_RANGE.max()) { BoardState.EMPTY } } } diff --git a/src/main/kotlin/BoardState.kt b/src/main/kotlin/BoardState.kt deleted file mode 100644 index 451e21867..000000000 --- a/src/main/kotlin/BoardState.kt +++ /dev/null @@ -1,3 +0,0 @@ -enum class BoardState { - EMPTY, BLACK, WHITE -} diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/Stone.kt index 0318df0d2..f162e1da4 100644 --- a/src/main/kotlin/Stone.kt +++ b/src/main/kotlin/Stone.kt @@ -4,6 +4,7 @@ data class Stone private constructor(val position: Position) { companion object { private val STONES = POSITION_RANGE.map { row -> POSITION_RANGE.map { col -> Stone(Position(row, col)) } } - fun of(row: Int, col: Int) = STONES[row - 1][col - 1] + fun of(row: Int, col: Int): Stone = STONES[row - 1][col - 1] + fun of(position: Position): Stone = STONES[position.row - 1][position.col - 1] } } diff --git a/src/main/kotlin/Turn.kt b/src/main/kotlin/StoneColor.kt similarity index 52% rename from src/main/kotlin/Turn.kt rename to src/main/kotlin/StoneColor.kt index 1f8155bbd..e71aa36a0 100644 --- a/src/main/kotlin/Turn.kt +++ b/src/main/kotlin/StoneColor.kt @@ -1,7 +1,7 @@ -enum class Turn { +enum class StoneColor { BLACK, WHITE; - fun next(): Turn = when (this) { + fun next(): StoneColor = when (this) { BLACK -> WHITE WHITE -> BLACK } diff --git a/src/main/kotlin/game/Omok.kt b/src/main/kotlin/game/Omok.kt new file mode 100644 index 000000000..bf5e55dba --- /dev/null +++ b/src/main/kotlin/game/Omok.kt @@ -0,0 +1,30 @@ +package game + +import Board +import Stone +import StoneColor +import listener.OmokEventListener +import player.BlackPlayer +import player.WhitePlayer + +class Omok(private val eventListener: OmokEventListener) { + private val board = Board(BlackPlayer(), WhitePlayer()) + + fun run() { + eventListener.onStartGame() + var currentStoneColor: StoneColor = StoneColor.BLACK + var currentBoard: Board = board + do { + currentBoard = takeTurn(currentBoard, currentStoneColor) + eventListener.onEndTurn(currentBoard.getPlayers()) + currentStoneColor = currentStoneColor.next() + } while (currentBoard.isRunning()) + + eventListener.onEndGame(board.getWinner()) + } + + private fun takeTurn(board: Board, stoneColor: StoneColor): Board { + val newStone = Stone.of(eventListener.onTakeTurn(stoneColor)) + return board.putStone(stoneColor, newStone) ?: return takeTurn(board, stoneColor) + } +} diff --git a/src/main/kotlin/listener/OmokEventListener.kt b/src/main/kotlin/listener/OmokEventListener.kt new file mode 100644 index 000000000..2d198da8e --- /dev/null +++ b/src/main/kotlin/listener/OmokEventListener.kt @@ -0,0 +1,13 @@ +package listener + +import Position +import StoneColor +import player.Player +import player.Players + +interface OmokEventListener { + fun onStartGame() + fun onTakeTurn(stoneColor: StoneColor): Position + fun onEndTurn(positions: Players) + fun onEndGame(player: Player) +} diff --git a/src/main/kotlin/player/BlackPlayer.kt b/src/main/kotlin/player/BlackPlayer.kt index c62162f86..acfb9034b 100644 --- a/src/main/kotlin/player/BlackPlayer.kt +++ b/src/main/kotlin/player/BlackPlayer.kt @@ -1,11 +1,9 @@ package player -import BoardState import Stone import state.PlayerState import state.PlayingState class BlackPlayer(state: PlayerState = PlayingState()) : Player(state) { - override val boardState: BoardState = BoardState.BLACK override fun putStone(stone: Stone): Player = BlackPlayer(state.add(stone)) } diff --git a/src/main/kotlin/player/Player.kt b/src/main/kotlin/player/Player.kt index c7f30a2a7..dd883e6db 100644 --- a/src/main/kotlin/player/Player.kt +++ b/src/main/kotlin/player/Player.kt @@ -1,18 +1,22 @@ package player -import BoardState import Position import Stone import state.PlayerState +import state.WinState -abstract class Player(protected val state: PlayerState) { - abstract val boardState: BoardState +abstract class Player(protected val state: PlayerState) : Cloneable { + val isWin: Boolean = state is WinState abstract fun putStone(stone: Stone): Player + fun canPlace(): Boolean = state !is WinState + fun isPlaced(stone: Stone): Boolean = state.hasStone(stone) - fun getPlaced(): List = state.getPlaced() + fun getPositions(): List = state.getPlaced() fun getLastStone(): Stone = state.getLastStone() + + public override fun clone(): Player = super.clone() as Player } diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/player/Players.kt index 23247335a..d62a920b7 100644 --- a/src/main/kotlin/player/Players.kt +++ b/src/main/kotlin/player/Players.kt @@ -1,12 +1,26 @@ package player import Stone -import Turn +import StoneColor -class Players private constructor(private val players: Map) { - constructor(black: Player, white: Player) : this(mapOf(Turn.BLACK to black, Turn.WHITE to white)) +data class Players private constructor(private val players: List) { + val isRunning: Boolean + get() = players.all { it.canPlace() } - fun putStone(turn: Turn, stone: Stone): Player? = players[turn]?.putStone(stone) + constructor(blackPlayer: Player, whitePlayer: Player) : this(listOf(blackPlayer.clone(), whitePlayer.clone())) - fun canPlace(stone: Stone): Boolean = players.values.none { it.isPlaced(stone) } + fun putStone(stoneColor: StoneColor, stone: Stone): Players { + if (stoneColor == StoneColor.BLACK) { + return Players(blackPlayer = getBlackPlayer().putStone(stone), whitePlayer = getWhitePlayer()) + } + return Players(blackPlayer = getBlackPlayer(), whitePlayer = getWhitePlayer().putStone(stone)) + } + + fun getBlackPlayer(): Player = players.first { it is BlackPlayer } + + fun getWhitePlayer(): Player = players.first { it is WhitePlayer } + + fun getWinner(): Player = players.first { it.isWin } + + fun canPlace(stone: Stone): Boolean = players.none { it.isPlaced(stone) } } diff --git a/src/main/kotlin/player/WhitePlayer.kt b/src/main/kotlin/player/WhitePlayer.kt index 1e9b51b29..a3854e08c 100644 --- a/src/main/kotlin/player/WhitePlayer.kt +++ b/src/main/kotlin/player/WhitePlayer.kt @@ -1,11 +1,9 @@ package player -import BoardState import Stone import state.PlayerState import state.PlayingState class WhitePlayer(state: PlayerState = PlayingState()) : Player(state) { - override val boardState: BoardState = BoardState.WHITE override fun putStone(stone: Stone): Player = WhitePlayer(state.add(stone)) } diff --git a/src/main/kotlin/state/PlayerState.kt b/src/main/kotlin/state/PlayerState.kt index 7cf1418a0..545b7c81b 100644 --- a/src/main/kotlin/state/PlayerState.kt +++ b/src/main/kotlin/state/PlayerState.kt @@ -10,5 +10,6 @@ abstract class PlayerState(protected val stones: Stones = Stones()) { fun hasStone(stone: Stone): Boolean = stones.hasStone(stone) fun getPlaced(): List = stones.getPositions() + fun getLastStone(): Stone = stones.lastStone } diff --git a/src/test/kotlin/BoardTest.kt b/src/test/kotlin/BoardTest.kt index 36ef6bcfa..4d8528a1c 100644 --- a/src/test/kotlin/BoardTest.kt +++ b/src/test/kotlin/BoardTest.kt @@ -11,7 +11,7 @@ class BoardTest { val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) val board = Board(blackPlayer, whitePlayer) - assertThat(board.putStone(Turn.BLACK, ONE_THREE_STONE)).isInstanceOf(Player::class.java) + assertThat(board.putStone(StoneColor.BLACK, ONE_THREE_STONE)).isInstanceOf(Player::class.java) } @Test @@ -19,6 +19,6 @@ class BoardTest { val blackPlayer: Player = BlackPlayer(PlayingState(Stones(ONE_ONE_STONE))) val whitePlayer: Player = WhitePlayer(PlayingState(Stones(ONE_TWO_STONE))) val board = Board(blackPlayer, whitePlayer) - assertThat(board.putStone(Turn.BLACK, ONE_TWO_STONE)).isNull() + assertThat(board.putStone(StoneColor.BLACK, ONE_TWO_STONE)).isNull() } } diff --git a/src/test/kotlin/OmokTest.kt b/src/test/kotlin/OmokTest.kt new file mode 100644 index 000000000..0390da31c --- /dev/null +++ b/src/test/kotlin/OmokTest.kt @@ -0,0 +1,9 @@ +import game.Omok +import org.junit.jupiter.api.Test + +class OmokTest { + @Test + fun `한 플레이어라도 승리할 때까지 차례를 번갈아가면서 돌을 놓는다`() { + val omok = Omok() + } +} diff --git a/src/test/kotlin/TurnTest.kt b/src/test/kotlin/StoneColorTest.kt similarity index 63% rename from src/test/kotlin/TurnTest.kt rename to src/test/kotlin/StoneColorTest.kt index 903774851..76a8d6029 100644 --- a/src/test/kotlin/TurnTest.kt +++ b/src/test/kotlin/StoneColorTest.kt @@ -2,11 +2,11 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource -class TurnTest { +class StoneColorTest { @CsvSource("WHITE, BLACK", "BLACK, WHITE") @ParameterizedTest - fun `상대방 차례를 반환한다`(myTurn: Turn, expected: Turn) { - val actual = myTurn.next() + fun `상대방 차례를 반환한다`(myStoneColor: StoneColor, expected: StoneColor) { + val actual = myStoneColor.next() assertThat(actual).isEqualTo(expected) } } From 28bb0cd59841850f221d69b1df1039481489369a Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 19:31:47 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=AA=A9=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EC=9E=85=EB=A0=A5,=20=EC=B6=9C=EB=A0=A5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/Application.kt | 5 ++ .../listener/OmokStartEndEventListener.kt | 12 +++ .../kotlin/listener/OmokTurnEventListener.kt | 9 ++ src/main/kotlin/view/BoardModel.kt | 12 +++ src/main/kotlin/view/ColorModel.kt | 13 +++ src/main/kotlin/view/InputView.kt | 42 ++++++++++ src/main/kotlin/view/OutputView.kt | 82 +++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 src/main/kotlin/Application.kt create mode 100644 src/main/kotlin/listener/OmokStartEndEventListener.kt create mode 100644 src/main/kotlin/listener/OmokTurnEventListener.kt create mode 100644 src/main/kotlin/view/BoardModel.kt create mode 100644 src/main/kotlin/view/ColorModel.kt create mode 100644 src/main/kotlin/view/InputView.kt create mode 100644 src/main/kotlin/view/OutputView.kt diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt new file mode 100644 index 000000000..685c1b54e --- /dev/null +++ b/src/main/kotlin/Application.kt @@ -0,0 +1,5 @@ +import controller.OmokController + +fun main() { + OmokController().start() +} diff --git a/src/main/kotlin/listener/OmokStartEndEventListener.kt b/src/main/kotlin/listener/OmokStartEndEventListener.kt new file mode 100644 index 000000000..832d90c54 --- /dev/null +++ b/src/main/kotlin/listener/OmokStartEndEventListener.kt @@ -0,0 +1,12 @@ +package listener + +import domain.Position +import domain.StoneColor +import domain.player.Players + +interface OmokStartEndEventListener { + fun onStartTurn(stoneColor: StoneColor, position: Position) + fun onEndTurn(players: Players) + fun onStartGame() + fun onEndGame(stoneColor: StoneColor) +} diff --git a/src/main/kotlin/listener/OmokTurnEventListener.kt b/src/main/kotlin/listener/OmokTurnEventListener.kt new file mode 100644 index 000000000..0e28afb1d --- /dev/null +++ b/src/main/kotlin/listener/OmokTurnEventListener.kt @@ -0,0 +1,9 @@ +package listener + +import domain.Position +import domain.StoneColor + +interface OmokTurnEventListener { + fun onTakeTurn(stoneColor: StoneColor): Position + fun onNotPlaceable() +} diff --git a/src/main/kotlin/view/BoardModel.kt b/src/main/kotlin/view/BoardModel.kt new file mode 100644 index 000000000..a9df6df8d --- /dev/null +++ b/src/main/kotlin/view/BoardModel.kt @@ -0,0 +1,12 @@ +package view + +import domain.Position + +object BoardModel { + private const val RANGE_MIN = 'A' + private const val RANGE_MAX = 'O' + + private val range = (RANGE_MIN..RANGE_MAX).toList() + fun getString(position: Position) = range[position.col] + (Position.POSITION_RANGE.max() - position.row).toString() + fun getColInt(col: String) = range.indexOf(col.toCharArray()[0]) + 1 +} diff --git a/src/main/kotlin/view/ColorModel.kt b/src/main/kotlin/view/ColorModel.kt new file mode 100644 index 000000000..e16ce17b7 --- /dev/null +++ b/src/main/kotlin/view/ColorModel.kt @@ -0,0 +1,13 @@ +package view + +import domain.StoneColor + +object ColorModel { + private const val BLACK = "흑" + private const val WHITE = "백" + + fun getString(color: StoneColor): String = when (color) { + StoneColor.BLACK -> BLACK + StoneColor.WHITE -> WHITE + } +} diff --git a/src/main/kotlin/view/InputView.kt b/src/main/kotlin/view/InputView.kt new file mode 100644 index 000000000..70c1c5234 --- /dev/null +++ b/src/main/kotlin/view/InputView.kt @@ -0,0 +1,42 @@ +package view + +import domain.Position +import domain.Position.Companion.POSITION_RANGE +import domain.StoneColor +import listener.OmokTurnEventListener + +class InputView : OmokTurnEventListener { + override fun onTakeTurn(stoneColor: StoneColor): Position = askPosition() + + override fun onNotPlaceable() { + reAskPosition(CANT_PLACE_STONE_ERROR_MESSAGE) + } + + private fun askPosition(): Position { + print(ASK_POSITION_MESSAGE) + val input = readln() + if (input.length !in POSITION_INPUT_RANGE) + return reAskPosition(INVALID_FORMAT_ERROR_MESSAGE) + + val col = BoardModel.getColInt(input.first().toString()) + val row = input.substring(ROW_INPUT_SIZE).toIntOrNull() + if (row == null || row !in POSITION_RANGE || col !in POSITION_RANGE) + return reAskPosition(INVALID_FORMAT_ERROR_MESSAGE) + return Position(row, col) + } + + private fun reAskPosition(message: String): Position { + println(message) + return askPosition() + } + + companion object { + private const val ASK_POSITION_MESSAGE = "위치를 입력하세요: " + private const val INVALID_FORMAT_ERROR_MESSAGE = "포맷에 맞지 않는 입력값입니다." + private const val CANT_PLACE_STONE_ERROR_MESSAGE = "해당 위치에는 오목알을 둘 수 없습니다." + private const val MIN_POSITION_INPUT_SIZE = 2 + private const val MAX_POSITION_INPUT_SIZE = 3 + private val POSITION_INPUT_RANGE = MIN_POSITION_INPUT_SIZE..MAX_POSITION_INPUT_SIZE + private const val ROW_INPUT_SIZE = 1 + } +} diff --git a/src/main/kotlin/view/OutputView.kt b/src/main/kotlin/view/OutputView.kt new file mode 100644 index 000000000..ed5609aa7 --- /dev/null +++ b/src/main/kotlin/view/OutputView.kt @@ -0,0 +1,82 @@ +package view + +import domain.Position +import domain.StoneColor +import domain.player.Players +import listener.OmokStartEndEventListener + +class OutputView : OmokStartEndEventListener { + override fun onStartGame() { + printStart() + } + + override fun onEndGame(stoneColor: StoneColor) { + printWinner(stoneColor) + } + + override fun onStartTurn(stoneColor: StoneColor, position: Position) { + printTurn(stoneColor, position) + } + + override fun onEndTurn(players: Players) { + printOmokBoard(players) + } + + private fun printStart() { + println(START_MESSAGE) + println(EMPTY_BOARD) + println(TURN_MESSAGE.format(ColorModel.getString(StoneColor.BLACK))) + } + + private fun printOmokBoard(players: Players) { + val board = EMPTY_BOARD.toMutableList() + + players.getBlackPlayer().getPositions().forEach { + board[calculateIndex(it)] = BLACK_STONE + } + players.getWhitePlayer().getPositions().forEach { + board[calculateIndex(it)] = WHITE_STONE + } + println(board.joinToString(separator = "")) + } + + private fun calculateIndex(position: Position): Int = 721 + 3 * position.col - 48 * position.row + + private fun printTurn(stoneColor: StoneColor, position: Position) { + print(TURN_MESSAGE.format(ColorModel.getString(stoneColor))) + println(LAST_STONE_POSITION_MESSAGE.format(BoardModel.getString(position))) + } + + private fun printWinner(stoneColor: StoneColor) { + println(GAME_END_MESSAGE) + println(WINNER_MESSAGE.format(ColorModel.getString(stoneColor))) + } + + companion object { + private const val START_MESSAGE = "오목 게임을 시작합니다." + private const val TURN_MESSAGE = "%s의 차례입니다. " + private const val LAST_STONE_POSITION_MESSAGE = "(마지막 돌의 위치: %s)" + private const val GAME_END_MESSAGE = "게임이 종료되었습니다." + private const val WINNER_MESSAGE = "%s의 승리입니다." + private val EMPTY_BOARD: String = """ + | 15 ┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐ + | 14 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 13 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 12 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 11 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 10 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 9 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 8 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 7 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 6 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 5 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 4 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 3 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 2 ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + | 1 └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ + | A B C D E F G H I J K L M N O + """.trimMargin() + private const val BLACK_STONE = '●' + private const val WHITE_STONE = '○' + } +} From f37f93b370975769f95dfdeebb6dddce1c9bd25f Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 19:32:34 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat:=20=EC=98=A4=EB=AA=A9=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/controller/OmokController.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/kotlin/controller/OmokController.kt diff --git a/src/main/kotlin/controller/OmokController.kt b/src/main/kotlin/controller/OmokController.kt new file mode 100644 index 000000000..65cec50dd --- /dev/null +++ b/src/main/kotlin/controller/OmokController.kt @@ -0,0 +1,11 @@ +package controller + +import domain.game.Omok +import view.InputView +import view.OutputView + +class OmokController { + fun start() { + Omok(OutputView(), InputView()).run() + } +} From b1fcb5bec46238b245fac02062ce0e596487fb07 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Wed, 15 Mar 2023 19:33:17 +0900 Subject: [PATCH 15/18] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/{ => domain/board}/Board.kt | 13 ++++--- src/main/kotlin/domain/game/Omok.kt | 36 +++++++++++++++++++ .../kotlin/{ => domain}/player/BlackPlayer.kt | 8 ++--- src/main/kotlin/{ => domain}/player/Player.kt | 10 +++--- .../kotlin/{ => domain}/player/Players.kt | 8 ++--- .../kotlin/{ => domain}/player/WhitePlayer.kt | 8 ++--- .../kotlin/{ => domain/position}/Position.kt | 2 ++ .../kotlin/{ => domain}/state/PlayerState.kt | 8 ++--- .../kotlin/{ => domain}/state/PlayingState.kt | 6 ++-- .../kotlin/{ => domain}/state/WinState.kt | 6 ++-- src/main/kotlin/{ => domain/stone}/Stone.kt | 5 ++- .../kotlin/{ => domain/stone}/StoneColor.kt | 2 ++ src/main/kotlin/{ => domain/stone}/Stones.kt | 12 +++++-- src/main/kotlin/game/Omok.kt | 30 ---------------- src/main/kotlin/listener/OmokEventListener.kt | 13 ------- .../listener/OmokStartEndEventListener.kt | 4 +-- .../kotlin/listener/OmokTurnEventListener.kt | 4 +-- src/main/kotlin/view/InputView.kt | 7 ++-- src/main/kotlin/view/OutputView.kt | 6 ++-- .../kotlin/view/{ => model}/BoardModel.kt | 4 +-- .../kotlin/view/{ => model}/ColorModel.kt | 4 +-- src/test/kotlin/BlackPlayerTest.kt | 7 ++-- src/test/kotlin/BoardTest.kt | 11 +++--- src/test/kotlin/Fixtures.kt | 2 ++ src/test/kotlin/OmokTest.kt | 4 +-- src/test/kotlin/PlayingStateTest.kt | 5 +-- src/test/kotlin/PositionTest.kt | 1 + src/test/kotlin/StoneColorTest.kt | 1 + src/test/kotlin/StoneTest.kt | 2 +- src/test/kotlin/StonesTest.kt | 1 + src/test/kotlin/WhitePlayerTest.kt | 7 ++-- 31 files changed, 127 insertions(+), 110 deletions(-) rename src/main/kotlin/{ => domain/board}/Board.kt (63%) create mode 100644 src/main/kotlin/domain/game/Omok.kt rename src/main/kotlin/{ => domain}/player/BlackPlayer.kt (58%) rename src/main/kotlin/{ => domain}/player/Player.kt (76%) rename src/main/kotlin/{ => domain}/player/Players.kt (88%) rename src/main/kotlin/{ => domain}/player/WhitePlayer.kt (58%) rename src/main/kotlin/{ => domain/position}/Position.kt (96%) rename src/main/kotlin/{ => domain}/state/PlayerState.kt (74%) rename src/main/kotlin/{ => domain}/state/PlayingState.kt (79%) rename src/main/kotlin/{ => domain}/state/WinState.kt (61%) rename src/main/kotlin/{ => domain/stone}/Stone.kt (76%) rename src/main/kotlin/{ => domain/stone}/StoneColor.kt (86%) rename src/main/kotlin/{ => domain/stone}/Stones.kt (86%) delete mode 100644 src/main/kotlin/game/Omok.kt delete mode 100644 src/main/kotlin/listener/OmokEventListener.kt rename src/main/kotlin/view/{ => model}/BoardModel.kt (87%) rename src/main/kotlin/view/{ => model}/ColorModel.kt (82%) diff --git a/src/main/kotlin/Board.kt b/src/main/kotlin/domain/board/Board.kt similarity index 63% rename from src/main/kotlin/Board.kt rename to src/main/kotlin/domain/board/Board.kt index 64bd804d1..ee1dbab35 100644 --- a/src/main/kotlin/Board.kt +++ b/src/main/kotlin/domain/board/Board.kt @@ -1,5 +1,9 @@ -import player.Player -import player.Players +package domain.board + +import domain.player.Player +import domain.player.Players +import domain.stone.Stone +import domain.stone.StoneColor class Board(private val players: Players) { constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) @@ -14,9 +18,4 @@ class Board(private val players: Players) { fun getPlayers(): Players = players.copy() fun isRunning(): Boolean = players.isRunning - - fun getWinner(): Player = players.getWinner() - - // private fun makeEmptyBoard(): List> = - // List(POSITION_RANGE.max()) { MutableList(POSITION_RANGE.max()) { BoardState.EMPTY } } } diff --git a/src/main/kotlin/domain/game/Omok.kt b/src/main/kotlin/domain/game/Omok.kt new file mode 100644 index 000000000..62fc5f1b1 --- /dev/null +++ b/src/main/kotlin/domain/game/Omok.kt @@ -0,0 +1,36 @@ +package domain.game + +import domain.board.Board +import domain.player.BlackPlayer +import domain.player.WhitePlayer +import domain.stone.Stone +import domain.stone.StoneColor +import listener.OmokStartEndEventListener +import listener.OmokTurnEventListener + +class Omok( + private val startEndEventListener: OmokStartEndEventListener, + private val turnEventListener: OmokTurnEventListener +) { + fun run() { + startEndEventListener.onStartGame() + var curStoneColor: StoneColor = StoneColor.BLACK + var curBoard = Board(BlackPlayer(), WhitePlayer()) + do { + curBoard = takeTurn(curBoard, curStoneColor) + startEndEventListener.onEndTurn(curBoard.getPlayers()) + curStoneColor = curStoneColor.next() + } while (curBoard.isRunning()) + startEndEventListener.onEndGame(curStoneColor.next()) + } + + private fun takeTurn(board: Board, stoneColor: StoneColor): Board { + val newStone = Stone.of(turnEventListener.onTakeTurn(stoneColor)) + val newBoard = board.putStone(stoneColor, newStone) + if (newBoard == null) { + turnEventListener.onNotPlaceable() + return takeTurn(board, stoneColor) + } + return newBoard + } +} diff --git a/src/main/kotlin/player/BlackPlayer.kt b/src/main/kotlin/domain/player/BlackPlayer.kt similarity index 58% rename from src/main/kotlin/player/BlackPlayer.kt rename to src/main/kotlin/domain/player/BlackPlayer.kt index acfb9034b..2a91eea77 100644 --- a/src/main/kotlin/player/BlackPlayer.kt +++ b/src/main/kotlin/domain/player/BlackPlayer.kt @@ -1,8 +1,8 @@ -package player +package domain.player -import Stone -import state.PlayerState -import state.PlayingState +import domain.state.PlayerState +import domain.state.PlayingState +import domain.stone.Stone class BlackPlayer(state: PlayerState = PlayingState()) : Player(state) { override fun putStone(stone: Stone): Player = BlackPlayer(state.add(stone)) diff --git a/src/main/kotlin/player/Player.kt b/src/main/kotlin/domain/player/Player.kt similarity index 76% rename from src/main/kotlin/player/Player.kt rename to src/main/kotlin/domain/player/Player.kt index dd883e6db..02142bdb4 100644 --- a/src/main/kotlin/player/Player.kt +++ b/src/main/kotlin/domain/player/Player.kt @@ -1,9 +1,9 @@ -package player +package domain.player -import Position -import Stone -import state.PlayerState -import state.WinState +import domain.position.Position +import domain.state.PlayerState +import domain.state.WinState +import domain.stone.Stone abstract class Player(protected val state: PlayerState) : Cloneable { val isWin: Boolean = state is WinState diff --git a/src/main/kotlin/player/Players.kt b/src/main/kotlin/domain/player/Players.kt similarity index 88% rename from src/main/kotlin/player/Players.kt rename to src/main/kotlin/domain/player/Players.kt index d62a920b7..2648f38ef 100644 --- a/src/main/kotlin/player/Players.kt +++ b/src/main/kotlin/domain/player/Players.kt @@ -1,7 +1,7 @@ -package player +package domain.player -import Stone -import StoneColor +import domain.stone.Stone +import domain.stone.StoneColor data class Players private constructor(private val players: List) { val isRunning: Boolean @@ -20,7 +20,5 @@ data class Players private constructor(private val players: List) { fun getWhitePlayer(): Player = players.first { it is WhitePlayer } - fun getWinner(): Player = players.first { it.isWin } - fun canPlace(stone: Stone): Boolean = players.none { it.isPlaced(stone) } } diff --git a/src/main/kotlin/player/WhitePlayer.kt b/src/main/kotlin/domain/player/WhitePlayer.kt similarity index 58% rename from src/main/kotlin/player/WhitePlayer.kt rename to src/main/kotlin/domain/player/WhitePlayer.kt index a3854e08c..32f2472e0 100644 --- a/src/main/kotlin/player/WhitePlayer.kt +++ b/src/main/kotlin/domain/player/WhitePlayer.kt @@ -1,8 +1,8 @@ -package player +package domain.player -import Stone -import state.PlayerState -import state.PlayingState +import domain.state.PlayerState +import domain.state.PlayingState +import domain.stone.Stone class WhitePlayer(state: PlayerState = PlayingState()) : Player(state) { override fun putStone(stone: Stone): Player = WhitePlayer(state.add(stone)) diff --git a/src/main/kotlin/Position.kt b/src/main/kotlin/domain/position/Position.kt similarity index 96% rename from src/main/kotlin/Position.kt rename to src/main/kotlin/domain/position/Position.kt index 786c47bca..f13fefd6b 100644 --- a/src/main/kotlin/Position.kt +++ b/src/main/kotlin/domain/position/Position.kt @@ -1,3 +1,5 @@ +package domain.position + data class Position(val row: Int, val col: Int) { init { require(row in POSITION_RANGE) { ROW_OUT_OF_RANGE_ERROR_MESSAGE } diff --git a/src/main/kotlin/state/PlayerState.kt b/src/main/kotlin/domain/state/PlayerState.kt similarity index 74% rename from src/main/kotlin/state/PlayerState.kt rename to src/main/kotlin/domain/state/PlayerState.kt index 545b7c81b..076a2c349 100644 --- a/src/main/kotlin/state/PlayerState.kt +++ b/src/main/kotlin/domain/state/PlayerState.kt @@ -1,8 +1,8 @@ -package state +package domain.state -import Position -import Stone -import Stones +import domain.position.Position +import domain.stone.Stone +import domain.stone.Stones abstract class PlayerState(protected val stones: Stones = Stones()) { abstract fun add(newStone: Stone): PlayerState diff --git a/src/main/kotlin/state/PlayingState.kt b/src/main/kotlin/domain/state/PlayingState.kt similarity index 79% rename from src/main/kotlin/state/PlayingState.kt rename to src/main/kotlin/domain/state/PlayingState.kt index de2219d03..0f131e568 100644 --- a/src/main/kotlin/state/PlayingState.kt +++ b/src/main/kotlin/domain/state/PlayingState.kt @@ -1,7 +1,7 @@ -package state +package domain.state -import Stone -import Stones +import domain.stone.Stone +import domain.stone.Stones class PlayingState(stones: Stones = Stones()) : PlayerState(stones) { override fun add(newStone: Stone): PlayerState { diff --git a/src/main/kotlin/state/WinState.kt b/src/main/kotlin/domain/state/WinState.kt similarity index 61% rename from src/main/kotlin/state/WinState.kt rename to src/main/kotlin/domain/state/WinState.kt index a4309788f..7350f2c69 100644 --- a/src/main/kotlin/state/WinState.kt +++ b/src/main/kotlin/domain/state/WinState.kt @@ -1,7 +1,7 @@ -package state +package domain.state -import Stone -import Stones +import domain.stone.Stone +import domain.stone.Stones class WinState(stones: Stones) : PlayerState(stones) { override fun add(newStone: Stone): PlayerState = this diff --git a/src/main/kotlin/Stone.kt b/src/main/kotlin/domain/stone/Stone.kt similarity index 76% rename from src/main/kotlin/Stone.kt rename to src/main/kotlin/domain/stone/Stone.kt index f162e1da4..f553c1565 100644 --- a/src/main/kotlin/Stone.kt +++ b/src/main/kotlin/domain/stone/Stone.kt @@ -1,4 +1,7 @@ -import Position.Companion.POSITION_RANGE +package domain.stone + +import domain.position.Position +import domain.position.Position.Companion.POSITION_RANGE data class Stone private constructor(val position: Position) { companion object { diff --git a/src/main/kotlin/StoneColor.kt b/src/main/kotlin/domain/stone/StoneColor.kt similarity index 86% rename from src/main/kotlin/StoneColor.kt rename to src/main/kotlin/domain/stone/StoneColor.kt index e71aa36a0..a3196d25a 100644 --- a/src/main/kotlin/StoneColor.kt +++ b/src/main/kotlin/domain/stone/StoneColor.kt @@ -1,3 +1,5 @@ +package domain.stone + enum class StoneColor { BLACK, WHITE; diff --git a/src/main/kotlin/Stones.kt b/src/main/kotlin/domain/stone/Stones.kt similarity index 86% rename from src/main/kotlin/Stones.kt rename to src/main/kotlin/domain/stone/Stones.kt index 718387a2b..36c439002 100644 --- a/src/main/kotlin/Stones.kt +++ b/src/main/kotlin/domain/stone/Stones.kt @@ -1,6 +1,11 @@ +package domain.stone + +import domain.position.Position + data class Stones(private val stones: List) { val lastStone: Stone get() = stones.last().copy() + constructor(vararg stones: Stone) : this(stones.toList()) init { @@ -31,15 +36,16 @@ data class Stones(private val stones: List) { val (startX, startY) = Pair(startPosition.row, startPosition.col) var count = 1 var (currentX, currentY) = Pair(startX + direction.first * weight, startY + direction.second * weight) - while (currentX in Position.POSITION_RANGE && currentY in Position.POSITION_RANGE && hasStone(Stone.of(currentX, currentY))) { + while (inRange(currentX, currentY) && hasStone(Stone.of(currentX, currentY))) { count++ currentX += direction.first * weight currentY += direction.second * weight } - if (count >= MINIMUM_WIN_CONDITION) return true - return false + return count >= MINIMUM_WIN_CONDITION } + private fun inRange(x: Int, y: Int) = x in Position.POSITION_RANGE && y in Position.POSITION_RANGE + companion object { private const val DUPLICATED_ERROR_MESSAGE = "중복되는 위치의 오목알을 가질 수 없습니다." private const val MINIMUM_WIN_CONDITION = 5 diff --git a/src/main/kotlin/game/Omok.kt b/src/main/kotlin/game/Omok.kt deleted file mode 100644 index bf5e55dba..000000000 --- a/src/main/kotlin/game/Omok.kt +++ /dev/null @@ -1,30 +0,0 @@ -package game - -import Board -import Stone -import StoneColor -import listener.OmokEventListener -import player.BlackPlayer -import player.WhitePlayer - -class Omok(private val eventListener: OmokEventListener) { - private val board = Board(BlackPlayer(), WhitePlayer()) - - fun run() { - eventListener.onStartGame() - var currentStoneColor: StoneColor = StoneColor.BLACK - var currentBoard: Board = board - do { - currentBoard = takeTurn(currentBoard, currentStoneColor) - eventListener.onEndTurn(currentBoard.getPlayers()) - currentStoneColor = currentStoneColor.next() - } while (currentBoard.isRunning()) - - eventListener.onEndGame(board.getWinner()) - } - - private fun takeTurn(board: Board, stoneColor: StoneColor): Board { - val newStone = Stone.of(eventListener.onTakeTurn(stoneColor)) - return board.putStone(stoneColor, newStone) ?: return takeTurn(board, stoneColor) - } -} diff --git a/src/main/kotlin/listener/OmokEventListener.kt b/src/main/kotlin/listener/OmokEventListener.kt deleted file mode 100644 index 2d198da8e..000000000 --- a/src/main/kotlin/listener/OmokEventListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package listener - -import Position -import StoneColor -import player.Player -import player.Players - -interface OmokEventListener { - fun onStartGame() - fun onTakeTurn(stoneColor: StoneColor): Position - fun onEndTurn(positions: Players) - fun onEndGame(player: Player) -} diff --git a/src/main/kotlin/listener/OmokStartEndEventListener.kt b/src/main/kotlin/listener/OmokStartEndEventListener.kt index 832d90c54..e337a01c8 100644 --- a/src/main/kotlin/listener/OmokStartEndEventListener.kt +++ b/src/main/kotlin/listener/OmokStartEndEventListener.kt @@ -1,8 +1,8 @@ package listener -import domain.Position -import domain.StoneColor import domain.player.Players +import domain.position.Position +import domain.stone.StoneColor interface OmokStartEndEventListener { fun onStartTurn(stoneColor: StoneColor, position: Position) diff --git a/src/main/kotlin/listener/OmokTurnEventListener.kt b/src/main/kotlin/listener/OmokTurnEventListener.kt index 0e28afb1d..d24c9367c 100644 --- a/src/main/kotlin/listener/OmokTurnEventListener.kt +++ b/src/main/kotlin/listener/OmokTurnEventListener.kt @@ -1,7 +1,7 @@ package listener -import domain.Position -import domain.StoneColor +import domain.position.Position +import domain.stone.StoneColor interface OmokTurnEventListener { fun onTakeTurn(stoneColor: StoneColor): Position diff --git a/src/main/kotlin/view/InputView.kt b/src/main/kotlin/view/InputView.kt index 70c1c5234..cc2869acd 100644 --- a/src/main/kotlin/view/InputView.kt +++ b/src/main/kotlin/view/InputView.kt @@ -1,9 +1,10 @@ package view -import domain.Position -import domain.Position.Companion.POSITION_RANGE -import domain.StoneColor +import domain.position.Position +import domain.position.Position.Companion.POSITION_RANGE +import domain.stone.StoneColor import listener.OmokTurnEventListener +import view.model.BoardModel class InputView : OmokTurnEventListener { override fun onTakeTurn(stoneColor: StoneColor): Position = askPosition() diff --git a/src/main/kotlin/view/OutputView.kt b/src/main/kotlin/view/OutputView.kt index ed5609aa7..d8ff82316 100644 --- a/src/main/kotlin/view/OutputView.kt +++ b/src/main/kotlin/view/OutputView.kt @@ -1,9 +1,11 @@ package view -import domain.Position -import domain.StoneColor import domain.player.Players +import domain.position.Position +import domain.stone.StoneColor import listener.OmokStartEndEventListener +import view.model.BoardModel +import view.model.ColorModel class OutputView : OmokStartEndEventListener { override fun onStartGame() { diff --git a/src/main/kotlin/view/BoardModel.kt b/src/main/kotlin/view/model/BoardModel.kt similarity index 87% rename from src/main/kotlin/view/BoardModel.kt rename to src/main/kotlin/view/model/BoardModel.kt index a9df6df8d..ebeb87097 100644 --- a/src/main/kotlin/view/BoardModel.kt +++ b/src/main/kotlin/view/model/BoardModel.kt @@ -1,6 +1,6 @@ -package view +package view.model -import domain.Position +import domain.position.Position object BoardModel { private const val RANGE_MIN = 'A' diff --git a/src/main/kotlin/view/ColorModel.kt b/src/main/kotlin/view/model/ColorModel.kt similarity index 82% rename from src/main/kotlin/view/ColorModel.kt rename to src/main/kotlin/view/model/ColorModel.kt index e16ce17b7..c22330c44 100644 --- a/src/main/kotlin/view/ColorModel.kt +++ b/src/main/kotlin/view/model/ColorModel.kt @@ -1,6 +1,6 @@ -package view +package view.model -import domain.StoneColor +import domain.stone.StoneColor object ColorModel { private const val BLACK = "흑" diff --git a/src/test/kotlin/BlackPlayerTest.kt b/src/test/kotlin/BlackPlayerTest.kt index e15269617..5cc163994 100644 --- a/src/test/kotlin/BlackPlayerTest.kt +++ b/src/test/kotlin/BlackPlayerTest.kt @@ -1,8 +1,9 @@ +import domain.player.BlackPlayer +import domain.player.Player +import domain.state.PlayingState +import domain.stone.Stones import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import player.BlackPlayer -import player.Player -import state.PlayingState class BlackPlayerTest { @Test diff --git a/src/test/kotlin/BoardTest.kt b/src/test/kotlin/BoardTest.kt index 4d8528a1c..1b993d064 100644 --- a/src/test/kotlin/BoardTest.kt +++ b/src/test/kotlin/BoardTest.kt @@ -1,9 +1,12 @@ +import domain.board.Board +import domain.player.BlackPlayer +import domain.player.Player +import domain.player.WhitePlayer +import domain.state.PlayingState +import domain.stone.StoneColor +import domain.stone.Stones import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import player.BlackPlayer -import player.Player -import player.WhitePlayer -import state.PlayingState class BoardTest { @Test diff --git a/src/test/kotlin/Fixtures.kt b/src/test/kotlin/Fixtures.kt index 158e5d154..9968a9624 100644 --- a/src/test/kotlin/Fixtures.kt +++ b/src/test/kotlin/Fixtures.kt @@ -1,3 +1,5 @@ +import domain.stone.Stone + val ONE_ONE_STONE = Stone.of(1, 1) val ONE_TWO_STONE = Stone.of(1, 2) val ONE_THREE_STONE = Stone.of(1, 3) diff --git a/src/test/kotlin/OmokTest.kt b/src/test/kotlin/OmokTest.kt index 0390da31c..5043dc04b 100644 --- a/src/test/kotlin/OmokTest.kt +++ b/src/test/kotlin/OmokTest.kt @@ -1,9 +1,9 @@ -import game.Omok + import org.junit.jupiter.api.Test class OmokTest { @Test fun `한 플레이어라도 승리할 때까지 차례를 번갈아가면서 돌을 놓는다`() { - val omok = Omok() + // val omok = Omok() } } diff --git a/src/test/kotlin/PlayingStateTest.kt b/src/test/kotlin/PlayingStateTest.kt index 2c94ea051..b6fde68c6 100644 --- a/src/test/kotlin/PlayingStateTest.kt +++ b/src/test/kotlin/PlayingStateTest.kt @@ -1,7 +1,8 @@ +import domain.state.PlayingState +import domain.state.WinState +import domain.stone.Stones import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import state.PlayingState -import state.WinState class PlayingStateTest { diff --git a/src/test/kotlin/PositionTest.kt b/src/test/kotlin/PositionTest.kt index c6439897a..4a661c35e 100644 --- a/src/test/kotlin/PositionTest.kt +++ b/src/test/kotlin/PositionTest.kt @@ -1,3 +1,4 @@ +import domain.position.Position import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll diff --git a/src/test/kotlin/StoneColorTest.kt b/src/test/kotlin/StoneColorTest.kt index 76a8d6029..3f4a542b1 100644 --- a/src/test/kotlin/StoneColorTest.kt +++ b/src/test/kotlin/StoneColorTest.kt @@ -1,3 +1,4 @@ +import domain.stone.StoneColor import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource diff --git a/src/test/kotlin/StoneTest.kt b/src/test/kotlin/StoneTest.kt index 6a52f944f..2aa0ff970 100644 --- a/src/test/kotlin/StoneTest.kt +++ b/src/test/kotlin/StoneTest.kt @@ -3,7 +3,7 @@ class StoneTest { // @Test // fun `오목알은 자신의 위치를 알고 있다`() { - // val stone = Stone("H", 10) + // val stone = domain.stone.Stone("H", 10) // assertThat(stone.x).isEqualTo("H") // assertThat(stone.y).isEqualTo(10) // } diff --git a/src/test/kotlin/StonesTest.kt b/src/test/kotlin/StonesTest.kt index 7d55ea0de..d0629049f 100644 --- a/src/test/kotlin/StonesTest.kt +++ b/src/test/kotlin/StonesTest.kt @@ -1,3 +1,4 @@ +import domain.stone.Stones import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow diff --git a/src/test/kotlin/WhitePlayerTest.kt b/src/test/kotlin/WhitePlayerTest.kt index fe6eca79e..ffcc9d82e 100644 --- a/src/test/kotlin/WhitePlayerTest.kt +++ b/src/test/kotlin/WhitePlayerTest.kt @@ -1,8 +1,9 @@ +import domain.player.Player +import domain.player.WhitePlayer +import domain.state.PlayingState +import domain.stone.Stones import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import player.Player -import player.WhitePlayer -import state.PlayingState class WhitePlayerTest { @Test From 11b26661829724d658c632879fe3c6c5f8c1197c Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Thu, 16 Mar 2023 11:59:51 +0900 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20=EC=A4=91=EA=B0=84=EC=97=90=20?= =?UTF-8?q?=EC=98=A4=EB=AA=A9=EC=95=8C=EC=9D=84=20=EB=92=80=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EC=8A=B9=EB=A6=AC=20=ED=8C=90=EC=A0=95=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/domain/stone/Stones.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/domain/stone/Stones.kt b/src/main/kotlin/domain/stone/Stones.kt index 36c439002..df1147fe0 100644 --- a/src/main/kotlin/domain/stone/Stones.kt +++ b/src/main/kotlin/domain/stone/Stones.kt @@ -22,8 +22,9 @@ data class Stones(private val stones: List) { val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) for (moveDirection in directions) { - if (checkStraight(startStone.position, moveDirection, FORWARD_WEIGHT)) return true - if (checkStraight(startStone.position, moveDirection, BACK_WEIGHT)) return true + val forwardCount = checkStraight(startStone.position, moveDirection, FORWARD_WEIGHT) + val backCount = checkStraight(startStone.position, moveDirection, BACK_WEIGHT) + if (forwardCount + backCount - 1 >= MINIMUM_WIN_CONDITION) return true } return false } @@ -32,7 +33,7 @@ data class Stones(private val stones: List) { startPosition: Position, direction: Pair, weight: Int = FORWARD_WEIGHT - ): Boolean { + ): Int { val (startX, startY) = Pair(startPosition.row, startPosition.col) var count = 1 var (currentX, currentY) = Pair(startX + direction.first * weight, startY + direction.second * weight) @@ -41,7 +42,7 @@ data class Stones(private val stones: List) { currentX += direction.first * weight currentY += direction.second * weight } - return count >= MINIMUM_WIN_CONDITION + return count } private fun inRange(x: Int, y: Int) = x in Position.POSITION_RANGE && y in Position.POSITION_RANGE From a0166e090c15e9a7251daf12a7c85cf6ddcce3a9 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Thu, 16 Mar 2023 15:39:00 +0900 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=EB=A0=8C=EC=A3=BC=EB=A3=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(=EB=AF=B8=EC=99=84?= =?UTF-8?q?=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/domain/player/BlackPlayer.kt | 11 +- src/main/kotlin/domain/player/Player.kt | 5 +- src/main/kotlin/domain/player/Players.kt | 4 +- src/main/kotlin/domain/player/WhitePlayer.kt | 3 +- src/main/kotlin/domain/rule/OmokRule.kt | 8 + src/main/kotlin/domain/rule/RenjuRule.kt | 154 +++++++++++++++++++ src/main/kotlin/domain/state/PlayerState.kt | 2 + 7 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/domain/rule/OmokRule.kt create mode 100644 src/main/kotlin/domain/rule/RenjuRule.kt diff --git a/src/main/kotlin/domain/player/BlackPlayer.kt b/src/main/kotlin/domain/player/BlackPlayer.kt index 2a91eea77..8f08c5577 100644 --- a/src/main/kotlin/domain/player/BlackPlayer.kt +++ b/src/main/kotlin/domain/player/BlackPlayer.kt @@ -1,9 +1,18 @@ package domain.player +import domain.rule.RenjuRule import domain.state.PlayerState import domain.state.PlayingState import domain.stone.Stone +import domain.stone.Stones class BlackPlayer(state: PlayerState = PlayingState()) : Player(state) { - override fun putStone(stone: Stone): Player = BlackPlayer(state.add(stone)) + override fun putStone(stone: Stone, otherStones: Stones): Player { + val blackStones = state.getAllStones() + if (!RenjuRule().check(blackStones, otherStones, stone)) { + return BlackPlayer(state.add(stone)) + } else { + throw IllegalStateException("3-3입니다!") + } + } } diff --git a/src/main/kotlin/domain/player/Player.kt b/src/main/kotlin/domain/player/Player.kt index 02142bdb4..ef857d644 100644 --- a/src/main/kotlin/domain/player/Player.kt +++ b/src/main/kotlin/domain/player/Player.kt @@ -4,11 +4,12 @@ import domain.position.Position import domain.state.PlayerState import domain.state.WinState import domain.stone.Stone +import domain.stone.Stones abstract class Player(protected val state: PlayerState) : Cloneable { val isWin: Boolean = state is WinState - abstract fun putStone(stone: Stone): Player + abstract fun putStone(stone: Stone, otherStones: Stones): Player fun canPlace(): Boolean = state !is WinState @@ -18,5 +19,7 @@ abstract class Player(protected val state: PlayerState) : Cloneable { fun getLastStone(): Stone = state.getLastStone() + fun getAllStones(): Stones = state.getAllStones() + public override fun clone(): Player = super.clone() as Player } diff --git a/src/main/kotlin/domain/player/Players.kt b/src/main/kotlin/domain/player/Players.kt index 2648f38ef..8318d200c 100644 --- a/src/main/kotlin/domain/player/Players.kt +++ b/src/main/kotlin/domain/player/Players.kt @@ -11,9 +11,9 @@ data class Players private constructor(private val players: List) { fun putStone(stoneColor: StoneColor, stone: Stone): Players { if (stoneColor == StoneColor.BLACK) { - return Players(blackPlayer = getBlackPlayer().putStone(stone), whitePlayer = getWhitePlayer()) + return Players(blackPlayer = getBlackPlayer().putStone(stone, getWhitePlayer().getAllStones()), whitePlayer = getWhitePlayer()) } - return Players(blackPlayer = getBlackPlayer(), whitePlayer = getWhitePlayer().putStone(stone)) + return Players(blackPlayer = getBlackPlayer(), whitePlayer = getWhitePlayer().putStone(stone, getBlackPlayer().getAllStones())) } fun getBlackPlayer(): Player = players.first { it is BlackPlayer } diff --git a/src/main/kotlin/domain/player/WhitePlayer.kt b/src/main/kotlin/domain/player/WhitePlayer.kt index 32f2472e0..d62cd8bad 100644 --- a/src/main/kotlin/domain/player/WhitePlayer.kt +++ b/src/main/kotlin/domain/player/WhitePlayer.kt @@ -3,7 +3,8 @@ package domain.player import domain.state.PlayerState import domain.state.PlayingState import domain.stone.Stone +import domain.stone.Stones class WhitePlayer(state: PlayerState = PlayingState()) : Player(state) { - override fun putStone(stone: Stone): Player = WhitePlayer(state.add(stone)) + override fun putStone(stone: Stone, otherStones: Stones): Player = WhitePlayer(state.add(stone)) } diff --git a/src/main/kotlin/domain/rule/OmokRule.kt b/src/main/kotlin/domain/rule/OmokRule.kt new file mode 100644 index 000000000..142b4b184 --- /dev/null +++ b/src/main/kotlin/domain/rule/OmokRule.kt @@ -0,0 +1,8 @@ +package domain.rule + +import domain.stone.Stone +import domain.stone.Stones + +interface OmokRule { + fun check(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean +} diff --git a/src/main/kotlin/domain/rule/RenjuRule.kt b/src/main/kotlin/domain/rule/RenjuRule.kt new file mode 100644 index 000000000..a5aa40ef7 --- /dev/null +++ b/src/main/kotlin/domain/rule/RenjuRule.kt @@ -0,0 +1,154 @@ +package domain.rule + +import domain.position.Position +import domain.stone.Stone +import domain.stone.Stones + +class RenjuRule : OmokRule { + private var threeCount: Int = 0 + + override fun check(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + return check33(blackStones, whiteStones, startStone) + } + + private fun check33(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + return checkAllDirections(blackStones, whiteStones, startStone) + } + + private fun checkAllDirections(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) + + for (moveDirection in directions) { + val (forwardCount, forwardEmptyCount) = + findThree(blackStones, whiteStones, startStone.position, moveDirection, FORWARD_WEIGHT) + val (backCount, backEmptyCount) = + findThree(blackStones, whiteStones, startStone.position, moveDirection, BACK_WEIGHT) + + // 만약 빈 칸이 2 미만이고, 같은 돌 개수가 무조건 3이면 3-3 가능성 ok + // 현재 방향과 3인지 여부를 확인한다. + println(moveDirection) + println("forwardCount : $forwardCount backCount : $backCount") + println("forwardEmptyCount : $forwardEmptyCount backEmptyCount : $backEmptyCount") + if (forwardCount + backCount - 1 == 3 && forwardEmptyCount + backEmptyCount <= 1) { + val (upDownDir, leftRightDir) = moveDirection + + // 백돌 양쪽 합 6칸 이내에 2개 이상 있는지 확인한다. + // 닫혀 있으면 다른 방향 확인 + println("ㅇㅇㅇ") + if (!isBlockedByWhiteStoneInSix(whiteStones, startStone.position.row, startStone.position.col, upDownDir, leftRightDir)) { + threeCount++ + println("ㅇㅇㅇ2") + } + if (threeCount == 2) { + // 금수 + println("금수!!") + return true + } + } + } + return false + } + + private fun isBlockedByWhiteStoneInSix( + whiteStones: Stones, + row: Int, + col: Int, + upDownDir: Int, + leftRightDir: Int + ): Boolean { + val (oneDirMoveCount, oneDirFound) = checkWhite( + whiteStones, + row + upDownDir, + col + leftRightDir, + upDownDir, + leftRightDir + ) + val (otherDirMoveCount, otherDirFound) = checkWhite( + whiteStones, + row - upDownDir, + col - leftRightDir, + upDownDir, + leftRightDir + ) + + // 양 방향 6칸 이하에 각각 1개씩 있으면 참 + if (oneDirMoveCount + otherDirMoveCount <= 6 && oneDirFound && otherDirFound) { + return true + } + return false + } + + private fun checkWhite( + whiteStones: Stones, + row: Int, + col: Int, + upDownDir: Int, + leftRightDir: Int + ): Pair { + var (curRow, curCol) = Pair(row, col) + var moveCount = 1 + while (!whiteStones.hasStone(Stone.of(curRow, curCol)) && moveCount <= 6) { + curRow += upDownDir + curCol += leftRightDir + moveCount++ + if (whiteStones.hasStone(Stone.of(curRow, curCol))) { + return Pair(moveCount, true) + } + } + return Pair(moveCount, false) + } + + private fun findThree( + blackStones: Stones, + whiteStones: Stones, + startPosition: Position, + direction: Pair, + weight: Int = FORWARD_WEIGHT + ): Pair { + val (startRow, startCol) = Pair(startPosition.row, startPosition.col) + var sameStoneCount = 1 + var emptyCount = 0 + var (currentRow, currentCol) = Pair(startRow + direction.first * weight, startCol + direction.second * weight) + + // 현재 탐색 방향에 + // 흰 돌이 아니고, 범위 안에 있고 + // 같은 돌의 개수가 3개 이하이고, 공백이 1개 이하일 때까지 + while (inRange(currentRow, currentCol) && emptyCount <= 1 && + sameStoneCount <= 3 && !whiteStones.hasStone(Stone.of(currentRow, currentCol)) + ) { + // 검은 돌이 있는지 확인한다. + if (blackStones.hasStone(Stone.of(currentRow, currentCol))) { + ++sameStoneCount + } + // 빈 칸인지 확인한다. + if (!blackStones.hasStone(Stone.of(currentRow, currentCol)) && + !whiteStones.hasStone(Stone.of(currentRow, currentCol)) + ) { + if (sameStoneCount == 3) break + ++emptyCount + } + currentRow += direction.first * weight + currentCol += direction.second * weight + } + + if (sameStoneCount == 1) emptyCount = 0 + + // println("현재 방향 : ${direction.first * weight} ${direction.second * weight}") + // println("검은 돌 : $sameStoneCount / 빈 칸 : $emptyCount") + + return Pair(sameStoneCount, emptyCount) + } + + private fun inRange(x: Int, y: Int) = x in Position.POSITION_RANGE && y in Position.POSITION_RANGE + + companion object { + + private val RIGHT_DIRECTION = Pair(1, 0) + private val TOP_DIRECTION = Pair(0, 1) + private val RIGHT_TOP_DIRECTION = Pair(1, 1) + private val LEFT_BOTTOM_DIRECTION = Pair(-1, -1) + + private const val FORWARD_WEIGHT = 1 + private const val BACK_WEIGHT = -1 + } +} diff --git a/src/main/kotlin/domain/state/PlayerState.kt b/src/main/kotlin/domain/state/PlayerState.kt index 076a2c349..894ec62fb 100644 --- a/src/main/kotlin/domain/state/PlayerState.kt +++ b/src/main/kotlin/domain/state/PlayerState.kt @@ -12,4 +12,6 @@ abstract class PlayerState(protected val stones: Stones = Stones()) { fun getPlaced(): List = stones.getPositions() fun getLastStone(): Stone = stones.lastStone + + fun getAllStones(): Stones = stones.copy() } From 7eb0bc90e2a80ffb01eead308135e10b06c14ec7 Mon Sep 17 00:00:00 2001 From: tmdgh1592 Date: Thu, 16 Mar 2023 17:57:32 +0900 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=EB=A0=8C=EC=A3=BC=EB=A3=B0=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(=EC=99=84=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/controller/OmokController.kt | 3 +- src/main/kotlin/domain/board/Board.kt | 5 +- src/main/kotlin/domain/game/Omok.kt | 9 +- src/main/kotlin/domain/player/BlackPlayer.kt | 15 +- src/main/kotlin/domain/player/Player.kt | 10 +- src/main/kotlin/domain/player/Players.kt | 36 +++- src/main/kotlin/domain/player/WhitePlayer.kt | 4 +- src/main/kotlin/domain/rule/RenjuRule.kt | 157 ++++++++++++++---- .../kotlin/domain/rule/RenjuRuleAdapter.kt | 25 +++ src/main/kotlin/domain/state/LoseState.kt | 9 + src/main/kotlin/domain/state/PlayerState.kt | 3 +- src/main/kotlin/domain/state/PlayingState.kt | 3 +- src/main/kotlin/domain/state/WinState.kt | 3 +- 13 files changed, 229 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/domain/rule/RenjuRuleAdapter.kt create mode 100644 src/main/kotlin/domain/state/LoseState.kt diff --git a/src/main/kotlin/controller/OmokController.kt b/src/main/kotlin/controller/OmokController.kt index 65cec50dd..1c4350bbf 100644 --- a/src/main/kotlin/controller/OmokController.kt +++ b/src/main/kotlin/controller/OmokController.kt @@ -1,11 +1,12 @@ package controller import domain.game.Omok +import domain.rule.RenjuRule import view.InputView import view.OutputView class OmokController { fun start() { - Omok(OutputView(), InputView()).run() + Omok(OutputView(), InputView(), RenjuRule()).run() } } diff --git a/src/main/kotlin/domain/board/Board.kt b/src/main/kotlin/domain/board/Board.kt index ee1dbab35..3a6b95ee7 100644 --- a/src/main/kotlin/domain/board/Board.kt +++ b/src/main/kotlin/domain/board/Board.kt @@ -2,11 +2,12 @@ package domain.board import domain.player.Player import domain.player.Players +import domain.rule.OmokRule import domain.stone.Stone import domain.stone.StoneColor class Board(private val players: Players) { - constructor(blackPlayer: Player, whitePlayer: Player) : this(Players(blackPlayer, whitePlayer)) + constructor(blackPlayer: Player, whitePlayer: Player, rule: OmokRule) : this(Players(blackPlayer, whitePlayer, rule)) fun putStone(stoneColor: StoneColor, stone: Stone): Board? { if (players.canPlace(stone)) { @@ -18,4 +19,6 @@ class Board(private val players: Players) { fun getPlayers(): Players = players.copy() fun isRunning(): Boolean = players.isRunning + + fun isLose(): Boolean = players.isBlackLose } diff --git a/src/main/kotlin/domain/game/Omok.kt b/src/main/kotlin/domain/game/Omok.kt index 62fc5f1b1..9952f7e7c 100644 --- a/src/main/kotlin/domain/game/Omok.kt +++ b/src/main/kotlin/domain/game/Omok.kt @@ -3,6 +3,7 @@ package domain.game import domain.board.Board import domain.player.BlackPlayer import domain.player.WhitePlayer +import domain.rule.OmokRule import domain.stone.Stone import domain.stone.StoneColor import listener.OmokStartEndEventListener @@ -10,18 +11,20 @@ import listener.OmokTurnEventListener class Omok( private val startEndEventListener: OmokStartEndEventListener, - private val turnEventListener: OmokTurnEventListener + private val turnEventListener: OmokTurnEventListener, + private val rule: OmokRule ) { fun run() { startEndEventListener.onStartGame() var curStoneColor: StoneColor = StoneColor.BLACK - var curBoard = Board(BlackPlayer(), WhitePlayer()) + var curBoard = Board(BlackPlayer(), WhitePlayer(), rule) do { curBoard = takeTurn(curBoard, curStoneColor) startEndEventListener.onEndTurn(curBoard.getPlayers()) curStoneColor = curStoneColor.next() } while (curBoard.isRunning()) - startEndEventListener.onEndGame(curStoneColor.next()) + if (curBoard.isLose()) startEndEventListener.onEndGame(curStoneColor) + else startEndEventListener.onEndGame(curStoneColor.next()) } private fun takeTurn(board: Board, stoneColor: StoneColor): Board { diff --git a/src/main/kotlin/domain/player/BlackPlayer.kt b/src/main/kotlin/domain/player/BlackPlayer.kt index 8f08c5577..c37dff322 100644 --- a/src/main/kotlin/domain/player/BlackPlayer.kt +++ b/src/main/kotlin/domain/player/BlackPlayer.kt @@ -1,18 +1,21 @@ package domain.player -import domain.rule.RenjuRule +import domain.rule.OmokRule +import domain.state.LoseState import domain.state.PlayerState import domain.state.PlayingState import domain.stone.Stone import domain.stone.Stones class BlackPlayer(state: PlayerState = PlayingState()) : Player(state) { - override fun putStone(stone: Stone, otherStones: Stones): Player { + val isLose + get() = state is LoseState + + override fun putStone(stone: Stone, otherStones: Stones, rule: OmokRule): Player { val blackStones = state.getAllStones() - if (!RenjuRule().check(blackStones, otherStones, stone)) { - return BlackPlayer(state.add(stone)) - } else { - throw IllegalStateException("3-3입니다!") + if (rule.check(blackStones, otherStones, stone)) { + return BlackPlayer(LoseState(state.getAllStones())) } + return BlackPlayer(state.add(stone, rule)) } } diff --git a/src/main/kotlin/domain/player/Player.kt b/src/main/kotlin/domain/player/Player.kt index ef857d644..34b8af25a 100644 --- a/src/main/kotlin/domain/player/Player.kt +++ b/src/main/kotlin/domain/player/Player.kt @@ -1,17 +1,15 @@ package domain.player import domain.position.Position +import domain.rule.OmokRule +import domain.state.LoseState import domain.state.PlayerState import domain.state.WinState import domain.stone.Stone import domain.stone.Stones abstract class Player(protected val state: PlayerState) : Cloneable { - val isWin: Boolean = state is WinState - - abstract fun putStone(stone: Stone, otherStones: Stones): Player - - fun canPlace(): Boolean = state !is WinState + fun canPlace(): Boolean = state !is WinState && state !is LoseState fun isPlaced(stone: Stone): Boolean = state.hasStone(stone) @@ -21,5 +19,7 @@ abstract class Player(protected val state: PlayerState) : Cloneable { fun getAllStones(): Stones = state.getAllStones() + abstract fun putStone(stone: Stone, otherStones: Stones, rule: OmokRule): Player + public override fun clone(): Player = super.clone() as Player } diff --git a/src/main/kotlin/domain/player/Players.kt b/src/main/kotlin/domain/player/Players.kt index 8318d200c..0621c9559 100644 --- a/src/main/kotlin/domain/player/Players.kt +++ b/src/main/kotlin/domain/player/Players.kt @@ -1,19 +1,45 @@ package domain.player +import domain.rule.OmokRule +import domain.rule.RenjuRule import domain.stone.Stone import domain.stone.StoneColor -data class Players private constructor(private val players: List) { +data class Players private constructor(private val players: List, private val rule: OmokRule) { val isRunning: Boolean get() = players.all { it.canPlace() } + val isBlackLose: Boolean + get() = (getBlackPlayer() as BlackPlayer).isLose - constructor(blackPlayer: Player, whitePlayer: Player) : this(listOf(blackPlayer.clone(), whitePlayer.clone())) + constructor(blackPlayer: Player, whitePlayer: Player, rule: OmokRule) : this( + listOf( + blackPlayer.clone(), + whitePlayer.clone() + ), + rule + ) fun putStone(stoneColor: StoneColor, stone: Stone): Players { - if (stoneColor == StoneColor.BLACK) { - return Players(blackPlayer = getBlackPlayer().putStone(stone, getWhitePlayer().getAllStones()), whitePlayer = getWhitePlayer()) + val whiteStones = getWhitePlayer().getAllStones() + val blackStones = getBlackPlayer().getAllStones() + + return when (stoneColor) { + StoneColor.BLACK -> { + Players( + blackPlayer = getBlackPlayer().putStone(stone, whiteStones, RenjuRule()), + whitePlayer = getWhitePlayer(), + rule, + ) + } + + StoneColor.WHITE -> { + Players( + blackPlayer = getBlackPlayer(), + whitePlayer = getWhitePlayer().putStone(stone, blackStones, rule), + rule, + ) + } } - return Players(blackPlayer = getBlackPlayer(), whitePlayer = getWhitePlayer().putStone(stone, getBlackPlayer().getAllStones())) } fun getBlackPlayer(): Player = players.first { it is BlackPlayer } diff --git a/src/main/kotlin/domain/player/WhitePlayer.kt b/src/main/kotlin/domain/player/WhitePlayer.kt index d62cd8bad..7e17da20e 100644 --- a/src/main/kotlin/domain/player/WhitePlayer.kt +++ b/src/main/kotlin/domain/player/WhitePlayer.kt @@ -1,10 +1,12 @@ package domain.player +import domain.rule.OmokRule import domain.state.PlayerState import domain.state.PlayingState import domain.stone.Stone import domain.stone.Stones class WhitePlayer(state: PlayerState = PlayingState()) : Player(state) { - override fun putStone(stone: Stone, otherStones: Stones): Player = WhitePlayer(state.add(stone)) + override fun putStone(stone: Stone, otherStones: Stones, rule: OmokRule): Player = + WhitePlayer(state.add(stone, rule)) } diff --git a/src/main/kotlin/domain/rule/RenjuRule.kt b/src/main/kotlin/domain/rule/RenjuRule.kt index a5aa40ef7..e15ebaaa2 100644 --- a/src/main/kotlin/domain/rule/RenjuRule.kt +++ b/src/main/kotlin/domain/rule/RenjuRule.kt @@ -8,14 +8,31 @@ class RenjuRule : OmokRule { private var threeCount: Int = 0 override fun check(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { - return check33(blackStones, whiteStones, startStone) + return check33(blackStones, whiteStones, startStone) || check44(blackStones, whiteStones, startStone) || checkLongOmok(blackStones, startStone) } private fun check33(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { - return checkAllDirections(blackStones, whiteStones, startStone) + return check33AllDirections(blackStones, whiteStones, startStone) } - private fun checkAllDirections(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + private fun check44(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + return check44AllDirections(blackStones, whiteStones, startStone) + } + + private fun checkLongOmok(blackStones: Stones, startStone: Stone): Boolean { + val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) + for (moveDirection in directions) { + val forwardCount = findLongOmok(blackStones, startStone.position, moveDirection, FORWARD_WEIGHT) + val backCount = findLongOmok(blackStones, startStone.position, moveDirection, BACK_WEIGHT) + + if (forwardCount + backCount - 1 > 5) { + return true + } + } + return false + } + + private fun check33AllDirections(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) for (moveDirection in directions) { @@ -26,22 +43,36 @@ class RenjuRule : OmokRule { // 만약 빈 칸이 2 미만이고, 같은 돌 개수가 무조건 3이면 3-3 가능성 ok // 현재 방향과 3인지 여부를 확인한다. - println(moveDirection) - println("forwardCount : $forwardCount backCount : $backCount") - println("forwardEmptyCount : $forwardEmptyCount backEmptyCount : $backEmptyCount") if (forwardCount + backCount - 1 == 3 && forwardEmptyCount + backEmptyCount <= 1) { val (upDownDir, leftRightDir) = moveDirection // 백돌 양쪽 합 6칸 이내에 2개 이상 있는지 확인한다. // 닫혀 있으면 다른 방향 확인 - println("ㅇㅇㅇ") if (!isBlockedByWhiteStoneInSix(whiteStones, startStone.position.row, startStone.position.col, upDownDir, leftRightDir)) { threeCount++ - println("ㅇㅇㅇ2") } if (threeCount == 2) { - // 금수 - println("금수!!") + return true + } + } + } + return false + } + + private fun check44AllDirections(blackStones: Stones, whiteStones: Stones, startStone: Stone): Boolean { + val directions = listOf(RIGHT_DIRECTION, TOP_DIRECTION, RIGHT_TOP_DIRECTION, LEFT_BOTTOM_DIRECTION) + var fourCount = 0 + for (moveDirection in directions) { + val (forwardCount, forwardEmptyCount) = + findFour(blackStones, whiteStones, startStone.position, moveDirection, FORWARD_WEIGHT) + val (backCount, backEmptyCount) = + findFour(blackStones, whiteStones, startStone.position, moveDirection, BACK_WEIGHT) + + // 만약 빈 칸이 2 미만이고, 같은 돌 개수가 무조건 3이면 3-3 가능성 ok + // 현재 방향과 3인지 여부를 확인한다. + if (forwardCount + backCount - 1 == 4 && forwardEmptyCount + backEmptyCount <= 1) { + fourCount++ + if (fourCount == 2) { return true } } @@ -54,21 +85,21 @@ class RenjuRule : OmokRule { row: Int, col: Int, upDownDir: Int, - leftRightDir: Int + leftRightDir: Int, ): Boolean { val (oneDirMoveCount, oneDirFound) = checkWhite( whiteStones, row + upDownDir, col + leftRightDir, upDownDir, - leftRightDir + leftRightDir, ) val (otherDirMoveCount, otherDirFound) = checkWhite( whiteStones, row - upDownDir, col - leftRightDir, - upDownDir, - leftRightDir + upDownDir * -1, + leftRightDir * -1, ) // 양 방향 6칸 이하에 각각 1개씩 있으면 참 @@ -83,17 +114,17 @@ class RenjuRule : OmokRule { row: Int, col: Int, upDownDir: Int, - leftRightDir: Int + leftRightDir: Int, ): Pair { var (curRow, curCol) = Pair(row, col) - var moveCount = 1 - while (!whiteStones.hasStone(Stone.of(curRow, curCol)) && moveCount <= 6) { - curRow += upDownDir - curCol += leftRightDir + var moveCount = 0 + while (inRange(curRow, curCol) && moveCount <= 6) { moveCount++ if (whiteStones.hasStone(Stone.of(curRow, curCol))) { return Pair(moveCount, true) } + curRow += upDownDir + curCol += leftRightDir } return Pair(moveCount, false) } @@ -103,7 +134,7 @@ class RenjuRule : OmokRule { whiteStones: Stones, startPosition: Position, direction: Pair, - weight: Int = FORWARD_WEIGHT + weight: Int = FORWARD_WEIGHT, ): Pair { val (startRow, startCol) = Pair(startPosition.row, startPosition.col) var sameStoneCount = 1 @@ -114,31 +145,101 @@ class RenjuRule : OmokRule { // 흰 돌이 아니고, 범위 안에 있고 // 같은 돌의 개수가 3개 이하이고, 공백이 1개 이하일 때까지 while (inRange(currentRow, currentCol) && emptyCount <= 1 && - sameStoneCount <= 3 && !whiteStones.hasStone(Stone.of(currentRow, currentCol)) + sameStoneCount < 3 && !whiteStones.hasStone(Stone.of(currentRow, currentCol)) ) { // 검은 돌이 있는지 확인한다. - if (blackStones.hasStone(Stone.of(currentRow, currentCol))) { - ++sameStoneCount - } + if (blackStones.hasStone(Stone.of(currentRow, currentCol))) ++sameStoneCount // 빈 칸인지 확인한다. if (!blackStones.hasStone(Stone.of(currentRow, currentCol)) && !whiteStones.hasStone(Stone.of(currentRow, currentCol)) ) { - if (sameStoneCount == 3) break ++emptyCount } currentRow += direction.first * weight currentCol += direction.second * weight } - if (sameStoneCount == 1) emptyCount = 0 + if (sameStoneCount == 1) emptyCount = 0 // B X X X + if (sameStoneCount == 2) emptyCount = 1 // B X B + if (sameStoneCount == 2 && !blackStones.hasStone(Stone.of(currentRow - direction.first * weight, currentCol - direction.second * weight)) && + !whiteStones.hasStone(Stone.of(currentRow - direction.first * weight, currentCol - direction.second * weight)) + ) { + emptyCount -= 1 // B B X W + } - // println("현재 방향 : ${direction.first * weight} ${direction.second * weight}") - // println("검은 돌 : $sameStoneCount / 빈 칸 : $emptyCount") + return Pair(sameStoneCount, emptyCount) + } + private fun findFour( + blackStones: Stones, + whiteStones: Stones, + startPosition: Position, + direction: Pair, + weight: Int = FORWARD_WEIGHT, + ): Pair { + val (startRow, startCol) = Pair(startPosition.row, startPosition.col) + var sameStoneCount = 1 + var emptyCount = 0 + var (currentRow, currentCol) = Pair(startRow + direction.first * weight, startCol + direction.second * weight) + + // 현재 탐색 방향에 + // 흰 돌이 아니고, 범위 안에 있고 + // 같은 돌의 개수가 3개 이하이고, 공백이 1개 이하일 때까지 + while (inRange(currentRow, currentCol) && emptyCount <= 1 && + sameStoneCount < 4 && !whiteStones.hasStone(Stone.of(currentRow, currentCol)) + ) { + // 검은 돌이 있는지 확인한다. + if (blackStones.hasStone(Stone.of(currentRow, currentCol))) ++sameStoneCount + // 빈 칸인지 확인한다. + if (!blackStones.hasStone(Stone.of(currentRow, currentCol)) && + !whiteStones.hasStone(Stone.of(currentRow, currentCol)) + ) { + ++emptyCount + } + currentRow += direction.first * weight + currentCol += direction.second * weight + } + + currentRow -= direction.first * weight + currentCol -= direction.second * weight + + while (inRange(currentRow, currentCol) && !blackStones.hasStone(Stone.of(currentRow, currentCol))) { + if (whiteStones.hasStone(Stone.of(currentRow, currentCol))) { + currentRow -= direction.first * weight + currentCol -= direction.second * weight + continue + } + emptyCount -= 1 + currentRow -= direction.first * weight + currentCol -= direction.second * weight + } return Pair(sameStoneCount, emptyCount) } + private fun findLongOmok( + blackStones: Stones, + startPosition: Position, + direction: Pair, + weight: Int = FORWARD_WEIGHT, + ): Int { + val (startRow, startCol) = Pair(startPosition.row, startPosition.col) + var sameStoneCount = 1 + var emptyCount = 0 + var (currentRow, currentCol) = Pair(startRow + direction.first * weight, startCol + direction.second * weight) + + // 현재 탐색 방향에 + // 흰 돌이 아니고, 범위 안에 있고 + // 같은 돌의 개수가 3개 이하이고, 공백이 1개 이하일 때까지 + while (inRange(currentRow, currentCol) && blackStones.hasStone(Stone.of(currentRow, currentCol))) { + // 검은 돌이 있는지 확인한다. + sameStoneCount++ + currentRow += direction.first * weight + currentCol += direction.second * weight + } + + return sameStoneCount + } + private fun inRange(x: Int, y: Int) = x in Position.POSITION_RANGE && y in Position.POSITION_RANGE companion object { diff --git a/src/main/kotlin/domain/rule/RenjuRuleAdapter.kt b/src/main/kotlin/domain/rule/RenjuRuleAdapter.kt new file mode 100644 index 000000000..1509a1054 --- /dev/null +++ b/src/main/kotlin/domain/rule/RenjuRuleAdapter.kt @@ -0,0 +1,25 @@ +// package domain.rule +// +// import domain.stone.Stone +// import domain.stone.Stones +// import rule.ArkRenjuRule +// +// class RenjuRuleAdapter(private val arkRule: ArkRenjuRule) : OmokRule { +// override fun countOpenThrees(blackStones: Stones, whiteStones: Stones, stone: Stone): Int { +// println(arkRule.countOpenThrees(stone.position.row - 1, stone.position.col - 1)) +// return arkRule.countOpenThrees(stone.position.row - 1, stone.position.col - 1) +// } +// +// override fun countOpenFours(blackStones: Stones, whiteStones: Stones, stone: Stone): Int { +// println(arkRule.countOpenFours(stone.position.row - 1, stone.position.col - 1)) +// return arkRule.countOpenFours(stone.position.row - 1, stone.position.col - 1) +// } +// +// override fun validateWhiteWin(blackStones: Stones, whiteStones: Stones, stone: Stone): Boolean { +// return arkRule.validateWhiteWin(stone.position.row - 1, stone.position.col - 1) +// } +// +// override fun validateBlackWin(blackStones: Stones, whiteStones: Stones, stone: Stone): Boolean { +// return arkRule.validateBlackWin(stone.position.row - 1, stone.position.col - 1) +// } +// } diff --git a/src/main/kotlin/domain/state/LoseState.kt b/src/main/kotlin/domain/state/LoseState.kt new file mode 100644 index 000000000..0a3c39be0 --- /dev/null +++ b/src/main/kotlin/domain/state/LoseState.kt @@ -0,0 +1,9 @@ +package domain.state + +import domain.rule.OmokRule +import domain.stone.Stone +import domain.stone.Stones + +class LoseState(stones: Stones) : PlayerState(stones) { + override fun add(newStone: Stone, rule: OmokRule): PlayerState = this +} diff --git a/src/main/kotlin/domain/state/PlayerState.kt b/src/main/kotlin/domain/state/PlayerState.kt index 894ec62fb..f7f8db146 100644 --- a/src/main/kotlin/domain/state/PlayerState.kt +++ b/src/main/kotlin/domain/state/PlayerState.kt @@ -1,11 +1,12 @@ package domain.state import domain.position.Position +import domain.rule.OmokRule import domain.stone.Stone import domain.stone.Stones abstract class PlayerState(protected val stones: Stones = Stones()) { - abstract fun add(newStone: Stone): PlayerState + abstract fun add(newStone: Stone, rule: OmokRule): PlayerState fun hasStone(stone: Stone): Boolean = stones.hasStone(stone) diff --git a/src/main/kotlin/domain/state/PlayingState.kt b/src/main/kotlin/domain/state/PlayingState.kt index 0f131e568..184ceba62 100644 --- a/src/main/kotlin/domain/state/PlayingState.kt +++ b/src/main/kotlin/domain/state/PlayingState.kt @@ -1,10 +1,11 @@ package domain.state +import domain.rule.OmokRule import domain.stone.Stone import domain.stone.Stones class PlayingState(stones: Stones = Stones()) : PlayerState(stones) { - override fun add(newStone: Stone): PlayerState { + override fun add(newStone: Stone, rule: OmokRule): PlayerState { val newStones = stones.add(newStone) if (newStones.checkWin(newStone)) return WinState(newStones) return PlayingState(newStones) diff --git a/src/main/kotlin/domain/state/WinState.kt b/src/main/kotlin/domain/state/WinState.kt index 7350f2c69..5ff3229c6 100644 --- a/src/main/kotlin/domain/state/WinState.kt +++ b/src/main/kotlin/domain/state/WinState.kt @@ -1,8 +1,9 @@ package domain.state +import domain.rule.OmokRule import domain.stone.Stone import domain.stone.Stones class WinState(stones: Stones) : PlayerState(stones) { - override fun add(newStone: Stone): PlayerState = this + override fun add(newStone: Stone, rule: OmokRule): PlayerState = this }