From c796537261c88649c9fd49ba8a1f655f1a475281 Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:35:25 +0900 Subject: [PATCH 01/10] docs: update README.md --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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 From 86321321c6f7d585f4d00eb8bbb1c4878ff6627c Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:40:10 +0900 Subject: [PATCH 02/10] feat: initialize game board --- .../main/java/nextstep/omok/MainActivity.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index e6cc7b8..1e7f7a7 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -8,16 +8,22 @@ 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) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + initializeBoard() + } - 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 initializeBoard() { + findViewById(R.id.board).apply { + children.filterIsInstance().forEach { row -> + row.children.filterIsInstance().forEach {view -> + view.setImageResource(0) + } + } + } } } From 8e200a288bddfc207b47e914b43ec91c778328fa Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:43:31 +0900 Subject: [PATCH 03/10] feat: place stone --- app/src/main/java/nextstep/omok/MainActivity.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index 1e7f7a7..ac83a43 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -11,6 +11,8 @@ 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) @@ -26,4 +28,9 @@ class MainActivity : AppCompatActivity() { } } } + + 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) + } } From 44fb6d9a0d30c456a2ae88cca6f60e7c1c3f4f63 Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:45:00 +0900 Subject: [PATCH 04/10] feat: place black/white stones alternately --- app/src/main/java/nextstep/omok/MainActivity.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index ac83a43..7f0a4a6 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -29,6 +29,11 @@ class MainActivity : AppCompatActivity() { } } + private fun onCellClicked(view: ImageView, rowIndex: Int, colIndex: Int) { + placeStone(view, rowIndex, colIndex) + isBlackTurn = !isBlackTurn + } + 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) From 642bf3b8c453c1b750502b37437776ab255949a8 Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:47:12 +0900 Subject: [PATCH 05/10] fix: initialize board --- app/src/main/java/nextstep/omok/MainActivity.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index 7f0a4a6..1c7224e 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -21,8 +21,9 @@ class MainActivity : AppCompatActivity() { private fun initializeBoard() { findViewById(R.id.board).apply { - children.filterIsInstance().forEach { row -> - row.children.filterIsInstance().forEach {view -> + children.filterIsInstance().forEachIndexed { rowIndex, row -> + row.children.filterIsInstance().forEachIndexed { colIndex, view -> + view.setOnClickListener { onCellClicked(view, rowIndex, colIndex) } view.setImageResource(0) } } From a86db7e75de3fa87d9289aed6659017169b4447e Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:49:57 +0900 Subject: [PATCH 06/10] feat: prevent being placed in duplicated cell --- app/src/main/java/nextstep/omok/MainActivity.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index 1c7224e..9148e65 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -31,10 +31,17 @@ class MainActivity : AppCompatActivity() { } private fun onCellClicked(view: ImageView, rowIndex: Int, colIndex: Int) { + if (!isCellOccupied(rowIndex, colIndex)) { + return + } placeStone(view, rowIndex, colIndex) 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) From cff4f5d1bda78fa7dda4b7c847a2a0c3326d7109 Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 04:54:34 +0900 Subject: [PATCH 07/10] feat: count stones to check win --- app/src/main/java/nextstep/omok/MainActivity.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index 9148e65..bf9ed23 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -46,4 +46,18 @@ class MainActivity : AppCompatActivity() { BOARD_ARRAY[rowIndex][colIndex] = if (isBlackTurn) 1 else 2 view.setImageResource(if (isBlackTurn) R.drawable.black_stone else R.drawable.white_stone) } + + 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 + } + } From a6840a53138cb8b6604dc35ce8e9bf7bcf017acb Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 05:03:05 +0900 Subject: [PATCH 08/10] feat: check win condition --- app/src/main/java/nextstep/omok/MainActivity.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index bf9ed23..7334f85 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -31,11 +31,15 @@ class MainActivity : AppCompatActivity() { } private fun onCellClicked(view: ImageView, rowIndex: Int, colIndex: Int) { - if (!isCellOccupied(rowIndex, colIndex)) { + if (isCellOccupied(rowIndex, colIndex)) { return } placeStone(view, rowIndex, colIndex) - isBlackTurn = !isBlackTurn + if (checkWin(rowIndex, colIndex)) { + return + } else { + isBlackTurn = !isBlackTurn + } } private fun isCellOccupied(rowIndex: Int, colIndex: Int): Boolean { @@ -47,6 +51,15 @@ class MainActivity : AppCompatActivity() { 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 From f06737b403a1255ce22c3eb10cf8f5f3742f97bb Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 05:16:59 +0900 Subject: [PATCH 09/10] feat: reset game board --- app/src/main/java/nextstep/omok/MainActivity.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index 7334f85..7216b26 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -36,7 +36,7 @@ class MainActivity : AppCompatActivity() { } placeStone(view, rowIndex, colIndex) if (checkWin(rowIndex, colIndex)) { - return + resetBoard() } else { isBlackTurn = !isBlackTurn } @@ -73,4 +73,13 @@ class MainActivity : AppCompatActivity() { return count } + 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 + } } From 27fdf17bceeb4a288efebca7fe10fab4a1286f07 Mon Sep 17 00:00:00 2001 From: ddeonseo Date: Mon, 10 Jun 2024 05:52:09 +0900 Subject: [PATCH 10/10] feat: add test code with reflection --- .../java/nextstep/omok/MainActivityTest.kt | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 app/src/androidTest/java/nextstep/omok/MainActivityTest.kt 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 + } + } + } + } +}