diff --git a/README.md b/README.md index 08a8c8a..6306685 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# android-omok-precourse \ No newline at end of file +# android-omok-precourse + +오목은 두 사람이 번갈아 돌을 놓아 가로나 세로, 대각선으로 다섯 개의 연속된 돌을 먼저 만들면 승리하는 게임이다 +초기 코드를 실행해보면 1) 오목판이 구현되어 있으며 2) 흑돌을 원하는 위치에 둘 수 있는 상황이다 + +## 구현할 기능 +1. 오목판 초기화(initializeBoard) +2. 돌 번갈아 놓기(onCellClikced, placeStone) +3. 예외 처리 - 같은 위치에 돌을 놓지 못하게 하기(isCellOccupied) +4. 승리 조건 확인(checkWin/countStones) +5. 게임 종료 후 보드 초기화(resetBoard) \ No newline at end of file diff --git a/app/src/androidTest/java/nextstep/omok/MainActivityTest.kt b/app/src/androidTest/java/nextstep/omok/MainActivityTest.kt new file mode 100644 index 0000000..f420b1f --- /dev/null +++ b/app/src/androidTest/java/nextstep/omok/MainActivityTest.kt @@ -0,0 +1,141 @@ +package nextstep.omok + +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TableRow +import androidx.core.view.children +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + private lateinit var scenario: ActivityScenario + + @Before + fun setUp() { + scenario = ActivityScenario.launch(MainActivity::class.java) + } + + private fun getPrivateField(instance: Any, fieldName: String): T { + val field = instance.javaClass.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(instance) as T + } + + private fun callPrivateMethod(instance: Any, methodName: String, vararg args: Any?): Any? { + val parameterTypes = args.map { + when (it) { + is Int -> Int::class.javaPrimitiveType + else -> it?.javaClass + } + }.toTypedArray() + val method = instance.javaClass.getDeclaredMethod(methodName, *parameterTypes) + method.isAccessible = true + return method.invoke(instance, *args) + } + + + @Test + fun testInitializeBoard() { + scenario.onActivity { activity -> + val board = activity.findViewById(R.id.board) + board.children.filterIsInstance().forEach { row -> + row.children.filterIsInstance().forEach { view -> + assertThat(view.drawable).isNull() // ImageView should be empty + } + } + } + } + + @Test + fun testPlaceStone() { + scenario.onActivity { activity -> + val row = 0 + val col = 0 + val board = activity.findViewById(R.id.board) + val cell = (board.getChildAt(row) as TableRow).getChildAt(col) as ImageView + + activity.runOnUiThread { + cell.performClick() + } + + val boardArray = getPrivateField>(activity, "BOARD_ARRAY") + assertThat(boardArray[row][col]).isEqualTo(1) // Black stone should be placed + + activity.runOnUiThread { + cell.performClick() + } + + assertThat(boardArray[row][col]).isEqualTo(1) // Should not change on second click + } + } + + @Test + fun testSwitchTurns() { + scenario.onActivity { activity -> + val row = 0 + val col = 0 + val nextRow = 0 + val nextCol = 1 + val board = activity.findViewById(R.id.board) + val cell1 = (board.getChildAt(row) as TableRow).getChildAt(col) as ImageView + val cell2 = (board.getChildAt(nextRow) as TableRow).getChildAt(nextCol) as ImageView + + activity.runOnUiThread { + cell1.performClick() + } + + val boardArray = getPrivateField>(activity, "BOARD_ARRAY") + assertThat(boardArray[row][col]).isEqualTo(1) // Black stone + + activity.runOnUiThread { + cell2.performClick() + } + + assertThat(boardArray[nextRow][nextCol]).isEqualTo(2) // White stone + } + } + + @Test + fun testCheckWin() { + scenario.onActivity { activity -> + val boardArray = getPrivateField>(activity, "BOARD_ARRAY") + boardArray[0][0] = 1 + boardArray[0][1] = 1 + boardArray[0][2] = 1 + boardArray[0][3] = 1 + boardArray[0][4] = 1 + + val hasWon = callPrivateMethod(activity, "checkWin", 0, 4) as Boolean + assertThat(hasWon).isTrue() // Black wins + } + } + + @Test + fun testResetBoard() { + scenario.onActivity { activity -> + val boardArray = getPrivateField>(activity, "BOARD_ARRAY") + boardArray[0][0] = 1 + + callPrivateMethod(activity, "resetBoard") + + boardArray.forEach { row -> + row.forEach { cell -> + assertThat(cell).isEqualTo(0) // Board should be reset to empty + } + } + + val board = activity.findViewById(R.id.board) + board.children.filterIsInstance().forEach { row -> + row.children.filterIsInstance().forEach { view -> + assertThat(view.drawable).isNull() // ImageView should be empty + } + } + } + } +} diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index e6cc7b8..7216b26 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -8,16 +8,78 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.children class MainActivity : AppCompatActivity() { + + private val BOARD_SIZE = 15 + private val BOARD_ARRAY = Array(BOARD_SIZE) { IntArray(BOARD_SIZE) } + private var isBlackTurn = true + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + initializeBoard() + } + + private fun initializeBoard() { + findViewById(R.id.board).apply { + children.filterIsInstance().forEachIndexed { rowIndex, row -> + row.children.filterIsInstance().forEachIndexed { colIndex, view -> + view.setOnClickListener { onCellClicked(view, rowIndex, colIndex) } + view.setImageResource(0) + } + } + } + } + + private fun onCellClicked(view: ImageView, rowIndex: Int, colIndex: Int) { + if (isCellOccupied(rowIndex, colIndex)) { + return + } + placeStone(view, rowIndex, colIndex) + if (checkWin(rowIndex, colIndex)) { + resetBoard() + } else { + isBlackTurn = !isBlackTurn + } + } + + private fun isCellOccupied(rowIndex: Int, colIndex: Int): Boolean { + return BOARD_ARRAY[rowIndex][colIndex] != 0 + } + + private fun placeStone(view: ImageView, rowIndex: Int, colIndex: Int) { + BOARD_ARRAY[rowIndex][colIndex] = if (isBlackTurn) 1 else 2 + view.setImageResource(if (isBlackTurn) R.drawable.black_stone else R.drawable.white_stone) + } + + private fun checkWin(row: Int, col: Int): Boolean { + val player = BOARD_ARRAY[row][col] + val directions = listOf(Pair(1, 0), Pair(0, 1), Pair(1, 1), Pair(1, -1)) + return directions.any { direction -> + countStones(row, col, direction.first, direction.second, player) + + countStones(row, col, -direction.first, -direction.second, player) > 3 + } + } + + private fun countStones(row: Int, col: Int, dRow: Int, dCol: Int, player: Int): Int { + var count = 0 + var r = row + dRow + var c = col + dCol + + while (r in 0 until BOARD_SIZE && c in 0 until BOARD_SIZE && BOARD_ARRAY[r][c] == player) { + count++ + r += dRow + c += dCol + } + return count + } - val board = findViewById(R.id.board) - board - .children - .filterIsInstance() - .flatMap { it.children } - .filterIsInstance() - .forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } } + private fun resetBoard() { + BOARD_ARRAY.forEach { row -> row.fill(0) } + findViewById(R.id.board).children.filterIsInstance().forEach { row -> + row.children.filterIsInstance().forEach { view -> + view.setImageResource(0) + } + } + isBlackTurn = true } }