Skip to content
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# android-omok-precourse
# android-omok-precourse

## 프로젝트 설명
이 프로젝트는 카카오테크캠퍼스 2기 2회차 미니과제로, Kotlin으로 작성한 오목 게임입니다. 두 사람이 번갈아 돌을 놓아 가로나 세로, 대각선으로 다섯 개의 연속된 돌을 먼저 만들면 승리하는 게임입니다.

## 게임 규칙
- 6목 이상의 장목도 착수 가능하며 승리 조건으로 인정합니다.
- 렌주 룰과 같은 복잡한 룰은 고려하지 않습니다.

## 구현 기능 목록
1. 보드 초기화 및 플레이어 초기화
2. 각 셀에 클릭 리스너 설정
3. 돌 위치 선정
- 유효한 자리 -> 돌 위치 확정
- 유효하지 않은 자리 -> 돌 위치 재선정 메시지 출력
4. 우승 조건 확인
- O -> 우승 메시지 출력 및 초기화
- X -> 플레이어 변경
5. 무승부 조건 확인
- O -> 무승부 메시지 출력 및 초기화
- X -> 플레이어 변경
62 changes: 57 additions & 5 deletions app/src/main/java/nextstep/omok/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,72 @@ import android.os.Bundle
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children

class MainActivity : AppCompatActivity() {

private val game = OmokGame()

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

private fun initializeBoard() {
val board = findViewById<TableLayout>(R.id.board)
board
.children
board.children
.filterIsInstance<TableRow>()
.flatMap { it.children }
.filterIsInstance<ImageView>()
.forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } }
.forEachIndexed { rowIndex, row ->
row.children
.filterIsInstance<ImageView>()
.forEachIndexed { colIndex, cell ->
cell.setImageResource(0)
cell.tag = null
cell.setOnClickListener { onCellClicked(rowIndex, colIndex, cell) }
}
}
}

private fun onCellClicked(row: Int, col: Int, cell: ImageView) {
if (game.placeStone(row, col)) {
placeStone(cell, game.currentPlayer)
if (game.checkWin(row, col)) {
showWinMessage()
resetBoard()
} else if (game.isBoardFull()) {
showDrawMessage()
resetBoard()
} else {
game.togglePlayer()
}
} else {
showInvalidMoveMessage()
}
}

private fun placeStone(cell: ImageView, player: Char) {
val resource = if (player == PLAYER_BLACK) R.drawable.black_stone else R.drawable.white_stone
cell.setImageResource(resource)
cell.tag = player
}

private fun showInvalidMoveMessage() {
Toast.makeText(this, "해당 위치에는 돌이 이미 존재합니다.\n다른 위치를 선택하세요.", Toast.LENGTH_SHORT).show()
}

private fun showWinMessage() {
Toast.makeText(this, "${game.currentPlayer}가 승리하였습니다.", Toast.LENGTH_LONG).show()
}

private fun showDrawMessage() {
Toast.makeText(this, "더 이상 돌을 놓을 위치가 존재하지 않습니다.\n무승부입니다.", Toast.LENGTH_LONG).show()
}

private fun resetBoard() {
game.resetGame()
initializeBoard()
}
}
69 changes: 69 additions & 0 deletions app/src/main/java/nextstep/omok/OmokGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package nextstep.omok

const val PLAYER_BLACK = 'B'
const val PLAYER_WHITE = 'W'
const val BOARD_SIZE = 15

class OmokGame {
var currentPlayer = PLAYER_BLACK

private val board = Array(BOARD_SIZE) { Array<Char?>(BOARD_SIZE) { null } }

fun placeStone(row: Int, col: Int): Boolean {
if (board[row][col] == null) {
board[row][col] = currentPlayer
return true
}
return false
}

fun checkWin(row: Int, col: Int): Boolean {
val directions = listOf(
listOf(0 to 1, 0 to -1), listOf(1 to 0, -1 to 0), listOf(1 to 1, -1 to -1), listOf(1 to -1, -1 to 1)
)
for (direction in directions) {
var count = 1
for ((dr, dc) in direction) {
count += countStonesInDirection(row, col, dr, dc)
}
if (count >= 5) {
return true
}
}
return false
}

private fun countStonesInDirection(row: Int, col: Int, dr: Int, dc: Int): Int {
var count = 0
var r = row + dr
var c = col + dc

while (r in 0 until BOARD_SIZE && c in 0 until BOARD_SIZE && board[r][c] == currentPlayer) {
count++
r += dr
c += dc
}
return count
}

fun togglePlayer() {
currentPlayer = if (currentPlayer == PLAYER_BLACK) PLAYER_WHITE else PLAYER_BLACK
}

fun resetGame() {
for (row in 0 until BOARD_SIZE) {
for (col in 0 until BOARD_SIZE) {
board[row][col] = null
}
}
currentPlayer = PLAYER_BLACK
}

fun getStone(row: Int, col: Int): Char? {
return board[row][col]
}

fun isBoardFull(): Boolean {
return board.all { row -> row.all { cell -> cell != null }}
}
}
61 changes: 61 additions & 0 deletions app/src/test/java/OmokGameTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package nextstep.omok

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class OmokGameTest {

private lateinit var game: OmokGame

@BeforeEach
fun setUp() {
game = OmokGame()
}

@Test
fun testPlaceStone() {
assertTrue(game.placeStone(0, 0))
assertEquals(PLAYER_BLACK, game.getStone(0, 0))
assertFalse(game.placeStone(0, 0))
}

@Test
fun testTogglePlayer() {
game.togglePlayer()
assertEquals(PLAYER_WHITE, game.currentPlayer)
game.togglePlayer()
assertEquals(PLAYER_BLACK, game.currentPlayer)
}

@Test
fun testCheckWin() {
for (i in 0 until 5) {
game.placeStone(0, i)
}
assertTrue(game.checkWin(0, 4))
assertFalse(game.checkWin(1, 2))
}

@Test
fun testIsBoardFull() {
for (row in 0 until BOARD_SIZE) {
for (col in 0 until BOARD_SIZE) {
game.placeStone(row, col)
game.togglePlayer()
}
}
assertTrue(game.isBoardFull())
}

@Test
fun testResetGame() {
game.placeStone(0, 0)
game.resetGame()
assertNull(game.getStone(0, 0))
assertEquals(PLAYER_BLACK, game.currentPlayer)
}
}