diff --git a/README.md b/README.md index 08a8c8a..7b163e5 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# android-omok-precourse \ No newline at end of file +# android-omok-precourse + +# 오목 게임 + +이 프로젝트는 안드로이드를 위한 코틀린으로 구현된 간단한 오목(Omok) 게임이다. +이 게임은 두 명의 플레이어가 15 X 15 오목 판에 검은 돌과 흰 돌을 놓을 수 있게 하며, 전통적인 오목 규칙에 따라 진행된다. 추가로, 흑돌은 금수 규칙이 적용된다. + + +## 주요 기능 + +- **두 명의 플레이어**: 흑돌과 백돌. + +- **기본 오목 규칙**: 가로, 세로 또는 대각선으로 다섯 개의 돌을 먼저 놓으면 승리 +- **흑돌 금수 규칙**: + - "삼삼" (Double Three): 두 개의 열린 삼을 만드는 돌 놓기 금지 + + - "사사" (Double Four): 두 개의 열린 사를 만드는 돌 놓기 금지 + - "장목" (Overline): 여섯 개 이상의 돌을 연속으로 놓는 것 금지 +- **백돌 자유 규칙**: 백돌은 아무 곳에나 놓을 수 있음 +- **승리 및 금수 위치 알림**: 승리 조건 충족 시 및 금수 위치에 돌을 놓으려 할 때 토스트 메시지로 알림 + + + +## 파일 설명 + +### MainActivity.kt + +`MainActivity`는 안드로이드 액티비티로서, 게임 보드를 설정하고 사용자 입력을 처리한다. 게임의 주요 로직은 `OmokGame` 클래스로 구현한다. + + +### OmokGame.kt + +`OmokGame` 클래스는 게임의 상태를 관리하고, 금수 규칙과 승리 조건을 확인하는 로직을 포함한다. +- 보드에 돌을 놓기 +- 플레이어 턴 전환 +- 가로, 세로, 대각선 승리 조건 확인 +- 삼삼, 사사, 장목과 같은 금수 규칙 적용 + +### GameLogicTest.kt + +`GameLogicTest` 클래스는 JUnit 5와 AssertJ를 사용하여 `OmokGame`의 주요 기능을 테스트. 삼삼, 사사, 장목 금수 규칙과 승리 조건을 검증한다. + +- 가로 승리 조건 +- 세로 승리 조건 +- 대각선 승리 조건 (우하향, 우상향) +- 흑돌 금수 규칙 (삼삼, 사사, 장목) + + diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index e6cc7b8..28d506c 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -4,20 +4,54 @@ import android.os.Bundle import android.widget.ImageView import android.widget.TableLayout import android.widget.TableRow +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.children class MainActivity : AppCompatActivity() { + + private val boardSize = 15 + private lateinit var game: OmokGame + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val board = findViewById(R.id.board) - board - .children + game = OmokGame(boardSize) + setupBoard() + } + + private fun setupBoard() { + val boardView = findViewById(R.id.board) + boardView.children .filterIsInstance() .flatMap { it.children } .filterIsInstance() - .forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } } + .forEachIndexed { index, view -> + val row = index / boardSize + val col = index % boardSize + view.setOnClickListener { handleCellClick(row, col, view) } + } + } + + private fun handleCellClick(row: Int, col: Int, view: ImageView) { + if (game.board[row][col] == 0) { + if (game.isForbidden(row, col)) { + Toast.makeText(this, "Forbidden move!", Toast.LENGTH_SHORT).show() + return + } + game.placeStone(row, col) + view.setImageResource(if (game.currentPlayer == 1) R.drawable.black_stone else R.drawable.white_stone) + if (game.checkWin(row, col, game.currentPlayer)) { + showWinner(game.currentPlayer) + } else { + game.switchPlayer() + } + } + } + + private fun showWinner(player: Int) { + val winner = if (player == 1) "Black" else "White" + Toast.makeText(this, "$winner wins!", Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/nextstep/omok/OmokGame.kt b/app/src/main/java/nextstep/omok/OmokGame.kt new file mode 100644 index 0000000..7ccc968 --- /dev/null +++ b/app/src/main/java/nextstep/omok/OmokGame.kt @@ -0,0 +1,123 @@ +package nextstep.omok + +class OmokGame(private val boardSize: Int) { + + val board = Array(boardSize) { Array(boardSize) { 0 } } + var currentPlayer = 1 // 1 for black, 2 for white + + fun placeStone(row: Int, col: Int): Boolean { + if (board[row][col] == 0) { + board[row][col] = currentPlayer + return true + } + return false + } + + fun switchPlayer() { + currentPlayer = if (currentPlayer == 1) 2 else 1 + } + + fun checkWin(row: Int, col: Int, player: Int): Boolean { + return checkDirection(row, col, player, 0, 1) >= 5 || // Horizontal + checkDirection(row, col, player, 1, 0) >= 5 || // Vertical + checkDirection(row, col, player, 1, 1) >= 5 || // Diagonal down-right + checkDirection(row, col, player, 1, -1) >= 5 // Diagonal up-right + } + + fun isForbidden(row: Int, col: Int): Boolean { + if (currentPlayer == 1) { + return isDoubleThree(row, col) || isDoubleFour(row, col) || isSixInARow(row, col) + } + return false + } + + private fun checkDirection(row: Int, col: Int, player: Int, dRow: Int, dCol: Int): Int { + var count = 1 + count += countStones(row, col, player, dRow, dCol) + count += countStones(row, col, player, -dRow, -dCol) + return count + } + + private fun countStones(row: Int, col: Int, player: Int, dRow: Int, dCol: Int): Int { + var count = 0 + var r = row + dRow + var c = col + dCol + while (r in 0 until boardSize && c in 0 until boardSize && board[r][c] == player) { + count++ + r += dRow + c += dCol + } + return count + } + + fun isDoubleThree(row: Int, col: Int): Boolean { + return countOpenThrees(row, col, 1, 0) + countOpenThrees(row, col, 0, 1) + + countOpenThrees(row, col, 1, 1) + countOpenThrees(row, col, 1, -1) > 1 + } + + private fun countOpenThrees(row: Int, col: Int, dRow: Int, dCol: Int): Int { + var count = 0 + if (isOpenThree(row, col, dRow, dCol)) count++ + if (isOpenThree(row, col, -dRow, -dCol)) count++ + return count + } + + private fun isOpenThree(row: Int, col: Int, dRow: Int, dCol: Int): Boolean { + val sequence = mutableListOf() + for (i in -4..4) { + val r = row + i * dRow + val c = col + i * dCol + if (r in 0 until boardSize && c in 0 until boardSize) { + sequence.add(board[r][c]) + } else { + sequence.add(-1) + } + } + val pattern = listOf(0, 1, 1, 1, 0) + for (i in 0..sequence.size - pattern.size) { + if (sequence.subList(i, i + pattern.size) == pattern) { + return true + } + } + return false + } + + fun isDoubleFour(row: Int, col: Int): Boolean { + return countOpenFours(row, col, 1, 0) + countOpenFours(row, col, 0, 1) + + countOpenFours(row, col, 1, 1) + countOpenFours(row, col, 1, -1) > 1 + } + + private fun countOpenFours(row: Int, col: Int, dRow: Int, dCol: Int): Int { + var count = 0 + if (isOpenFour(row, col, dRow, dCol)) count++ + if (isOpenFour(row, col, -dRow, -dCol)) count++ + return count + } + + private fun isOpenFour(row: Int, col: Int, dRow: Int, dCol: Int): Boolean { + val sequence = mutableListOf() + for (i in -4..4) { + val r = row + i * dRow + val c = col + i * dCol + if (r in 0 until boardSize && c in 0 until boardSize) { + sequence.add(board[r][c]) + } else { + sequence.add(-1) + } + } + val pattern = listOf(0, 1, 1, 1, 1, 0) + for (i in 0..sequence.size - pattern.size) { + if (sequence.subList(i, i + pattern.size) == pattern) { + return true + } + } + return false + } + + fun isSixInARow(row: Int, col: Int): Boolean { + return checkDirection(row, col, currentPlayer, 0, 1) > 5 || + checkDirection(row, col, currentPlayer, 1, 0) > 5 || + checkDirection(row, col, currentPlayer, 1, 1) > 5 || + checkDirection(row, col, currentPlayer, 1, -1) > 5 + } +} diff --git a/app/src/test/java/nextstep/omok/GameLogicTest.kt b/app/src/test/java/nextstep/omok/GameLogicTest.kt new file mode 100644 index 0000000..f64d5bc --- /dev/null +++ b/app/src/test/java/nextstep/omok/GameLogicTest.kt @@ -0,0 +1,93 @@ +package nextstep.omok + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class GameLogicTest { + + private val boardSize = 15 + private lateinit var game: OmokGame + + @BeforeEach + fun setUp() { + game = OmokGame(boardSize) + } + + @Test + fun `horizontal win check`() { + for (i in 0 until 5) { + game.board[0][i] = 1 + } + val result = game.checkWin(0, 2, 1) + assertThat(result).isTrue() + } + + @Test + fun `vertical win check`() { + for (i in 0 until 5) { + game.board[i][0] = 1 + } + val result = game.checkWin(2, 0, 1) + assertThat(result).isTrue() + } + + @Test + fun `diagonal down-right win check`() { + for (i in 0 until 5) { + game.board[i][i] = 1 + } + val result = game.checkWin(2, 2, 1) + assertThat(result).isTrue() + } + + @Test + fun `diagonal up-right win check`() { + for (i in 0 until 5) { + game.board[4 - i][i] = 1 + } + val result = game.checkWin(2, 2, 1) + assertThat(result).isTrue() + } + + @Test + fun `double three forbidden move`() { + // Set up a double three situation + game.board[1][2] = 1 + game.board[1][3] = 1 + game.board[1][5] = 1 + game.board[1][6] = 1 + game.board[2][3] = 1 + game.board[3][3] = 1 + game.board[4][3] = 1 + + val result = game.isDoubleThree(1, 4) + assertThat(result).isTrue() + } + + @Test + fun `double four forbidden move`() { + // Set up a double four situation + game.board[1][2] = 1 + game.board[1][3] = 1 + game.board[1][4] = 1 + game.board[1][6] = 1 + game.board[1][7] = 1 + game.board[1][8] = 1 + game.board[0][5] = 1 + game.board[2][5] = 1 + + val result = game.isDoubleFour(1, 5) + assertThat(result).isTrue() + } + + @Test + fun `six in a row forbidden move`() { + // Set up a six in a row situation + for (i in 0 until 6) { + game.board[0][i] = 1 + } + val result = game.isSixInARow(0, 2) + assertThat(result).isTrue() + } +}