Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# android-omok-precourse
# Android Omok Precourse

## 구현해야 할 기능 목록

### 1. 게임 레이아웃
- **15x15 그리드**: 제공된 이미지 소스를 이용해 15x15 바둑판 이미지를 구현합니다.
- **흑돌, 백돌**: 제공된 이미지 소스를 이용해 플레이어에 의해 놓일 바둑돌 이미지를 구현합니다.

### 2. 게임 로직
- **턴 관리**: 흑돌을 시작으로 두 플레이어가 번갈아 가며 턴을 진행합니다. 턴의 수를 기록하는 변수가 필요합니다.
- **배치 제한**: 돌은 비어 있는 위치에만 놓을 수 있어야 합니다.
- **턴 종료**: 방금 놓인 돌을 기준으로 승리 감지 로직을 실행합니다.

### 3. 승리 조건
- **승리 감지**: 플레이어가 가로, 세로 또는 대각선으로 다섯 개의 돌을 일렬로 배치하면 승리를 감지합니다.

### 4. 게임 종료 및 재시작
- **승리 Dialog**: 승리 감지 시 승리한 사용자를 알려줍니다.
- **재시작 옵션**: 현재 게임이 끝나면 새 게임을 시작하거나 앱을 종료할 수 있습니다.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ dependencies {
androidTestImplementation("io.kotest:kotest-runner-junit5:5.8.0")
androidTestImplementation("de.mannodermaus.junit5:android-test-core:1.3.0")
androidTestRuntimeOnly("de.mannodermaus.junit5:android-test-runner:1.3.0")
}
}
89 changes: 89 additions & 0 deletions app/src/androidTest/java/nextstep/omok/MainActivityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package nextstep.omok

import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Test
fun testPlaceStone() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
// 보드의 (1, 1) 위치를 클릭하여 돌을 둠
val board = activity.findViewById<TableLayout>(R.id.board)
val row = board.getChildAt(1) as TableRow
val cell = row.getChildAt(1) as ImageView
cell.performClick()

// 클릭한 위치에 올바른 돌이 놓였는지 확인
val drawable = cell.drawable
assert(drawable != null) { "Drawable should not be null" }
}
}
}

@Test
fun testShowGameOverDialog() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity -> placeStonesToWin(activity) } // Dialog가 화면에 표시되는지 확인
onView(withText("게임 종료")).inRoot(isDialog()).check(matches(isDisplayed())) // 게임 종료 대화상자 내의 승리 메시지가 정확하게 표시되는지 확인
onView(withText("흑이 승리했습니다. 새 게임을 진행하시겠습니까?")).inRoot(isDialog()).check(matches(isDisplayed()))
}
}

@Test
fun testRestartGame() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity -> placeStonesToWin(activity) }
onView(withText("게임 종료")).inRoot(isDialog()).check(matches(isDisplayed())) // Dialog가 화면에 표시되는지 확인
onView(withText("Yes")).perform(click()) // "Yes" 버튼을 눌러 새로운 게임 시작
onView(withId(R.id.board)).check { view, _ ->
val board = view as TableLayout
for (rowIndex in 0 until board.childCount) {
val row = board.getChildAt(rowIndex) as TableRow
for (columnIndex in 0 until row.childCount) {
val cell = row.getChildAt(columnIndex) as ImageView
assert(cell.drawable == null) { "Board should be reset, but found a stone at ($rowIndex, $columnIndex)" }
}
}
}// 새로운 게임이 시작되었는지 확인 (보드가 초기화되었는지 확인)
}
}

@Test
fun testEndGame() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity -> placeStonesToWin(activity) }
onView(withText("게임 종료")).inRoot(isDialog()).check(matches(isDisplayed())) // Dialog가 화면에 표시되는지 확인
onView(withText("No")).perform(click()) // "No" 버튼을 눌러 앱 종료
scenario.onActivity { activity -> assert(activity.isFinishing || activity.isDestroyed) { "Activity should be finishing or destroyed" } }// 액티비티가 종료되었는지 확인
}
}

private fun placeStonesToWin(activity: MainActivity) {
for (i in 0..4) {
val cellBlack = getImageViewAt(activity, 0, i)
activity.placeStone(cellBlack, 0, i)
if (i < 4) {
val cellWhite = getImageViewAt(activity, 1, i)
activity.placeStone(cellWhite, 1, i)
}
}
} // 흑이 승리하는 시나리오

private fun getImageViewAt(activity: MainActivity, row: Int, col: Int): ImageView {
val board = activity.findViewById<TableLayout>(R.id.board)
val tableRow = board.getChildAt(row) as TableRow
return tableRow.getChildAt(col) as ImageView
}
}
90 changes: 82 additions & 8 deletions app/src/main/java/nextstep/omok/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,94 @@ import android.os.Bundle
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children

class MainActivity : AppCompatActivity() {
private var turn = 0
private val boardSize = 15
private var table = Array(boardSize) { IntArray(boardSize) { 0 } }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupBoard()
}

private fun setupBoard() {
val board = findViewById<TableLayout>(R.id.board)
board
.children
.filterIsInstance<TableRow>()
.flatMap { it.children }
.filterIsInstance<ImageView>()
.forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } }
}
}
board.children.filterIsInstance<TableRow>().forEachIndexed { rowIndex, tableRow ->
setupRow(tableRow, rowIndex)
}
}

private fun setupRow(tableRow: TableRow, rowIndex: Int) {
tableRow.children.filterIsInstance<ImageView>().forEachIndexed { columnIndex, imageView ->
imageView.setOnClickListener {
placeStone(imageView, rowIndex, columnIndex)
}
}
}

fun placeStone(view: ImageView, row: Int, col: Int) {
if (table[row][col] == 0) {
val stoneResource = getStoneResource()
view.setImageResource(stoneResource)
updateBoardState(row, col)
if (checkWin(row, col)) handleWin()
}
}

private fun getStoneResource(): Int = if (turn % 2 == 0) R.drawable.black_stone else R.drawable.white_stone

private fun updateBoardState(row: Int, col: Int) {
table[row][col] = if (turn % 2 == 0) 1 else 2
turn++
}

private fun countStones(row: Int, col: Int, dx: Int, dy: Int): Int {
return 1 + countDirection(row, col, dx, dy) + countDirection(row, col, -dx, -dy)
}

private fun countDirection(row: Int, col: Int, dx: Int, dy: Int): Int {
var r = row + dx
var c = col + dy
var count = 0
while (r in 0 until boardSize && c in 0 until boardSize && table[r][c] == table[row][col]) {
count++
r += dx
c += dy
}
return count
}

private fun checkWin(row: Int, col: Int): Boolean {
val directions = listOf(listOf(0, 1), listOf(1, 0), listOf(1, 1), listOf(1, -1))
return directions.any { countStones(row, col, it[0], it[1]) >= 5 }
}

private fun handleWin() {
val winner = getWinner()
showWinDialog(winner)
}

private fun getWinner() = if (turn % 2 == 1) "흑" else "백"

private fun showWinDialog(winner: String) {
runOnUiThread {
AlertDialog.Builder(this@MainActivity).apply {
setTitle("게임 종료")
setMessage("${winner}이 승리했습니다. 새 게임을 진행하시겠습니까?")
setupDialogButtons()
setCancelable(false)
show()
}
}
}

private fun AlertDialog.Builder.setupDialogButtons() {
setNegativeButton("No") { _,_ -> finish() }
setPositiveButton("Yes") { _,_ -> finish(); startActivity(intent) }
}
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.3.1" apply false
id("com.android.application") version "8.1.3" apply false
id("org.jetbrains.kotlin.android") version "1.9.0" apply false
id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false
}
Expand Down