diff --git a/README.md b/README.md index 08a8c8a..5e5d409 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ -# android-omok-precourse \ No newline at end of file +# android-omok-precourse +## 게임기능 +### 화면 그리기 +- [x] 배경색을 가진 격자 그리기 +- [x] 격자 교차점을 터치했을 때 돌을 그리기 + - [x] 이미 돌이 있는 곳에는 돌을 놓을 수 없음 + - [x] 흑, 백 번갈아 가며 그리기 +### 게임 진행 +- [x] 최대 연결 수 계산하기 +- [x] 처음으로 5목 이상 배치시 게임 종료 + - [x] 끝나면 다시하기 여부 물어보기 + - [x] 재시작 시 맵 초기화 하기 \ No newline at end of file diff --git a/app/src/androidTest/java/nextstep/omok/OmokSystemAndroidTest.kt b/app/src/androidTest/java/nextstep/omok/OmokSystemAndroidTest.kt new file mode 100644 index 0000000..d7635c4 --- /dev/null +++ b/app/src/androidTest/java/nextstep/omok/OmokSystemAndroidTest.kt @@ -0,0 +1,138 @@ +package nextstep.omok + +import org.assertj.core.api.Assertions.* +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TableRow +import androidx.test.core.app.ActivityScenario +import org.junit.Test + +class OmokSystemAndroidTest { + + + @Test + fun getBoardImageViewTest() { + val activityScenario = ActivityScenario.launch(MainActivity::class.java) + activityScenario.onActivity { + val board = it.findViewById(R.id.board) + val omokSystem = OmokSystem(it, board) + val imageView: ImageView = (board.getChildAt(1) as TableRow).getChildAt(1) as ImageView + assertThat(omokSystem.getBoardImageView(1,1)).isEqualTo(imageView) + } + activityScenario.close() + } + + @Test + fun putStoneTest() { + val activityScenario = ActivityScenario.launch(MainActivity::class.java) + activityScenario.onActivity { + val board = it.findViewById(R.id.board) + val omokSystem = OmokSystem(it, board) + + // 지정한 위치에 착수 + setStoneBlack(omokSystem) + omokSystem.putStone(1, 1) + val oldStone = (board.getChildAt(1) as TableRow).getChildAt(1) as ImageView + assertThat(omokSystem.getBoardImageView(1,1)).isEqualTo(oldStone) + + // 이미 착수한 곳에 착수 시도: 기존의 돌이 유지되어야 함 + omokSystem.putStone(1, 1) + assertThat(omokSystem.getBoardImageView(1, 1)).isEqualTo(oldStone) + } + activityScenario.close() + } + + @Test + fun calculateCombo() { + val activityScenario = ActivityScenario.launch(MainActivity::class.java) + + activityScenario.onActivity { + val board = it.findViewById(R.id.board) + val omokSystem = OmokSystem(it, board) + + setStoneBlack(omokSystem) + omokSystem.putStone(1, 1) + setStoneBlack(omokSystem) + omokSystem.putStone(1, 2) + setStoneBlack(omokSystem) + omokSystem.putStone(1, 3) + setStoneBlack(omokSystem) + omokSystem.putStone(1, 4) + setStoneBlack(omokSystem) + omokSystem.putStone(1, 5) + assertThat(omokSystem.calculateCombo(1,1)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(1,2)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(1,3)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(1,4)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(1,5)).isEqualTo(5) + + setStoneWhite(omokSystem) + omokSystem.putStone(2, 2) + setStoneWhite(omokSystem) + omokSystem.putStone(3, 3) + setStoneWhite(omokSystem) + omokSystem.putStone(4, 4) + setStoneWhite(omokSystem) + omokSystem.putStone(5, 5) + setStoneWhite(omokSystem) + omokSystem.putStone(6, 6) + assertThat(omokSystem.calculateCombo(2,2)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(3,3)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(4,4)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(5,5)).isEqualTo(5) + assertThat(omokSystem.calculateCombo(6,6)).isEqualTo(5) + } + activityScenario.close() + } + + @Test + fun isGameEndTest() { + val activityScenario = ActivityScenario.launch(MainActivity::class.java) + + activityScenario.onActivity { + val board = it.findViewById(R.id.board) + val omokSystem = OmokSystem(it, board) + + assertThat(omokSystem.isGameEnd(omokSystem.scoreCombo - 1)).isEqualTo(false) + assertThat(omokSystem.isGameEnd(omokSystem.scoreCombo)).isEqualTo(true) + assertThat(omokSystem.isGameEnd(omokSystem.scoreCombo + 1)).isEqualTo(true) + } + activityScenario.close() + + } + + @Test + fun resetBoardTest() { + val activityScenario = ActivityScenario.launch(MainActivity::class.java) + + activityScenario.onActivity { + val board = it.findViewById(R.id.board) + val omokSystem = OmokSystem(it, board) + + // 판에 돌을 모두 착수 + for (i in 0..14) { + for (j in 0..14) { + omokSystem.putStone(i,j) + assertThat(omokSystem.getBoardImageView(i,j).drawable).isNotNull + } + } + omokSystem.resetBoard() + // 리셋되었는지 확인 + for (i in 0..14) { + for (j in 0..14) { + assertThat(omokSystem.getBoardImageView(i,j).drawable).isNull() + } + } + assertThat(omokSystem.nowStone).isEqualTo(omokSystem.BLACK) + } + activityScenario.close() + } + + fun setStoneBlack(omokSystem: OmokSystem) { + omokSystem.nowStone = omokSystem.BLACK + } + + fun setStoneWhite(omokSystem: OmokSystem) { + omokSystem.nowStone = omokSystem.WHITE + } +} \ No newline at end of file diff --git a/app/src/main/java/nextstep/omok/MainActivity.kt b/app/src/main/java/nextstep/omok/MainActivity.kt index e6cc7b8..d77af01 100644 --- a/app/src/main/java/nextstep/omok/MainActivity.kt +++ b/app/src/main/java/nextstep/omok/MainActivity.kt @@ -13,11 +13,7 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) val board = findViewById(R.id.board) - board - .children - .filterIsInstance() - .flatMap { it.children } - .filterIsInstance() - .forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } } + val omokSystem = OmokSystem(this, board) + omokSystem.registerClickListener() } } diff --git a/app/src/main/java/nextstep/omok/OmokSystem.kt b/app/src/main/java/nextstep/omok/OmokSystem.kt new file mode 100644 index 0000000..5add50b --- /dev/null +++ b/app/src/main/java/nextstep/omok/OmokSystem.kt @@ -0,0 +1,243 @@ +package nextstep.omok + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.Log +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TableRow +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.children +import androidx.core.view.size + +class OmokSystem ( + val context: Context, + val board: TableLayout) { + val BLACK = "흑" + val WHITE = "백" + val scoreCombo = 5 + private val rowSize: Int + private val colSize: Int + + var nowStone = BLACK + private val cache = Cache() + init { + rowSize = board.childCount + colSize = (board.getChildAt(0) as TableRow).childCount + } + + // 각 이미지뷰에 onClickListener를 부착하는 함수 + fun registerClickListener() { + board.children.forEachIndexed { row, tableRow -> + tableRow as TableRow + tableRow.children.forEachIndexed { col, imgView -> + imgView as ImageView + imgView.setOnClickListener { + performTurn(row, col) + } + } + } + } + + // 이미지뷰를 터치했을 때 수행할 한턴의 동작 + private fun performTurn(row: Int, col: Int) { + putStone(row, col) + val combo = calculateCombo(row, col) + if (isGameEnd(combo)) + alertEnd(row, col) + } + + // 돌을 착수하는 함수 + // performTurn 내부에서 호출 + fun putStone(row: Int, col: Int) { + val currView = getBoardImageView(row, col) + if (isPositionEmpty(currView)) { + if (nowStone == WHITE) { + currView.setImageDrawable(cache.stoneCache[cache.STONE_WHITE]) + nowStone = BLACK + } else { + currView.setImageDrawable(cache.stoneCache[cache.STONE_BLACK]) + nowStone = WHITE + } + } + } + + // 보드의 row, col 위치에 존재하는 imageView 반환 + fun getBoardImageView(row: Int, col: Int): ImageView { + return (board.getChildAt(row) as TableRow).getChildAt(col) as ImageView + } + + // 돌을 놓으려는 위치가 비어있는지 확인하는 함수 + // putStone 내부에서 호출 + fun isPositionEmpty(imgView: ImageView): Boolean { + if (imgView.drawable == null) + return true + else { + Toast.makeText(context, "이미 돌이 착수된 자리입니다!", Toast.LENGTH_SHORT).show() + return false + } + } + + // 콤보를 계산하는 함수 + // 4가지 방향에 대해 가장 큰 콤보를 반환 + fun calculateCombo(row: Int, col: Int): Int { + return maxOf( + checkVerticalCombo(row, col), + checkHorizontalCombo(row, col), + checkUpperRightCombo(row, col), + checkUpperLeftCombo(row, col) + ) + } + + // 4가지 방향 각각에 대해 콤보를 계산하는 함수 + // 각각의 함수는 calculateCombo함수 내부에서 호출되어 사용 + // 점수를 내는 기준인 scoreCombo개수까지만 인접한 돌을 셈 + // 수직 방향 + private fun checkVerticalCombo(row: Int, col: Int) : Int { + var combo = 1 + val targetStone = getBoardImageView(row, col) + + for (d in 1..= rowSize || + targetStone.drawable != getBoardImageView(row + d, col).drawable) + break + combo++ + } + + return combo + } + + // 수평 방향 + private fun checkHorizontalCombo(row: Int, col: Int) : Int { + var combo = 1 + val targetStone = getBoardImageView(row, col) + + for (d in 1..= colSize || + targetStone.drawable != getBoardImageView(row, col + d).drawable) + break + combo++ + } + + return combo + } + + // 오른쪽 위를 향하는 대각선 방향 + private fun checkUpperRightCombo(row: Int, col: Int) : Int { + var combo = 1 + val targetStone = getBoardImageView(row, col) + + for (d in 1..= rowSize || + targetStone.drawable != getBoardImageView(row + d, col - d).drawable) + break + combo++ + } + for (d in 1..= colSize || row - d < 0 || + targetStone.drawable != getBoardImageView(row - d, col + d).drawable) + break + combo++ + } + + return combo + } + + // 왼쪽 위를 향하는 대각선 방향 + private fun checkUpperLeftCombo(row: Int, col: Int) : Int { + var combo = 1 + val targetStone = getBoardImageView(row, col) + + for (d in 1..= colSize || row + d >= rowSize || + targetStone.drawable != getBoardImageView(row + d, col + d).drawable) + break + combo++ + } + for (d in 1..= scoreCombo + } + + // 게임 종료 알림 함수 + private fun alertEnd(row: Int, col: Int): Unit { + val targetStone = getBoardImageView(row, col) + AlertDialog.Builder(context).apply { + setCancelable(false) + setTitle("게임이 종료되었습니다! 😎") + if (targetStone.drawable == cache.stoneCache[cache.STONE_BLACK]) + setMessage("🎉${BLACK}⚫️이 승리했습니다!🎉\n재시작 하시겠습니까?") + else + setMessage("🎉${WHITE}⚪️️이 승리했습니다!🎉\n재시작 하시겠습니까?") + + setPositiveButton("네") { dialog, _ -> + resetBoard() + dialog.dismiss() + } + + setNegativeButton("아니요") { dialog, _ -> + dialog.dismiss() + } + }.create().show() + } + + /* 보드 초기화 함수 */ + fun resetBoard(): Unit { + board.children.forEach { tableRow -> + tableRow as TableRow + tableRow.children.forEach { imgView -> + imgView as ImageView + imgView.setImageDrawable(null) + } + } + nowStone = BLACK + } + + // 돌을 착수 할 때마다 Drawable을 얻어오는 동작을 줄이기 위해 캐시로 갖고있는 클래스 + // OmokSystem 객체가 생성될 때 내부에서 생성 + private inner class Cache() { + // 돌 관련 상수 + val STONE_BLACK = R.drawable.black_stone + val STONE_WHITE = R.drawable.white_stone + + // 캐시 + val stoneCache: HashMap + + init { + stoneCache = createStoneCache() + } + + private fun createStoneCache(): HashMap { + val res = HashMap() + + res[STONE_WHITE] = ResourcesCompat.getDrawable(context.resources, STONE_WHITE, null) + res[STONE_BLACK] = ResourcesCompat.getDrawable(context.resources, STONE_BLACK, null) + + return res + } + } +} \ No newline at end of file