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
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,48 @@
# android-omok-precourse
# android-omok-precourse

# 오목 게임

이 프로젝트는 안드로이드를 위한 코틀린으로 구현된 간단한 오목(Omok) 게임이다.
이 게임은 두 명의 플레이어가 15 X 15 오목 판에 검은 돌과 흰 돌을 놓을 수 있게 하며, 전통적인 오목 규칙에 따라 진행된다. 추가로, 흑돌은 금수 규칙이 적용된다.


## 주요 기능

- **두 명의 플레이어**: 흑돌과 백돌.

- **기본 오목 규칙**: 가로, 세로 또는 대각선으로 다섯 개의 돌을 먼저 놓으면 승리
- **흑돌 금수 규칙**:
- "삼삼" (Double Three): 두 개의 열린 삼을 만드는 돌 놓기 금지

- "사사" (Double Four): 두 개의 열린 사를 만드는 돌 놓기 금지
- "장목" (Overline): 여섯 개 이상의 돌을 연속으로 놓는 것 금지
- **백돌 자유 규칙**: 백돌은 아무 곳에나 놓을 수 있음
- **승리 및 금수 위치 알림**: 승리 조건 충족 시 및 금수 위치에 돌을 놓으려 할 때 토스트 메시지로 알림



## 파일 설명

### MainActivity.kt

`MainActivity`는 안드로이드 액티비티로서, 게임 보드를 설정하고 사용자 입력을 처리한다. 게임의 주요 로직은 `OmokGame` 클래스로 구현한다.


### OmokGame.kt

`OmokGame` 클래스는 게임의 상태를 관리하고, 금수 규칙과 승리 조건을 확인하는 로직을 포함한다.
- 보드에 돌을 놓기
- 플레이어 턴 전환
- 가로, 세로, 대각선 승리 조건 확인
- 삼삼, 사사, 장목과 같은 금수 규칙 적용

### GameLogicTest.kt

`GameLogicTest` 클래스는 JUnit 5와 AssertJ를 사용하여 `OmokGame`의 주요 기능을 테스트. 삼삼, 사사, 장목 금수 규칙과 승리 조건을 검증한다.

- 가로 승리 조건
- 세로 승리 조건
- 대각선 승리 조건 (우하향, 우상향)
- 흑돌 금수 규칙 (삼삼, 사사, 장목)


42 changes: 38 additions & 4 deletions app/src/main/java/nextstep/omok/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,54 @@ 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 boardSize = 15
private lateinit var game: OmokGame

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

val board = findViewById<TableLayout>(R.id.board)
board
.children
game = OmokGame(boardSize)
setupBoard()
}

private fun setupBoard() {
val boardView = findViewById<TableLayout>(R.id.board)
boardView.children
.filterIsInstance<TableRow>()
.flatMap { it.children }
.filterIsInstance<ImageView>()
.forEach { view -> view.setOnClickListener { view.setImageResource(R.drawable.black_stone) } }
.forEachIndexed { index, view ->
val row = index / boardSize
val col = index % boardSize
view.setOnClickListener { handleCellClick(row, col, view) }
}
}

private fun handleCellClick(row: Int, col: Int, view: ImageView) {
if (game.board[row][col] == 0) {
if (game.isForbidden(row, col)) {
Toast.makeText(this, "Forbidden move!", Toast.LENGTH_SHORT).show()
return
}
game.placeStone(row, col)
view.setImageResource(if (game.currentPlayer == 1) R.drawable.black_stone else R.drawable.white_stone)
if (game.checkWin(row, col, game.currentPlayer)) {
showWinner(game.currentPlayer)
} else {
game.switchPlayer()
}
}
}

private fun showWinner(player: Int) {
val winner = if (player == 1) "Black" else "White"
Toast.makeText(this, "$winner wins!", Toast.LENGTH_SHORT).show()
}
}
123 changes: 123 additions & 0 deletions app/src/main/java/nextstep/omok/OmokGame.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package nextstep.omok

class OmokGame(private val boardSize: Int) {

val board = Array(boardSize) { Array(boardSize) { 0 } }
var currentPlayer = 1 // 1 for black, 2 for white

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

fun switchPlayer() {
currentPlayer = if (currentPlayer == 1) 2 else 1
}

fun checkWin(row: Int, col: Int, player: Int): Boolean {
return checkDirection(row, col, player, 0, 1) >= 5 || // Horizontal
checkDirection(row, col, player, 1, 0) >= 5 || // Vertical
checkDirection(row, col, player, 1, 1) >= 5 || // Diagonal down-right
checkDirection(row, col, player, 1, -1) >= 5 // Diagonal up-right
}

fun isForbidden(row: Int, col: Int): Boolean {
if (currentPlayer == 1) {
return isDoubleThree(row, col) || isDoubleFour(row, col) || isSixInARow(row, col)
}
return false
}

private fun checkDirection(row: Int, col: Int, player: Int, dRow: Int, dCol: Int): Int {
var count = 1
count += countStones(row, col, player, dRow, dCol)
count += countStones(row, col, player, -dRow, -dCol)
return count
}

private fun countStones(row: Int, col: Int, player: Int, dRow: Int, dCol: Int): Int {
var count = 0
var r = row + dRow
var c = col + dCol
while (r in 0 until boardSize && c in 0 until boardSize && board[r][c] == player) {
count++
r += dRow
c += dCol
}
return count
}

fun isDoubleThree(row: Int, col: Int): Boolean {
return countOpenThrees(row, col, 1, 0) + countOpenThrees(row, col, 0, 1) +
countOpenThrees(row, col, 1, 1) + countOpenThrees(row, col, 1, -1) > 1
}

private fun countOpenThrees(row: Int, col: Int, dRow: Int, dCol: Int): Int {
var count = 0
if (isOpenThree(row, col, dRow, dCol)) count++
if (isOpenThree(row, col, -dRow, -dCol)) count++
return count
}

private fun isOpenThree(row: Int, col: Int, dRow: Int, dCol: Int): Boolean {
val sequence = mutableListOf<Int>()
for (i in -4..4) {
val r = row + i * dRow
val c = col + i * dCol
if (r in 0 until boardSize && c in 0 until boardSize) {
sequence.add(board[r][c])
} else {
sequence.add(-1)
}
}
val pattern = listOf(0, 1, 1, 1, 0)
for (i in 0..sequence.size - pattern.size) {
if (sequence.subList(i, i + pattern.size) == pattern) {
return true
}
}
return false
}

fun isDoubleFour(row: Int, col: Int): Boolean {
return countOpenFours(row, col, 1, 0) + countOpenFours(row, col, 0, 1) +
countOpenFours(row, col, 1, 1) + countOpenFours(row, col, 1, -1) > 1
}

private fun countOpenFours(row: Int, col: Int, dRow: Int, dCol: Int): Int {
var count = 0
if (isOpenFour(row, col, dRow, dCol)) count++
if (isOpenFour(row, col, -dRow, -dCol)) count++
return count
}

private fun isOpenFour(row: Int, col: Int, dRow: Int, dCol: Int): Boolean {
val sequence = mutableListOf<Int>()
for (i in -4..4) {
val r = row + i * dRow
val c = col + i * dCol
if (r in 0 until boardSize && c in 0 until boardSize) {
sequence.add(board[r][c])
} else {
sequence.add(-1)
}
}
val pattern = listOf(0, 1, 1, 1, 1, 0)
for (i in 0..sequence.size - pattern.size) {
if (sequence.subList(i, i + pattern.size) == pattern) {
return true
}
}
return false
}

fun isSixInARow(row: Int, col: Int): Boolean {
return checkDirection(row, col, currentPlayer, 0, 1) > 5 ||
checkDirection(row, col, currentPlayer, 1, 0) > 5 ||
checkDirection(row, col, currentPlayer, 1, 1) > 5 ||
checkDirection(row, col, currentPlayer, 1, -1) > 5
}
}
93 changes: 93 additions & 0 deletions app/src/test/java/nextstep/omok/GameLogicTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package nextstep.omok

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class GameLogicTest {

private val boardSize = 15
private lateinit var game: OmokGame

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

@Test
fun `horizontal win check`() {
for (i in 0 until 5) {
game.board[0][i] = 1
}
val result = game.checkWin(0, 2, 1)
assertThat(result).isTrue()
}

@Test
fun `vertical win check`() {
for (i in 0 until 5) {
game.board[i][0] = 1
}
val result = game.checkWin(2, 0, 1)
assertThat(result).isTrue()
}

@Test
fun `diagonal down-right win check`() {
for (i in 0 until 5) {
game.board[i][i] = 1
}
val result = game.checkWin(2, 2, 1)
assertThat(result).isTrue()
}

@Test
fun `diagonal up-right win check`() {
for (i in 0 until 5) {
game.board[4 - i][i] = 1
}
val result = game.checkWin(2, 2, 1)
assertThat(result).isTrue()
}

@Test
fun `double three forbidden move`() {
// Set up a double three situation
game.board[1][2] = 1
game.board[1][3] = 1
game.board[1][5] = 1
game.board[1][6] = 1
game.board[2][3] = 1
game.board[3][3] = 1
game.board[4][3] = 1

val result = game.isDoubleThree(1, 4)
assertThat(result).isTrue()
}

@Test
fun `double four forbidden move`() {
// Set up a double four situation
game.board[1][2] = 1
game.board[1][3] = 1
game.board[1][4] = 1
game.board[1][6] = 1
game.board[1][7] = 1
game.board[1][8] = 1
game.board[0][5] = 1
game.board[2][5] = 1

val result = game.isDoubleFour(1, 5)
assertThat(result).isTrue()
}

@Test
fun `six in a row forbidden move`() {
// Set up a six in a row situation
for (i in 0 until 6) {
game.board[0][i] = 1
}
val result = game.isSixInARow(0, 2)
assertThat(result).isTrue()
}
}