Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[수달] 1, 2단계 오목 제출합니다. #9

Merged
merged 48 commits into from
Mar 23, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1a6b873
docs[README]: 기능 목록 작성
Choisehyeon Mar 14, 2023
6af9c36
test[StudyTest]: 학습테스트 작성
Choisehyeon Mar 14, 2023
b974dbd
feat[StonePosition]: x좌표와 y좌표를 가지는 클래스 생성
Choisehyeon Mar 14, 2023
0c09508
feat[StonePosition]: x좌표와 y좌표 범위에 대해 값 검사하는 기능 추가
Choisehyeon Mar 14, 2023
76b2c9e
feat[Stone]: 위치와 타입을 가지는 Stone 클래스 생성
Choisehyeon Mar 14, 2023
82de313
feat[Stones]: 스톤들을 가지는 Stones 클래스 생성
Choisehyeon Mar 14, 2023
d30249a
feat[Stones]: 스톤을 받아 추가하는 기능 구현
Choisehyeon Mar 14, 2023
1875013
feat: Stones를 상속받은 WhiteStones와 BlackStones 생성
Choisehyeon Mar 14, 2023
dc9fd03
refactor: 패키지 분리
Choisehyeon Mar 14, 2023
3c79aed
feat: State 인터페이스 생성
Choisehyeon Mar 14, 2023
d4dfb16
feat[Stones]: Stones 기능 추가
Choisehyeon Mar 15, 2023
2fbb475
feat[InputView]: 위치를 입력받는 기능 추가
Choisehyeon Mar 15, 2023
13921b1
feat[OutputView]: 흑돌들과 백돌들을 받아서 바둑판 위에 출력하는 기능 추가
Choisehyeon Mar 15, 2023
1979607
refactor[Stones]: 테스트를 위한 제품코드 삭제
Choisehyeon Mar 15, 2023
33be215
feat[Board]: 게임 턴의 상태를 가진다
Choisehyeon Mar 15, 2023
a077120
feat[OmokRule]: 오목 조건 충족하는지 확인하는 기능 추가
Choisehyeon Mar 15, 2023
857be63
refactor: 불필요한 코드 제거
Choisehyeon Mar 15, 2023
a375b7f
refactor: 로직 수정
Choisehyeon Mar 15, 2023
4996ba6
feat: Running abstact 클래스 생성
Choisehyeon Mar 15, 2023
3e82542
fix: 오류 수정
Choisehyeon Mar 15, 2023
3464215
feat: BlackTurn 클래스 생성
Choisehyeon Mar 15, 2023
7fd0bc6
feat: WhiteTurn 클래스 생성
Choisehyeon Mar 15, 2023
8ea16e2
feat: 우승자의 StoneType 반환하는 End 클래스 생성
Choisehyeon Mar 15, 2023
9c0c939
feat[OutputView]: 턴과 마지막 돌의 위치 출력하는 기능 추가
Choisehyeon Mar 16, 2023
4d24b87
feat[OutputView]: 승리한 돌의 색을 출력한다.
Choisehyeon Mar 16, 2023
de2a188
feat[board]: board에 스톤들을 관리하도록 구현
Choisehyeon Mar 16, 2023
3cfb7c2
refactor: 생성자 변경
Choisehyeon Mar 16, 2023
19ba6f9
refactor: 출력 양식에 맞춰서 수정
Choisehyeon Mar 16, 2023
4ae2570
feat: Controller 구현
Choisehyeon Mar 16, 2023
94dfcf9
feat: 렌주룰 규칙 적용
Choisehyeon Mar 16, 2023
dfa8d17
refactor: 불필요한 코드 제거
Choisehyeon Mar 16, 2023
6ce3f28
refactor: 패키지 분리
Mar 19, 2023
1e17bf4
refactor: 인터페이스 메서드 변경
Mar 19, 2023
721eea8
refactor(view): 가독성 개선
Mar 20, 2023
7c46fe6
refactor(state): 가독성 개선
Mar 20, 2023
1675575
feat(rule): 규칙 인터페이스 생성
Mar 20, 2023
5adf3b5
feat(state): 돌을 놓고 다음 상태로 넘어가는 메서드 변경
Mar 20, 2023
ae4daed
refactor: 코드 개선
Mar 20, 2023
257813a
feat(state): interface를 상속 받는 class에서 호출되면 안되는 메서드에 대한 처리 추가
Mar 20, 2023
27ec424
refactor(board): 방어적 복사 사용
Mar 20, 2023
363cba5
refactor(studyTest): 출력 형식 연습
Mar 20, 2023
74905a3
refactor(state): 의존성 주입 방식 변경
Mar 21, 2023
edb24fb
refactor: 가독성 개선
Mar 22, 2023
9d2022c
refactor(StonePosition): 팩터리 메서드에서 null을 반환하지 않도록 변경
Mar 22, 2023
023316f
refactor(StonePosition): 팩터리 메서드 제거
Mar 22, 2023
76ba39e
refactor(Stone): 팩터리 메서드 제거
Mar 22, 2023
653b9c6
refactor(State): 불필요 interface 메서드 삭제
Mar 23, 2023
34bb948
feat(State): 상태 확인 메서드 추가
Mar 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion README.md
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개로 고칠 수 있지만 요구사항에 맞지 않음
Empty file removed src/main/kotlin/.gitkeep
Empty file.
7 changes: 7 additions & 0 deletions src/main/kotlin/Application.kt
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()
}
36 changes: 36 additions & 0 deletions src/main/kotlin/controller/OmokController.kt
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))

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 이 고정되어 있는것을 잘 고민해보면 좋겠습니다 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

돌을 놓을 때 위치만 전달하도록 하여 상태에서 돌을 생성해 놓도록 하였습니다!!!

is WhiteTurn -> state = state.put(Stone(inputView.inputStonePosition(), StoneType.WHITE))
}

outputView.printBoard(board.stones)
}

outputView.printWinner(state)
}
}
229 changes: 229 additions & 0 deletions src/main/kotlin/domain/rule/OmokRule.kt
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
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/domain/state/BlackTurn.kt
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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State 가 Board 를 주입받는 방식과
Board 가 State 를 소유하고 있는 방식의

장/단점은 무엇이 있을까요?

Copy link
Author

@otter66 otter66 Mar 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State에 의존성을 생성자를 통해 주입하는 방식, 메서드를 통해 주입하는 방식에 대해 고민해 보았습니다!

아직까지는 두가지에 크게 차이가 없다고 생각합니다!
클래스 내부에서 다수의 메서드들이 사용한다면 생성자로 의존성을 받는 것이 유리할 것이고,
일부의 메서드에서만 이용한다면 메서드로 의존성을 받는 것이 유리할 것 같습니다.
또한, 생성자로 의존성을 유입받을 시에는 해당 객체의 의존성을 파악하기 편리한 것 같습니다!

피드백 주신 내용을 통해 제 코드를 살펴보니 board와 position은 사용하는 메서드에 차이가 없음에도
board는 생성자를 통해 주입하고, position은 메서드를 통해 주입하고 있었네요…!

두 객체는 돌을 놓고, 다음 상태로 넘어가는 메서드에서만 사용하기 때문에
메서드를 통해 의존성을 주입하도록 통일시키겠습니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정리 잘해주신 것 같습니다 👍

제가 생각했을 때, 생성자 주입과 함수를 통한 주입의 또다른 큰 차이점은 상태의 지속성입니다.
생성자로 상태를 주입받을 경우, 보통은 그 클래스의 생명주기와 주입받은 상태의 생명주기가 같이가게 됩니다.

이는 2가지를 뜻합니다.

  1. 해당 클래스에 해당 상태는 필수적인 요소이다.
  2. 주입받은 상태가 외부로부터 변경이 발생했을때, 해당 클래스에서 예기치 못한 이슈가 발생해서는 안된다.

한편, 함수로 주입을 받을 경우 상태의 생명주기는 함수 콜스택에서 멈추게 됩니다.
위에서 얘기한 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오목의 규칙은 대회에 따라 세부사항이 달라질 수 있는 걸로 알고 있는데요.
OmokRule 에 직접 접근하기보다 인터페이스를 통해 언제든 교체할 수 있도록 구성해보는 것은 어떨까요?

Copy link
Author

@otter66 otter66 Mar 20, 2023

Choose a reason for hiding this comment

The 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
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/domain/state/End.kt
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")
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/domain/state/Running.kt
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)
}
}
Loading