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_전영주] 미션 제출합니다. #33

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dc5b4ec
docs: 기능 목록 작성
aengzu Jun 8, 2024
5517474
feat: add logo resource file
aengzu Jun 8, 2024
384ad8c
feat: add font resource file
aengzu Jun 8, 2024
1fa93cf
feat: create custom button drawable
aengzu Jun 8, 2024
52e720d
feat: add custom colors
aengzu Jun 8, 2024
dcd5396
feat: implement player model
aengzu Jun 8, 2024
8dca8e6
feat: add basic structure of game model
aengzu Jun 8, 2024
23263c5
feat: create main screen view
aengzu Jun 8, 2024
8f7fb5d
feat: create game board view
aengzu Jun 8, 2024
e9e5e98
feat: create victory popup view
aengzu Jun 8, 2024
b0b61c6
feat: add navigation from main screen to game board
aengzu Jun 8, 2024
babd584
feat: integrate game board fragment into BoardActivity
aengzu Jun 8, 2024
7afd4e8
feat: add game initialization
aengzu Jun 8, 2024
91af28d
feat: add place stone function
aengzu Jun 9, 2024
b76050f
feat: add player turn switching function
aengzu Jun 9, 2024
f062dce
feat: add turn image update function
aengzu Jun 9, 2024
7380391
feat: add game reset functionality
aengzu Jun 9, 2024
6489513
feat: add win condition checking
aengzu Jun 9, 2024
bf2a175
feat: add victory popup display
aengzu Jun 9, 2024
6400d77
docs: 일부 기능 삭제
aengzu Jun 9, 2024
7e3349d
docs: 완성 기능 체크
aengzu Jun 9, 2024
4645764
test: add unit tests for GameModel
aengzu Jun 9, 2024
b37e6ac
docs: 완료 기능 체크
aengzu Jun 9, 2024
e5c0c1e
style: build gradle 파일이 수정된 것 같아 원본 파일로 재업로드
aengzu Jun 9, 2024
fe04270
Update README.md
aengzu Jun 9, 2024
f05d2e1
Update README.md
aengzu Jun 9, 2024
a9f362d
Update README.md
aengzu Jun 9, 2024
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
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,40 @@
# android-omok-precourse
# android-omok-precourse

## 1. 기능 목록
---
### 1. 리소스 파일 추가
- [x] 로고 리소스 파일 추가하기
- [x] 폰트 리소스 파일 추가하기
- [x] 커스텀 버튼 드로우블 제작하기
- [x] 커스텀 컬러 추가하기

### 2. 모델 구현
- [x] 게임 모델 구현
- [x] 플레이어 모델 구현

### 3. 뷰 구성
- [x] 메인 화면 뷰 구성하기
- [x] 오목판 뷰 구성하기
- [x] 승리 시 팝업 뷰 구성

### 4. 기능 구현
- [x] 메인 화면에서 오목판으로 화면 전환 기능 추가
- [x] 게임 초기화 기능 추가
- [x] 오목판에 돌 놓기 기능 추가
- [x] 플레이어 전환 기능 추가
- [x] 승리 조건 확인 기능 추가
- [x] 승리 시 팝업 표시 기능 추가


### 5. 단위 테스트 추가
- [x] 단위 테스트 추가
---

## 2. 스크린샷
<p align="center">
<img src="https://github.com/aengzu/android-omok-precourse/assets/102356873/0edbcc04-3a2a-4b76-81d3-de27f51c0c09" width="200" />
<img src="https://github.com/aengzu/android-omok-precourse/assets/102356873/955802a5-7011-4fa2-ade0-0efe625701e1" width="200" />
<img src="https://github.com/aengzu/android-omok-precourse/assets/102356873/aa9fee4f-944e-43e8-b8e0-5685241ed0c3" width="200" />
<img src="https://github.com/aengzu/android-omok-precourse/assets/102356873/6a86bb06-2253-4776-bff3-1566ab2c76dc" width="200" />
</p>

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")
}
}
5 changes: 4 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.Omok"
tools:targetApi="31">
<activity
android:name=".BoardActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
Expand All @@ -23,4 +26,4 @@
</activity>
</application>

</manifest>
</manifest>
88 changes: 88 additions & 0 deletions app/src/main/java/nextstep/omok/BoardActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package nextstep.omok

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class BoardActivity : AppCompatActivity() {
private lateinit var turnImage: ImageView
private lateinit var newGame: TextView
private lateinit var gameBoardFragment: GameBoardFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_board)

turnImage = findViewById(R.id.turnImage)
newGame = findViewById(R.id.newGameButton)

// 프래그먼트 추가
if (savedInstanceState==null){
gameBoardFragment = GameBoardFragment()
supportFragmentManager.beginTransaction()
.add(R.id.fragment_container, gameBoardFragment)
.commit()
} else {
gameBoardFragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as GameBoardFragment
}

GameModel.resetGame()
updateTurnImage()

newGame.setOnClickListener {
resetGame()
}

}

// 턴 이미지 업데이트
fun updateTurnImage() {
turnImage.setImageResource(GameModel.getCurrentPlayerStoneResId())
}

// 새 게임 버튼 클릭시 게임 초기화
fun resetGame() {
GameModel.resetGame()
gameBoardFragment.resetBoard()
updateTurnImage()
}

// 승리 다이얼로그 표시
fun showWinDialog() {
val dialogView = layoutInflater.inflate(R.layout.popup_victory , null)
// 다이얼로그 생성 및 표시
val dialog = androidx.appcompat.app.AlertDialog.Builder(this).setView(dialogView).create()
dialog.show()

setWinnerImage(dialog)

dialog.findViewById<Button>(R.id.newGameButton)?.setOnClickListener {
resetGame()
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.mainMenuButton)?.setOnClickListener {
// 메인 메뉴로 이동
navigateToMainMenu()
}
}

// 승자 이미지 설정
private fun setWinnerImage(dialog: androidx.appcompat.app.AlertDialog) {
val winnerImageView = dialog.findViewById<ImageView>(R.id.winner)
GameModel.winner?.let {
winnerImageView?.setImageResource(it.stoneResId)
}
}
// 메인 메뉴로 이동
private fun navigateToMainMenu() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}
64 changes: 64 additions & 0 deletions app/src/main/java/nextstep/omok/GameBoardFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package nextstep.omok

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import androidx.core.view.children
import androidx.fragment.app.Fragment

class GameBoardFragment: Fragment() {
private lateinit var board: TableLayout
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.board, container, false)
setupBoard(view)
return view
}

private fun setupBoard(view: View) {
board = view.findViewById(R.id.board)
board.children.filterIsInstance<TableRow>().forEachIndexed { rowIndex, row ->
row.children.filterIsInstance<ImageView>().forEachIndexed { colIndex, cell ->
cell.setOnClickListener {
Log.d("testt", "${rowIndex}행 ${colIndex}눌림")
handleCellClick(rowIndex, colIndex)
}
}
}
}

private fun handleCellClick(x: Int, y: Int) {
if (GameModel.placeStone(x, y)) {
updateCell(x, y, GameModel.currentPlayer)
if (GameModel.checkWinCondition(x, y)) {
GameModel.handelWin()
(activity as? BoardActivity)?.showWinDialog()
} else {
GameModel.switchPlayer()
(activity as? BoardActivity)?.updateTurnImage()
}
}
}

fun updateCell(x: Int, y: Int, player: Player) {
val row = board.getChildAt(x) as? TableRow
val cell = row?.getChildAt(y) as? ImageView
cell?.setImageResource(player.stoneResId)
}

// 보드 초기화
fun resetBoard() {
board.children.filterIsInstance<TableRow>().forEach { row ->
row.children.filterIsInstance<ImageView>().forEach { cell ->
cell.setImageResource(0) // 이미지를 초기화
}
}
}
}
86 changes: 86 additions & 0 deletions app/src/main/java/nextstep/omok/GameModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package nextstep.omok

import android.util.Log

// 싱글톤 패턴을 위해 object로 선언
object GameModel {
var currentPlayer: Player = Player.BLACK // 우선 흑돌로 현재 플레이어 초기화
var board: Array<Array<Player?>> = Array(15) { arrayOfNulls<Player>(15) } // 15x15 보드, null로 초기화
var winner: Player? = null // 승리한 플레이어를 저장

// 방향 체크할 때 쓰기 위한 변수
private val directions = mapOf(
"Horizontal" to Pair(1, 0), // 가로
"Vertical" to Pair(0, 1), // 세로
"PositiveDiagonal" to Pair(1, 1), // 대각선 \
"NegativeDiagonal" to Pair(1, -1) // 대각선 /
)

fun resetGame() {
// NOTE: 게임 초기화 로직
for (i in board.indices) {
for (j in board[i].indices) {
board[i][j] = null // 보드의 각 위치를 null로 초기화
}
}
currentPlayer = Player.BLACK // 흑돌로 초기화
}
fun switchPlayer() {
// NOTE: 플레이어 전환 로직
currentPlayer = if (currentPlayer == Player.BLACK) Player.WHITE else Player.BLACK
}

fun checkWinCondition(x: Int, y: Int):Boolean{
// NOTE: 승리 조건 확인 로직
return checkDirection(x, y, directions["Horizontal"]!!) || // 가로
checkDirection(x, y, directions["Vertical"]!!) || // 세로
checkDirection(x, y, directions["PositiveDiagonal"]!!) || // 대각선 \
checkDirection(x, y, directions["NegativeDiagonal"]!!) // 대각선 /
}

// 각 방향 넣으면 해당 방향으로 5개 이상의 돌이 있는지 확인
private fun checkDirection(x: Int, y: Int, direction: Pair<Int, Int>): Boolean {
val (dx, dy) = direction
val count = countStones(x, y, dx, dy) + countStones(x, y, -dx, -dy) + 1
return count >= 5
}

// 해당 방향으로 돌이 몇개 있는지 확인
private fun countStones(x: Int, y: Int, dx: Int, dy: Int): Int {
var count = 0
var nx = x + dx
var ny = y + dy

while (nx in board.indices && ny in board[nx].indices && board[nx][ny] == currentPlayer) {
count++
nx += dx
ny += dy
}

return count
}



fun placeStone(x: Int, y: Int): Boolean {
// NOTE: 현재 플레이어가 돌을 놓는 로직
if (board[x][y] == null) {
board[x][y] = currentPlayer // 현재 플레이어가 돌을 놓음
return true
}
return false
}

fun handelWin(): Player? {
// NOTE : 승리 조건이 만족되었을 때 winner 를 현재 플레이어로 초기화
winner = currentPlayer
return winner
}

fun getCurrentPlayerStoneResId(): Int {
// 현재 플레이어의 돌 리소스 파일 ID를 반환
return currentPlayer.stoneResId
}


}
17 changes: 10 additions & 7 deletions app/src/main/java/nextstep/omok/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package nextstep.omok

import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children

Expand All @@ -12,12 +15,12 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

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) } }
val startGameButton = findViewById<TextView>(R.id.startGameButton)
startGameButton.setOnClickListener {
val intent = Intent(this, BoardActivity::class.java)
startActivity(intent)
}


}
}
6 changes: 6 additions & 0 deletions app/src/main/java/nextstep/omok/Player.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package nextstep.omok

enum class Player(val color: String, val stoneResId: Int) {
BLACK("black", R.drawable.black_stone),
WHITE("white", R.drawable.white_stone)
}
34 changes: 34 additions & 0 deletions app/src/main/res/drawable/custom_button.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 그림자 -->
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<size android:width="180dp" android:height="70dp" />
<solid android:color="#08000000" /> <!-- 그림자 색상 -->
<padding android:left="2dp" android:top="0dp" android:right="2dp" android:bottom="6dp" />
</shape>
</item>

<!-- 기본 배경 -->
<item android:bottom="4dp" android:right="2dp">
<selector>
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#D3D3D3" /> <!-- 눌린 상태의 배경색 -->
<corners android:radius="8dp" /> <!-- 모서리 반경 -->
<stroke android:width="2dp" android:color="#000000" /> <!-- 테두리 색과 두께 -->
<size android:width="180dp" android:height="70dp" /> <!-- 크기 지정 -->
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#EEEEEE" /> <!-- 기본 배경색 -->
<corners android:radius="8dp" /> <!-- 모서리 반경 -->
<stroke android:width="0.1dp" android:color="#80656565" /> <!-- 테두리 색과 두께 -->
<size android:width="180dp" android:height="70dp" /> <!-- 크기 지정 -->
</shape>
</item>
</selector>
</item>
</layer-list>
Binary file added app/src/main/res/drawable/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/font/seoulhangangeb.ttf
Binary file not shown.
Loading