-
Notifications
You must be signed in to change notification settings - Fork 51
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단계 오목 제출합니다. #26
Changes from all commits
fe3ce07
ca4182c
e13e7da
fa5e2b9
0c398fd
190d007
9d0c276
606cfcf
f96746e
e5e8ce4
c472a31
f4f1b15
28bb0cd
f37f93b
b1fcb5b
11b2666
a0166e0
7eb0bc9
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 |
---|---|---|
@@ -0,0 +1,27 @@ | ||
## 오목 | ||
|
||
### Domain | ||
- [ ] 한 플레이어라도 승리할 때까지 차례를 번갈아가면서 돌을 놓는다. | ||
- [x] 흑, 백 플레이어를 가지고 있다 | ||
- [x] 흑돌이 먼저 시작한다. | ||
- [ ] 게임의 진행 여부는 `PlayerState`가 결정한다. | ||
- [x] 오목알은 자신의 위치를 알고 있다. | ||
- [x] `x, y` 위치는 `1부터 15`로 제한된다. | ||
- [x] 중복되는 위치의 오목알을 가질 수 없다. | ||
- [x] 오목판의 크기는 `15 x 15`이다. | ||
- [x] 사용자는 오목알을 놓는다. | ||
- [x] 오목알을 놓았을 때 5개 이상 연이어 있으면 승리한다. | ||
- [x] 오목알을 놓았을 때 5개 미만 연이어 있으면 게임을 계속 진행한다. | ||
- [x] 특정 위치에 돌을 놓을 수 있는지 판단한다. | ||
- [x] 플레이어는 흑과 백으로 이루어져 있다. | ||
- [x] 오목알을 놓은 플레이어가 게임에서 이겼는지 확인한다. | ||
- [x] 사용자는 특정 위치에 내 돌이 있는지 확인한다. | ||
- [x] 사용자는 마지막 돌의 위치를 알고 있다. | ||
- [x] 오목알을 놓으면 상대방의 차례가 된다. | ||
|
||
### Input | ||
- [ ] 오목알을 놓을 위치를 입력받는다. | ||
|
||
### Output | ||
- [ ] 오목알의 위치를 입력받기 전에 오목판을 출력한다. | ||
- [ ] 마지막 돌의 위치를 출력한다. | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import controller.OmokController | ||
|
||
fun main() { | ||
OmokController().start() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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(), RenjuRule()).run() | ||
} | ||
Comment on lines
+9
to
+11
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. 오목 게임으로부터 어떤 규칙을 적용할지 넘겨주는 방식 좋습니다! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
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, rule: OmokRule) : this(Players(blackPlayer, whitePlayer, rule)) | ||
|
||
fun putStone(stoneColor: StoneColor, stone: Stone): Board? { | ||
if (players.canPlace(stone)) { | ||
return Board(players.putStone(stoneColor, stone)) | ||
} | ||
return null | ||
} | ||
Comment on lines
+12
to
+17
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. 돌을 배치할 수 없는 경우, null을 반환하는 것이 논리적인 흐름에 맞을까요? 왜 null이지 라는 의문을 가질 수 있지는 않을까요? 아무런 변경이 없다면, 자기 자신을 반환하도록 할 수 있지 않을까요? |
||
|
||
fun getPlayers(): Players = players.copy() | ||
|
||
fun isRunning(): Boolean = players.isRunning | ||
|
||
fun isLose(): Boolean = players.isBlackLose | ||
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. 보드의 기준으로 패배의 상태가 블랙이 패배라는 것이 올바른 흐름으로 볼 수 있을까요? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
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 | ||
import listener.OmokTurnEventListener | ||
|
||
class Omok( | ||
private val startEndEventListener: OmokStartEndEventListener, | ||
private val turnEventListener: OmokTurnEventListener, | ||
private val rule: OmokRule | ||
) { | ||
fun run() { | ||
startEndEventListener.onStartGame() | ||
var curStoneColor: StoneColor = StoneColor.BLACK | ||
var curBoard = Board(BlackPlayer(), WhitePlayer(), rule) | ||
do { | ||
curBoard = takeTurn(curBoard, curStoneColor) | ||
startEndEventListener.onEndTurn(curBoard.getPlayers()) | ||
curStoneColor = curStoneColor.next() | ||
} while (curBoard.isRunning()) | ||
Comment on lines
+21
to
+25
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. 가급적 do, while은 사용을 지양하는면 좋습니다. 조건을 마지막에 확인하기 때문에 코드를 읽는데 어려움이 있고 가독성을 낮춥니다. var isRunning = curBoard.isRunning()
while (isRunning) {
...
isRunning = curBoard.isRunning()
}
|
||
if (curBoard.isLose()) startEndEventListener.onEndGame(curStoneColor) | ||
else 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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package domain.player | ||
|
||
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) { | ||
val isLose | ||
get() = state is LoseState | ||
|
||
override fun putStone(stone: Stone, otherStones: Stones, rule: OmokRule): Player { | ||
val blackStones = state.getAllStones() | ||
if (rule.check(blackStones, otherStones, stone)) { | ||
return BlackPlayer(LoseState(state.getAllStones())) | ||
} | ||
return BlackPlayer(state.add(stone, rule)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
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 { | ||
fun canPlace(): Boolean = state !is WinState && state !is LoseState | ||
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가 판단해야할 책임이 아닐까요? |
||
|
||
fun isPlaced(stone: Stone): Boolean = state.hasStone(stone) | ||
|
||
fun getPositions(): List<Position> = state.getPlaced() | ||
|
||
fun getLastStone(): Stone = state.getLastStone() | ||
|
||
fun getAllStones(): Stones = state.getAllStones() | ||
|
||
abstract fun putStone(stone: Stone, otherStones: Stones, rule: OmokRule): Player | ||
|
||
public override fun clone(): Player = super.clone() as Player | ||
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. Cloneable 인터페이스를 구현한 이유는 무엇인가요? 어떠한 기능을 수행했다면 구현체에서 그 책임을 가져야 하지 않을까요? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
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<Player>, 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, rule: OmokRule) : this( | ||
listOf( | ||
blackPlayer.clone(), | ||
whitePlayer.clone() | ||
), | ||
rule | ||
) | ||
|
||
fun putStone(stoneColor: StoneColor, stone: Stone): Players { | ||
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, | ||
) | ||
} | ||
} | ||
} | ||
Comment on lines
+22
to
+43
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. 플레이어들을 Player 타입으로 일반화 했지만 실제 구현은 블랙, 화이트에 대한 강한 의존성을 가지고 있습니다. 제가 생각한 플레이어들의 개념은 현재 턴의 플레이어가 해당 위치에 돌을 착수한다 라고 판단했습니다. 마지막에 착수한 플레이어를 Players에서 관리하도록 하면 어떨까요? private var latestPlayer: Player?
fun putStone(...) {
val othersStones = latestPlayer?.getAllStones()
val currentPlayer = nextPlayers()
latestPlayer = currentPlayer
...
} 불변 객체를 유지하려면, 살짝의 구현방법을 변경하면 간단하게 수정할 수 있습니다. |
||
|
||
fun getBlackPlayer(): Player = players.first { it is BlackPlayer } | ||
|
||
fun getWhitePlayer(): Player = players.first { it is WhitePlayer } | ||
|
||
fun canPlace(stone: Stone): Boolean = players.none { it.isPlaced(stone) } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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, rule: OmokRule): Player = | ||
WhitePlayer(state.add(stone, rule)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package domain.position | ||
|
||
data class Position(val row: Int, val col: Int) { | ||
init { | ||
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 ROW_OUT_OF_RANGE_ERROR_MESSAGE = "행의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" | ||
private const val COLUMN_OUT_OF_RANGE_ERROR_MESSAGE = "열의 범위는 ${MIN_BOUND}부터 ${MAX_BOUND}입니다" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
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.
모든 규칙은 적용되었지만... 미완성인걸까요!?