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

[부나] 3, 4단계 오목 제출합니다. #32

Merged
merged 29 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8d59987
feat: set up the android project
woowahan-pjs Mar 14, 2023
fae3223
feat: add main activity and resources
woowahan-pjs Mar 21, 2023
ea6854b
feat: 안드로이드 프로젝트 적용
tmdgh1592 Mar 22, 2023
2f4f911
feat: domain을 안드로이드로 이동
tmdgh1592 Mar 22, 2023
dad2995
feat(android): 오목 게임 구현
tmdgh1592 Mar 22, 2023
9f32c95
feat: 오목 데이터베이스 헬퍼 구현
tmdgh1592 Mar 23, 2023
f8c5492
chore: 불필요한 의존성 제거
tmdgh1592 Mar 23, 2023
84e3150
refactor: 데이터베이스 모듈화
tmdgh1592 Mar 23, 2023
169c6c2
feat: 결과 출력 화면 구현
tmdgh1592 Mar 23, 2023
f4153d7
feat: 오목판 색상 변경
tmdgh1592 Mar 23, 2023
99a94dd
feat: 캐릭터 턴 홀더 이미지 적용
tmdgh1592 Mar 23, 2023
7ea3064
fix: 이전 게임을 불러오면 오목알을 놓은 결과를 모두 재처리하는 오류 수정
tmdgh1592 Mar 23, 2023
2990bb6
fix: 이전 게임을 불러 왔을 때 캐릭터 홀더 이미지는 변경되지 않는 버그 수정
tmdgh1592 Mar 23, 2023
f314d9d
feat: 불필요한 클래스 제거
tmdgh1592 Mar 24, 2023
78b7c98
refactor: manifest에 마지막 개행 추가
tmdgh1592 Mar 25, 2023
1713db9
refactor: 불필요한 클래스 제거
tmdgh1592 Mar 25, 2023
b66ca08
refactor: Repository를 Generic을 사용하여 일반화
tmdgh1592 Mar 25, 2023
5ca1a10
refactor: 레이아웃 속성의 left, right를 start, end로 변경
tmdgh1592 Mar 25, 2023
89fb85c
chore: Coroutine 라이브러리 추가
tmdgh1592 Mar 25, 2023
5f561a1
refactor: MainActivity에서 컨트롤러를 재사용하도록 변경
tmdgh1592 Mar 26, 2023
add4939
refactor: presentation과 data 분리
tmdgh1592 Mar 26, 2023
d4e3c81
feat: intent의 Serializable 객체를 가져오는 확장 함수 구현
tmdgh1592 Mar 26, 2023
3cf09c5
refactor: 마지막 돌을 놓은 위치를 보여주는 화면 구현
tmdgh1592 Mar 26, 2023
c0355ae
chore: androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 라이브러리 추가
tmdgh1592 Mar 27, 2023
4009199
refactor: MainActivity에서 CoroutineScope를 구현하지 않고 lifecycleScope를 사용하도…
tmdgh1592 Mar 27, 2023
c1d4f10
refactor: readPoint() 메서드를 suspendCoroutine을 사용하여 Point를 받도록 변경
tmdgh1592 Mar 27, 2023
8b8a053
refactor: 콘솔 관련 파일을 console 패키지로 이동
tmdgh1592 Mar 27, 2023
744e6cd
refactor: Android와 Console Controller 공통 로직 분리
tmdgh1592 Mar 27, 2023
6f5657c
refactor: 불필요한 클래스 제거
tmdgh1592 Mar 27, 2023
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ android {

dependencies {
implementation(project(":domain"))
implementation(project(":data"))
galcyurio marked this conversation as resolved.
Show resolved Hide resolved
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.0")
implementation("com.google.android.material:material:1.7.0")
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@
</intent-filter>
</activity>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import controller.OmokController
package woowacourse.omok

import domain.game.Omok.Companion.OMOK_BOARD_SIZE
import domain.rule.BlackRenjuRule
import domain.rule.WhiteRenjuRule
import view.OmokInputView
import view.OmokOutputView
import woowacourse.omok.controller.ConsoleOmokController
import woowacourse.omok.view.OmokInputView
import woowacourse.omok.view.OmokOutputView

fun main() {
OmokController(
suspend fun main() {
ConsoleOmokController(
OmokInputView(),
OmokOutputView(),
).startGame(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package woowacourse.omok.activity

import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import woowacourse.omok.R
import woowacourse.omok.model.StoneColorModel
import woowacourse.omok.utils.getSerializable
import woowacourse.omok.utils.setImageByResId

class GameResultActivity : AppCompatActivity(), View.OnClickListener {
Expand All @@ -21,11 +21,7 @@ class GameResultActivity : AppCompatActivity(), View.OnClickListener {
}

private fun setWinnerStoneImage() {
val winnerStoneColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra("winner_color", StoneColorModel::class.java)
} else {
intent.getSerializableExtra("winner_color")
}
val winnerStoneColor = getSerializable(WINNER_KEY, StoneColorModel::class.java)
val winnerStoneIv = findViewById<ImageView>(R.id.winner_stone_iv)
val winnerStoneTv = findViewById<TextView>(R.id.winner_stone_tv)

Expand Down Expand Up @@ -61,4 +57,8 @@ class GameResultActivity : AppCompatActivity(), View.OnClickListener {
private fun endGame() {
finish()
}

companion object {
internal const val WINNER_KEY = "winner_color"
}
}
177 changes: 97 additions & 80 deletions app/src/main/java/woowacourse/omok/activity/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,51 +1,56 @@
package woowacourse.omok.activity

import android.content.ContentValues
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import android.widget.TableLayout
import android.widget.TableRow
import android.widget.Toast
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import domain.game.*
import domain.game.Omok
import domain.point.Point
import domain.rule.BlackRenjuRule
import domain.rule.WhiteRenjuRule
import domain.stone.StoneColor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.launch
import woowacourse.omok.R
import woowacourse.omok.database.OmokDatabase
import woowacourse.omok.database.repository.OmokRepository
import woowacourse.omok.database.repository.Repository
import woowacourse.omok.controller.AndroidOmokController
import woowacourse.omok.controller.ConsoleOmokController
import woowacourse.omok.data.database.OmokDatabase
import woowacourse.omok.data.database.repository.OmokRepository
import woowacourse.omok.mapper.toPresentation
import woowacourse.omok.model.StoneColorModel
import woowacourse.omok.utils.createAlertDialog
import woowacourse.omok.utils.message
import woowacourse.omok.utils.negativeButton
import woowacourse.omok.utils.positiveButton
import woowacourse.omok.utils.showMessage
import woowacourse.omok.view.InputView
import woowacourse.omok.view.OutputView

class MainActivity : AppCompatActivity() {
class MainActivity(override val coroutineContext: MainCoroutineDispatcher = Dispatchers.Main) :
AppCompatActivity(), InputView, OutputView, CoroutineScope {
galcyurio marked this conversation as resolved.
Show resolved Hide resolved
private lateinit var board: TableLayout
private lateinit var omok: Omok
private lateinit var omokRepo: Repository
private lateinit var manTurnHolder: View
private lateinit var womanTurnHolder: View
private lateinit var omokController: AndroidOmokController

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
launch { omokController.startGame(BlackRenjuRule(), WhiteRenjuRule()) }
}

omokRepo = OmokRepository(OmokDatabase.getInstance(this))
omok = Omok(BlackRenjuRule(), WhiteRenjuRule())
private fun init() {
initView()

if (hasPreviousGameHistory()) {
showRestartDialog()
}
setOmokClickListener()
showMessage(getString(R.string.omok_start_message))
initOmokController()
initOmokIntersectionClickListener()
}

private fun initView() {
Expand All @@ -54,105 +59,117 @@ class MainActivity : AppCompatActivity() {
womanTurnHolder = findViewById(R.id.woman_turn_iv)
}

private fun continuePreviousGame() {
omokRepo.getAll {
if (it.count % 2 == 1) toggleTurnHolder(StoneColorModel.BLACK)
while (it.moveToNext()) {
val col = it.getInt(it.getColumnIndexOrThrow("x"))
val row = it.getInt(it.getColumnIndexOrThrow("y"))
val index = row * Omok.OMOK_BOARD_SIZE + col
val omokIv: ImageView = boardImageViews()[index]
val putResult = omok.put { Point(row, col) }
if (putResult is PutSuccess) {
drawStoneOnBoard(omokIv, putResult.stoneColor)
}
}
}
private fun initOmokController() {
omokController = AndroidOmokController(
ConsoleOmokController(this, this),
OmokRepository(OmokDatabase.getInstance(this)),
)
}

private fun setOmokClickListener() {
boardImageViews().forEachIndexed { index, view ->
private fun initOmokIntersectionClickListener() {
boardImageViews().forEachIndexed { index, omokIntersection ->
val row: Int = index / Omok.OMOK_BOARD_SIZE
val col: Int = index % Omok.OMOK_BOARD_SIZE

view.setOnClickListener {
val putResult = omok.put { Point(row, col) }
processPutResult(putResult, view)
omokIntersection.setOnClickListener {
launch { omokController.putStone(row, col) }
}
}
}

private fun processPutResult(result: PutResult, view: ImageView) {
when (result) {
is PutSuccess -> {
toggleTurnHolder(result.stoneColor.toPresentation())
savePoint(result.stoneColor.toPresentation(), result.point)
drawStoneOnBoard(view, result.stoneColor)
}
is PutFailed -> showMessage(getString(R.string.inplace_stone))
is GameFinish -> {
omokRepo.clear()
showWinner(result.winnerStoneColor.toPresentation())
private fun continuePreviousGame() {
launch(Dispatchers.IO) {
omokController.loadPreviousPoints { points, pointsCount ->
showLatestTurnHolder(pointsCount)
drawPreviousPoints(points)
}
}
}

private fun toggleTurnHolder(prevStoneColor: StoneColorModel) {
when(prevStoneColor) {
StoneColorModel.BLACK -> {
manTurnHolder.visibility = View.GONE
womanTurnHolder.visibility = View.VISIBLE
}
StoneColorModel.WHITE -> {
womanTurnHolder.visibility = View.GONE
manTurnHolder.visibility = View.VISIBLE
}
private fun showLatestTurnHolder(pointsCount: Int) {
val latestColor = StoneColorModel.calcLatestTurn(pointsCount + 1)
toggleTurnHolder(latestColor ?: StoneColorModel.WHITE)
}

private fun drawPreviousPoints(points: List<Point>) {
points.forEachIndexed { index, point ->
val viewIndex = point.row * Omok.OMOK_BOARD_SIZE + point.col
val omokIv: ImageView = boardImageViews()[viewIndex]
val curColor = StoneColorModel.calcLatestTurn(index) ?: StoneColorModel.BLACK

launch(Dispatchers.Main) { drawStoneOnBoard(omokIv, curColor) }
}
}

private fun savePoint(stoneColor: StoneColorModel, point: Point) {
val record = ContentValues().apply {
put("stone_color", stoneColor.value)
put("x", point.col)
put("y", point.row)
private fun drawStoneOnBoard(view: ImageView, stoneColor: StoneColorModel) {
when (stoneColor) {
StoneColorModel.BLACK -> view.setImageResource(R.drawable.white_bear)
StoneColorModel.WHITE -> view.setImageResource(R.drawable.pink_bear)
}
omokRepo.insert(record)
}

private fun hasPreviousGameHistory(): Boolean = !omokRepo.isEmpty()
override suspend fun readPosition(onPutStone: (Point) -> Unit) {
for (newPoint in omokController.pointChannel) {
onPutStone(newPoint)
}
}

private fun drawStoneOnBoard(view: ImageView, stoneColor: StoneColor) {
when (stoneColor) {
StoneColor.BLACK -> view.setImageResource(R.drawable.pink_bear)
StoneColor.WHITE -> view.setImageResource(R.drawable.white_bear)
override fun startGame() {
showMessage(getString(R.string.omok_start_message))
if (omokController.hasPreviousGameHistory()) showRestartDialog()
}

override fun drawStone(lastStoneColor: StoneColor, newPoint: Point) {
omokController.savePoint(newPoint)
val index = newPoint.row * Omok.OMOK_BOARD_SIZE + newPoint.col
drawStoneOnBoard(boardImageViews()[index], lastStoneColor.toPresentation())
}

override fun showPutFailed() {
showMessage(getString(R.string.inplace_stone))
}

override fun showResult(
lastStoneColor: StoneColor,
winnerStoneColor: StoneColor,
newPoint: Point,
) {
omokController.clearPoints()
showWinner(winnerStoneColor.toPresentation())
}

override fun showCurrentTurnColor(curStoneColor: StoneColor, point: Point?) {
point?.let {
toggleTurnHolder(curStoneColor.next().toPresentation())
findViewById<TextView>(R.id.last_point_tv).text =
getString(R.string.last_put_point, it.col, it.row)
}
}

private fun toggleTurnHolder(prevStoneColor: StoneColorModel) {
launch(Dispatchers.Main) {
manTurnHolder.visibility =
if (prevStoneColor == StoneColorModel.WHITE) View.VISIBLE else View.GONE
womanTurnHolder.visibility =
if (prevStoneColor == StoneColorModel.BLACK) View.VISIBLE else View.GONE
}
}

private fun showWinner(winnerColor: StoneColorModel) {
val resultIntent = Intent(this, GameResultActivity::class.java)
.putExtra("winner_color", winnerColor)
.putExtra(GameResultActivity.WINNER_KEY, winnerColor)
startActivity(resultIntent)
finish()
}

private fun showMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

private fun showRestartDialog(): Unit = createAlertDialog {
message(getString(R.string.restart_game_ask_message))
positiveButton(getString(R.string.yes)) { continuePreviousGame() }
negativeButton(getString(R.string.no)) { omokRepo.clear() }
negativeButton(getString(R.string.no)) { omokController.clearPoints() }
}.show()

private fun boardImageViews(): List<ImageView> = board.children
.filterIsInstance<TableRow>()
.flatMap { it.children }
.filterIsInstance<ImageView>()
.toList()

override fun onDestroy() {
super.onDestroy()
omokRepo.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package woowacourse.omok.controller

import domain.game.Omok
import domain.point.Point
import domain.rule.OmokRule
import kotlinx.coroutines.channels.Channel
import repository.Repository

class AndroidOmokController(
private val controller: ConsoleOmokController,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같은 layer 끼리 서로 의존성을 가지지 않아야 합니다.
공통되는 로직이 있다면 해당 로직만 별도 객체로 분리해주세요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

템플릿 메서드를 사용하여 공통되는 로직을 상위 클래스로 옮기고, 변경이 필요한 로직에 대해서는 별도의 서브 클래스에서 구현하도록 변경하였습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. 지금 리뷰를 다시 읽어 봤는데 공통되는 기능을 하나의 클래스로 분리해서 컴포지션하기를 의도하신 걸까요?!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, Composition을 의도했지만 상속을 통한 재사용도 좋습니다!

private val omokRepo: Repository<Point>,
) {
private lateinit var omok: Omok
val pointChannel = Channel<Point>()
galcyurio marked this conversation as resolved.
Show resolved Hide resolved

suspend fun startGame(blackRule: OmokRule, whiteRule: OmokRule) {
omok = Omok(blackRule, whiteRule)
controller.startGame(omok)
}

suspend fun loadPreviousPoints(onLoaded: (List<Point>, pointsCount: Int) -> Unit) {
val previousPoints = omokRepo.getAll()
previousPoints.forEach { point -> omok.put { point } }
onLoaded(previousPoints, previousPoints.size)
}

fun clearPoints() {
omokRepo.clear()
}

fun savePoint(point: Point) {
omokRepo.insert(point)
}

fun hasPreviousGameHistory(): Boolean = !omokRepo.isEmpty()

suspend fun putStone(row: Int, col: Int) {
pointChannel.send(Point(row, col))
}
}
Loading