-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[수달] 1, 2단계 오목 제출합니다. #9
Changes from 31 commits
1a6b873
6af9c36
b974dbd
0c09508
76b2c9e
82de313
d30249a
1875013
dc9fd03
3c79aed
d4dfb16
2fbb475
13921b1
1979607
33be215
a077120
857be63
a375b7f
4996ba6
3e82542
3464215
7fd0bc6
8ea16e2
9c0c939
4d24b87
de2a188
3cfb7c2
19ba6f9
4ae2570
94dfcf9
dfa8d17
6ce3f28
1e17bf4
721eea8
7c46fe6
1675575
5adf3b5
ae4daed
257813a
27ec424
363cba5
74905a3
edb24fb
9d2022c
023316f
76ba39e
653b9c6
34bb948
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,60 @@ | ||
# kotlin-omok | ||
# kotlin-omok | ||
|
||
## 기능 목록 | ||
|
||
data class Position | ||
- [X] x좌표와 y좌표를 알고 있다. | ||
- [X] x좌표는 1이상 15이하의 값을 가진다. | ||
- [X] y좌표는 1이상 15이하의 값을 가진다. | ||
--- | ||
enum StoneType | ||
- [x] BLACK, WHITE, EMPTY를 가진다. | ||
Stone | ||
- [x] 자신의 위치와 스톤 타입을 가진다. | ||
class Stones | ||
- [x] 스톤들의 정보를 담고 있다. | ||
- [x] 스톤을 받아 추가한다. | ||
- [x] 스톤을 받아 해당 스톤의 위치에 돌이 놓여져있는지 확인한다. | ||
- [x] 스톤들을 받아서 두 스톤들을 더한 값을 반환한다. | ||
- [x] 돌들의 위치를 board에 표시해 반환한다. | ||
- | ||
--- | ||
interface State | ||
- [x] put(): State | ||
- [x] getWinner(): StoneType | ||
- [x] isValidPut(): Boolean | ||
- [x] isOmokCondition(): Boolean | ||
abstract Running(stones) : State | ||
- [x] isValidPut(): Boolean { } | ||
- [x] 오목 조건을 충족하는지 확인한다. | ||
WhiteTurn : Running | ||
- [x] stone을 추가할 수 없는 상태라면 추가하지 않고 WhiteTurn을 반환 | ||
- [x] stone를 추가한 후 BlackTurn을 반환 | ||
- [x] 오목 조건 충족하면 End 상태로 White가 Win | ||
BlackTurn : Running | ||
- [x] stone을 추가할 수 없는 상태라면 추가하지 않고 BlackTurn을 반환 | ||
- [x] stone를 추가한 후 WhitTurn를 반환 | ||
- [x] 오목 조건 충족하면 End 상태로 Black이 Win | ||
End : State | ||
- [x] 우승자에 해당하는 StoneType을 가진다. | ||
- [x] 우승자의 StoneType을 반환한다. | ||
--- | ||
OmokRule(board: Board) | ||
- [X] 오목 조건을 충족하는지 확인한다. | ||
--- | ||
Board | ||
- [x] 게임 턴의 상태를 가진다. | ||
--- | ||
InputView | ||
- [X] 위치를 입력받는다. | ||
--- | ||
OutputView | ||
- [X] 흑돌들과 백돌들을 받아서 바둑판 위에 출력한다. | ||
- [x] 누구 차례인지 출력한다. | ||
- [x] 마지막 돌의 위치를 출력한다. | ||
- [x] 누가 승리했는지 출력한다.(black이면 흑 white면 백) | ||
|
||
|
||
--- | ||
찝찝한 것 | ||
- [ ] 모든 상태들이 크게 차이가 없는 두 돌인 흑돌과 백돌을 따로 받는다. -> Stones가 백돌을 흑돌을 가지게 해서 프로퍼티가 3개로 고칠 수 있지만 요구사항에 맞지 않음 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import controller.OmokController | ||
|
||
fun main() { | ||
val controller: OmokController = OmokController() | ||
|
||
controller.run() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package controller | ||
|
||
import domain.state.BlackTurn | ||
import domain.state.End | ||
import domain.state.State | ||
import domain.state.WhiteTurn | ||
import domain.stone.Board | ||
import domain.stone.Stone | ||
import domain.stone.StoneType | ||
import view.InputView | ||
import view.OutputView | ||
|
||
class OmokController( | ||
val inputView: InputView = InputView(), | ||
val outputView: OutputView = OutputView(), | ||
) { | ||
|
||
fun run() { | ||
val board: Board = Board() | ||
var state: State = BlackTurn(board) | ||
|
||
outputView.printOmokStart() | ||
while (state !is End) { | ||
outputView.printTurn(state, board.stones) | ||
|
||
when (state) { | ||
is BlackTurn -> state = state.put(Stone(inputView.inputStonePosition(), StoneType.BLACK)) | ||
is WhiteTurn -> state = state.put(Stone(inputView.inputStonePosition(), StoneType.WHITE)) | ||
} | ||
|
||
outputView.printBoard(board.stones) | ||
} | ||
|
||
outputView.printWinner(state) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
package domain.rule | ||
|
||
import domain.stone.Board.Companion.BOARD_SIZE | ||
import domain.stone.Stone | ||
import domain.stone.StoneType | ||
|
||
object OmokRule { | ||
|
||
const val MIN_OPEN_THREES = 2 | ||
const val MIN_OPEN_FOURS = 2 | ||
private const val MIN_X = 1 | ||
private const val MAX_X = 15 | ||
private const val MIN_Y = 1 | ||
private const val MAX_Y = 15 | ||
private val X_Edge = listOf(MIN_X, MAX_X) | ||
private val Y_Edge = listOf(MIN_Y, MAX_Y) | ||
|
||
fun countOpenThrees(board: List<List<StoneType>>, stone: Stone): Int = | ||
checkOpenThree(board, stone, 1, 0) + | ||
checkOpenThree(board, stone, 1, 1) + | ||
checkOpenThree(board, stone, 0, 1) + | ||
checkOpenThreeReverse(board, stone, 1, -1) | ||
|
||
fun countOpenFours(board: List<List<StoneType>>, stone: Stone): Int = | ||
checkOpenFour(board, stone, 1, 0) + | ||
checkOpenFour(board, stone, 1, 1) + | ||
checkOpenFour(board, stone, 0, 1) + | ||
checkOpenFourReverse(board, stone, 1, -1) | ||
|
||
private fun checkOpenThree(board: List<List<StoneType>>, stone: Stone, dx: Int, dy: Int): Int { | ||
val (stone1, blink1) = search(board, stone, -dx, -dy) | ||
val (stone2, blink2) = search(board, stone, dx, dy) | ||
|
||
val leftDown = stone1 + blink1 | ||
val rightUp = stone2 + blink2 | ||
|
||
return when { | ||
stone1 + stone2 != 2 -> 0 | ||
blink1 + blink2 == 2 -> 0 | ||
dx != 0 && stone.position.x.minus(leftDown) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.minus(leftDown) in Y_Edge -> 0 | ||
dx != 0 && stone.position.x.plus(rightUp) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.plus(rightUp) in Y_Edge -> 0 | ||
board[stone.position.y - dy * (leftDown + 1)][stone.position.x - dx * (leftDown + 1)] == StoneType.WHITE -> 0 | ||
board[stone.position.y + dy * (rightUp + 1)][stone.position.x + dx * (rightUp + 1)] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
} | ||
|
||
private fun checkOpenThreeReverse(board: List<List<StoneType>>, stone: Stone, dx: Int, dy: Int): Int { | ||
val (stone1, blink1) = search(board, stone, -dx, -dy) | ||
val (stone2, blink2) = search(board, stone, dx, dy) | ||
|
||
val leftUp = stone1 + blink1 | ||
val rightBottom = stone2 + blink2 | ||
|
||
return when { | ||
stone1 + stone2 != 2 -> 0 | ||
blink1 + blink2 == 2 -> 0 | ||
dx != 0 && stone.position.x.minus(leftUp) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.plus(leftUp) in Y_Edge -> 0 | ||
dx != 0 && stone.position.x.plus(rightBottom) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.minus(rightBottom) in Y_Edge -> 0 | ||
board[stone.position.y - rightBottom - 1][stone.position.x + rightBottom + 1] == StoneType.WHITE -> 0 | ||
board[stone.position.y + leftUp + 1][stone.position.x - leftUp - 1] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
} | ||
|
||
private fun checkOpenFour(board: List<List<StoneType>>, stone: Stone, dx: Int, dy: Int): Int { | ||
val (stone1, blink1) = search(board, stone, -dx, -dy) | ||
val (stone2, blink2) = search(board, stone, dx, dy) | ||
|
||
val leftDown = stone1 + blink1 | ||
val rightUp = stone2 + blink2 | ||
|
||
when { | ||
blink1 + blink2 == 2 && stone1 + stone2 == 4 -> return 2 | ||
blink1 + blink2 == 2 && stone1 + stone2 == 5 -> return 2 | ||
stone1 + stone2 != 3 -> return 0 | ||
blink1 + blink2 == 2 -> return 0 | ||
} | ||
|
||
val leftDownValid = when { | ||
dx != 0 && stone.position.x.minus(dx * leftDown) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.minus(dy * leftDown) in Y_Edge -> 0 | ||
board[stone.position.y - dy * (leftDown + 1)][stone.position.x - dx * (leftDown + 1)] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
val rightUpValid = when { | ||
dx != 0 && stone.position.x.plus(dx * rightUp) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.plus(dy * rightUp) in Y_Edge -> 0 | ||
board[stone.position.y + dy * (rightUp + 1)][stone.position.x + dx * (rightUp + 1)] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
|
||
return if (leftDownValid + rightUpValid >= 1) 1 else 0 | ||
} | ||
|
||
private fun checkOpenFourReverse(board: List<List<StoneType>>, stone: Stone, dx: Int, dy: Int): Int { | ||
val (stone1, blink1) = search(board, stone, -dx, -dy) | ||
val (stone2, blink2) = search(board, stone, dx, dy) | ||
|
||
val leftUp = stone1 + blink1 | ||
val rightBottom = stone2 + blink2 | ||
|
||
when { | ||
blink1 + blink2 == 2 && stone1 + stone2 == 5 -> return 2 | ||
blink1 + blink2 == 2 && stone1 + stone2 == 4 -> return 2 | ||
stone1 + stone2 != 3 -> return 0 | ||
blink1 + blink2 == 2 -> return 0 | ||
} | ||
|
||
val leftUpValid = when { | ||
dx != 0 && stone.position.x.minus(leftUp) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.plus(leftUp) in Y_Edge -> 0 | ||
board[stone.position.y - rightBottom - 1][stone.position.x + rightBottom + 1] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
|
||
val rightBottomValid = when { | ||
dx != 0 && stone.position.x.plus(rightBottom) in X_Edge -> 0 | ||
dy != 0 && stone.position.y.minus(rightBottom) in Y_Edge -> 0 | ||
board[stone.position.y + leftUp + 1][stone.position.x - leftUp - 1] == StoneType.WHITE -> 0 | ||
else -> 1 | ||
} | ||
|
||
return if (leftUpValid + rightBottomValid >= 1) 1 else 0 | ||
} | ||
|
||
private fun search(board: List<List<StoneType>>, stone: Stone, dx: Int, dy: Int): Pair<Int, Int> { | ||
var toRight = stone.position.x | ||
var toTop = stone.position.y | ||
var stoneCount = 0 | ||
var blink = 0 | ||
var blinkCount = 0 | ||
while (true) { | ||
if (dx > 0 && toRight == MAX_X) break | ||
if (dx < 0 && toRight == MIN_X) break | ||
if (dy > 0 && toTop == MAX_Y) break | ||
if (dy < 0 && toTop == MIN_X) break | ||
toRight += dx | ||
toTop += dy | ||
when (board[toTop][toRight]) { | ||
StoneType.BLACK -> { | ||
stoneCount++ | ||
blink = blinkCount | ||
} | ||
|
||
StoneType.WHITE -> break | ||
StoneType.EMPTY -> { | ||
if (blink == 1) break | ||
if (blinkCount++ == 1) break | ||
} | ||
} | ||
} | ||
return Pair(stoneCount, blink) | ||
} | ||
|
||
fun isWinCondition(board: List<List<StoneType>>, stone: Stone): Boolean { | ||
if (checkHorizontal(board, stone)) return true | ||
if (checkVertical(board, stone)) return true | ||
if (checkDiagonal1(board, stone)) return true | ||
if (checkDiagonal2(board, stone)) return true | ||
return false | ||
} | ||
|
||
private fun checkHorizontal(board: List<List<StoneType>>, stone: Stone): Boolean { | ||
var count: Int = 0 | ||
val x: Int = stone.position.x | ||
val y: Int = stone.position.y | ||
for (i in -4..4) { | ||
if (x + i !in 1..BOARD_SIZE) continue | ||
if (board[y][x + i] != stone.type) count = 0 | ||
if (board[y][x + i] == stone.type) { | ||
count++ | ||
if (count >= 5) break | ||
} | ||
} | ||
return count >= 5 | ||
} | ||
|
||
private fun checkVertical(board: List<List<StoneType>>, stone: Stone): Boolean { | ||
var count: Int = 0 | ||
val x: Int = stone.position.x | ||
val y: Int = stone.position.y | ||
for (i in -4..4) { | ||
if (y + i !in 1..BOARD_SIZE) continue | ||
if (board[y + i][x] != stone.type) count = 0 | ||
if (board[y + i][x] == stone.type) { | ||
count++ | ||
if (count >= 5) break | ||
} | ||
} | ||
return count >= 5 | ||
} | ||
|
||
private fun checkDiagonal1(board: List<List<StoneType>>, stone: Stone): Boolean { | ||
var count: Int = 0 | ||
val x: Int = stone.position.x | ||
val y: Int = stone.position.y | ||
for (i in -4..4) { | ||
if (x + i !in 1..BOARD_SIZE) continue | ||
if (y + i !in 1..BOARD_SIZE) continue | ||
if (board[y + i][x + i] != stone.type) count = 0 | ||
if (board[y + i][x + i] == stone.type) { | ||
count++ | ||
if (count >= 5) break | ||
} | ||
} | ||
return count >= 5 | ||
} | ||
|
||
private fun checkDiagonal2(board: List<List<StoneType>>, stone: Stone): Boolean { | ||
var count: Int = 0 | ||
val x: Int = stone.position.x | ||
val y: Int = stone.position.y | ||
for (i in -4..4) { | ||
if (x - i !in 1..BOARD_SIZE) continue | ||
if (y + i !in 1..BOARD_SIZE) continue | ||
if (board[y + i][x - i] != stone.type) count = 0 | ||
if (board[y + i][x - i] == stone.type) { | ||
count++ | ||
if (count >= 5) break | ||
} | ||
} | ||
return count >= 5 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package domain.state | ||
|
||
import domain.rule.OmokRule | ||
import domain.stone.Board | ||
import domain.stone.Stone | ||
import domain.stone.StoneType | ||
|
||
class BlackTurn(board: Board) : Running(board) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. State 가 Board 를 주입받는 방식과 장/단점은 무엇이 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
아직까지는 두가지에 크게 차이가 없다고 생각합니다! 피드백 주신 내용을 통해 제 코드를 살펴보니 board와 position은 사용하는 메서드에 차이가 없음에도 두 객체는 돌을 놓고, 다음 상태로 넘어가는 메서드에서만 사용하기 때문에 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 정리 잘해주신 것 같습니다 👍 제가 생각했을 때, 생성자 주입과 함수를 통한 주입의 또다른 큰 차이점은 이는 2가지를 뜻합니다.
한편, 함수로 주입을 받을 경우 상태의 생명주기는 함수 콜스택에서 멈추게 됩니다. 주입 방식을 고민할 때, 위 내용도 같이 생각해보면 좋을 것 같습니다. |
||
override fun put(stone: Stone): State { | ||
if (!isValidPut(stone)) return BlackTurn(board) | ||
if (checkForbidden(board, stone)) return BlackTurn(board) | ||
board.putStone(stone) | ||
if (OmokRule.isWinCondition(board.board, stone)) return End(StoneType.BLACK) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오목의 규칙은 대회에 따라 세부사항이 달라질 수 있는 걸로 알고 있는데요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오...!! 이 부분은 'State 인터페이스에서 선언해야하는건가...?'라는 등 잘 이해가 되지 않아 같은 모바일 크루인 해시와 이야기 해보았는데, 해시가 규칙에 interface를 둔 것을 보고 배웠습니다! |
||
return WhiteTurn(board) | ||
} | ||
|
||
private fun checkForbidden(board: Board, stone: Stone): Boolean { | ||
return OmokRule.countOpenThrees(board.board, stone) >= OmokRule.MIN_OPEN_THREES || | ||
OmokRule.countOpenFours(board.board, stone) >= OmokRule.MIN_OPEN_FOURS | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package domain.state | ||
|
||
import domain.stone.Stone | ||
import domain.stone.StoneType | ||
|
||
class End(val stoneType: StoneType) : State { | ||
|
||
override fun getWinner(): StoneType = stoneType | ||
|
||
override fun isValidPut(stone: Stone): Boolean { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun put(stone: Stone): State { | ||
TODO("Not yet implemented") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package domain.state | ||
|
||
import domain.stone.Board | ||
import domain.stone.Stone | ||
import domain.stone.StoneType | ||
|
||
abstract class Running(val board: Board) : State { | ||
abstract override fun put(stone: Stone): State | ||
|
||
override fun getWinner(): StoneType { | ||
TODO("Not yet implemented") | ||
} | ||
|
||
override fun isValidPut(stone: Stone): Boolean { | ||
return !board.stones.containsPosition(stone) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[질문 주신 부분]
stone 을 정의할 때
꼭 필요한 부분만 외부에서 전달
받고 나머지는 State '스스로' 결정 하도록 바꿔보면, 분기처리가 필요없지 않을까요?*black 다음에는 white, white 다음에는 black 이 고정되어 있는것을 잘 고민해보면 좋겠습니다 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
돌을 놓을 때 위치만 전달하도록 하여 상태에서 돌을 생성해 놓도록 하였습니다!!!