Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[전남대 Android_이민서] 미션 제출합니다. #23

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# android-omok-precourse
# android-omok-precourse
# 오목(android-omok-precourse)

## 기능 요구 사항
### 규칙
- 흑돌 먼저 실행
- 흑돌: 1 / 백돌: 2
- 5개 이상 연속 시 승리

## 구현 기능 목록
1. 보드 초기화
2. 플레이어: 빈 칸 클릭하여 돌 놓기
- ImageView -> black_stone.xml & white_stone.xml
- 플레이어 전환: 흑돌 -> 백돌 -> 흑돌 -> 백돌 -> ...
- 상태 업데이트(해당 위치에 플레이어1 or 플레이어2)
- 승리 조건 만족 여부 확인
3. 승리 조건
- 이동 후 승리 조건 확인
- 가로 || 세로 || 대각선 -> 5개 이상 연속된 돌(같은 색)
4. 무승부 조건
- 모든 칸 채워졌으나 승리 조건 불만족 시
5. 종료 조건
- 5개 이상 연속된 돌 존재 시
- 텍스트 뷰(or 토스트 메세지) 통해 승리한 플레이어 표시(ex. 흑돌(플레이어1)/백돌(플레이어2) 승리)
- 게임 종료 후 빈 칸 클릭 X
- 다시 시작 버튼 -> 초기화(게임 재시작)
117 changes: 117 additions & 0 deletions app/src/androidTest/java/nextstep/omok/TestCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

package nextstep.omok

import android.view.WindowManager
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.Toast
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.assertj.core.api.Assertions.assertThat
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.espresso.Root

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

// 돌 놓기 테스트
@Test
fun testPlaceStone() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
val board = activity.findViewById<TableLayout>(R.id.board)
val cell = (board.getChildAt(7) as TableRow).getChildAt(7) as ImageView

activity.runOnUiThread {
activity.onCellClick(cell, 7, 7)
}

val drawable = cell.drawable

assertThat(drawable).isNotNull
assertThat(activity.boardState[7][7]).isEqualTo(1)
}
}
}

// 플레이어 순서 변경 테스트
@Test
fun testSwitchPlayer() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
activity.isBlackTurn = true

val cell1 = ImageView(activity)
activity.runOnUiThread {
activity.onCellClick(cell1, 0, 0)
}
assertThat(activity.isBlackTurn).isFalse

val cell2 = ImageView(activity)
activity.runOnUiThread {
activity.onCellClick(cell2, 0, 1)
}
assertThat(activity.isBlackTurn).isTrue
}
}
}

// 보드 초기화 테스트
@Test
fun testBoardReset() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity ->
val board = activity.findViewById<TableLayout>(R.id.board)
activity.runOnUiThread {
activity.onCellClick((board.getChildAt(7) as TableRow).getChildAt(7) as ImageView, 7, 7)
activity.onCellClick((board.getChildAt(8) as TableRow).getChildAt(8) as ImageView, 8, 8)
}

activity.runOnUiThread {
activity.resetBoard(board)
}

for (i in 0 until board.childCount) {
val row = board.getChildAt(i) as TableRow
for (j in 0 until row.childCount) {
val cell = row.getChildAt(j) as ImageView
assertThat(cell.drawable).isNull()
}
}

for (row in activity.boardState) {
for (cell in row) {
assertThat(cell).isEqualTo(0)
}
}
assertThat(activity.isBlackTurn).isTrue
assertThat(activity.gameEnded).isFalse
}
}
}
}

// 토스트 메시지 확인
class ToastMatcher : TypeSafeMatcher<Root?>() {
override fun describeTo(description: Description) {
description.appendText("is toast")
}

public override fun matchesSafely(root: Root?): Boolean {
val type = root?.windowLayoutParams?.get()?.type
if (type == WindowManager.LayoutParams.TYPE_TOAST) {
val windowToken = root.decorView.windowToken
val appToken = root.decorView.applicationWindowToken
return windowToken === appToken
}
return false
}
}
108 changes: 106 additions & 2 deletions app/src/main/java/nextstep/omok/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,124 @@ import android.os.Bundle
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.Toast
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import android.util.Log

class MainActivity : AppCompatActivity() {

var isBlackTurn = true //플레이어1(흑돌) or 플레이어2(백돌) 여부
val boardState = Array(15) { IntArray(15) {0} } //보드 상태(2차원 배열), 0: 빈 칸, 1: 흑돌, 2: 백돌
var gameEnded = false //게임 종료 여부

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

val board = findViewById<TableLayout>(R.id.board)
val board = findViewById<TableLayout>(R.id.board) //보드
val restartButton = findViewById<Button>(R.id.restartButton) //Restart Button

setBoard(board) //보드의 각 칸에 클릭 리스너 설정
setRestartButton(restartButton, board) //Restart Button 클릭 리스너 설정
/*
board
.children
.filterIsInstance<TableRow>()
.flatMap { it.children }
.filterIsInstance<ImageView>()
.forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } }
.forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } }*/
}

//보드의 각 칸에 클릭 리스너 설정
fun setBoard(board: TableLayout) {
board.children.filterIsInstance<TableRow>().forEachIndexed { rowIndex, row ->
row.children.filterIsInstance<ImageView>().forEachIndexed { colIndex, view ->
view.setOnClickListener { onCellClick(view, rowIndex, colIndex) } //칸(셀) 클릭 시 동작
}
}
}

//칸(셀) 클릭 시 동작 정의
fun onCellClick(view: ImageView, row: Int, col: Int) {
if (!gameEnded && boardState[row][col] == 0) {
placeStone(view, row, col) //돌 놓기
handleGameState(row, col) //게임 상태 처리
}
}

//돌 놓고 보드 상태 업데이트
fun placeStone(view: ImageView, row: Int, col: Int) {
val drawable = if (isBlackTurn) R.drawable.black_stone else R.drawable.white_stone
view.setImageResource(drawable) //돌 이미지 설정
boardState[row][col] = if (isBlackTurn) 1 else 2 //보드 상태 업데이트
}

//게임 상태
fun handleGameState(row: Int, col: Int) {
when {
checkWin(row, col) -> endGame(if (isBlackTurn) "플레이어1(흑돌)" else "플레이어2(백돌)") //승리 조건 확인
isBoardFull() -> endGame("무승부") //무승부 조건 확인
else -> isBlackTurn = !isBlackTurn //플레이어 순서 변경
}
}

//게임 종료
fun endGame(result: String) {
gameEnded = true
Toast.makeText(this, "$result 승리", Toast.LENGTH_LONG).show()
}

//승리 조건 확인
fun checkWin(row: Int, col: Int): Boolean {
val player = boardState[row][col]
return (checkDirection(row, col, 1, 0, player) >= 5 || // 가로 방향
checkDirection(row, col, 0, 1, player) >= 5 || // 세로 방향
checkDirection(row, col, 1, 1, player) >= 5 || // 대각선(\) 방향
checkDirection(row, col, 1, -1, player) >= 5) // 대각선(/) 방향
}

//특정 방향으로 연속된 돌 개수 세기
fun checkDirection(row: Int, col: Int, dRow: Int, dCol: Int, player: Int): Int {
return (1 + countStones(row, col, dRow, dCol, player) + countStones(row, col, -dRow, -dCol, player))
}

fun countStones(row: Int, col: Int, dRow: Int, dCol: Int, player: Int): Int {
var count = 0
var i = 1
while (true) {
val newRow = row + i * dRow
val newCol = col + i * dCol
if (newRow in 0..14 && newCol in 0..14 && boardState[newRow][newCol] == player) {
count++
} else break
i++
}
return count
}

//무승부 조건 확인
fun isBoardFull(): Boolean {
return boardState.all { row -> row.all { it != 0 } }
}

//Restart Button 클릭 리스너 설정
fun setRestartButton(restartButton: Button, board: TableLayout) {
restartButton.setOnClickListener {
resetBoard(board)
}
}

//보드 초기화
fun resetBoard(board: TableLayout) {
boardState.forEach { row -> row.fill(0) } //보드 상태 초기화
board.children.filterIsInstance<TableRow>().forEach { row ->
row.children.filterIsInstance<ImageView>().forEach { view ->
view.setImageDrawable(null) // 모든 칸(셀) 비움
}
}
isBlackTurn = true // 플레이어1(흑돌)부터 시작
gameEnded = false // 게임 종료 상태 초기화
}
}
11 changes: 10 additions & 1 deletion app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:orientation="vertical">

<TableLayout
android:id="@+id/board"
Expand Down Expand Up @@ -1180,4 +1181,12 @@
</TableRow>

</TableLayout>

<Button
android:id="@+id/restartButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Restart"
android:layout_gravity="center_horizontal"
android:layout_marginTop="70dp" />
</LinearLayout>
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.overridePathCheck=true