diff --git a/.gitignore b/.gitignore index 6c0187813..347e252ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,33 @@ -HELP.md -.gradle +# Gradle files +.gradle/ build/ -!gradle/wrapper/gradle-wrapper.jar -!**/src/main/** -!**/src/test/** -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache +# Local configuration file (sdk path, etc) +local.properties -### IntelliJ IDEA ### -.idea -*.iws +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ *.iml -*.ipr -out/ +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ +# Google Services (e.g. APIs or Firebase) +google-services.json -### VS Code ### -.vscode/ +# Android Profiling +*.hprof diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..22d36f8c2 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "woowacourse.omok" + compileSdk = 33 + + defaultConfig { + applicationId = "woowacourse.omok" + minSdk = 26 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + implementation(project(":domain")) + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.appcompat:appcompat:1.6.0") + implementation("com.google.android.material:material:1.7.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src/main/kotlin/.gitkeep b/app/src/androidTest/java/woowacourse/omok/.gitkeep similarity index 100% rename from src/main/kotlin/.gitkeep rename to app/src/androidTest/java/woowacourse/omok/.gitkeep diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ca6ae7bc0 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/woowacourse/omok/Converter.kt b/app/src/main/java/woowacourse/omok/Converter.kt new file mode 100644 index 000000000..5860bd2ee --- /dev/null +++ b/app/src/main/java/woowacourse/omok/Converter.kt @@ -0,0 +1,26 @@ +package woowacourse.omok + +import domain.Board +import domain.Color +import domain.Position + +object Converter { + fun colorToString(color: Color): String { + return when (color) { + Color.BLACK -> "흑" + Color.WHITE -> "백" + } + } + + fun indexToPosition(index: Int): Position { + val boardSize = Board.getSize() + val x = index / boardSize + val y = index % boardSize + return Position(x, y) + } + + fun positionToIndex(position: Position): Int { + val boardSize = Board.getSize() + return position.x * boardSize + position.y + } +} diff --git a/app/src/main/java/woowacourse/omok/MainActivity.kt b/app/src/main/java/woowacourse/omok/MainActivity.kt new file mode 100644 index 000000000..0749865ca --- /dev/null +++ b/app/src/main/java/woowacourse/omok/MainActivity.kt @@ -0,0 +1,112 @@ +package woowacourse.omok + +import android.os.Bundle +import android.widget.ImageView +import android.widget.TableLayout +import android.widget.TableRow +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.children +import domain.* // ktlint-disable no-wildcard-imports + +class MainActivity : AppCompatActivity() { + + private val turnTextView: TextView by lazy { findViewById(R.id.turn) } + private val boardCoordinateViews: List by lazy { + findViewById(R.id.board) + .children + .filterIsInstance() + .flatMap { it.children } + .filterIsInstance() + .toList() + } + private val omokDbHelper: OmokDbHelper by lazy { + OmokDbHelper(this) + } + private val omokGame: OmokGame by lazy { + omokDbHelper.getOmokGame(rule = RenjuRuleAdapter()) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + printInitBoard(boardCoordinateViews, omokGame.board) + showTurn(omokGame.currentColor) + gameStart() + } + + private fun printInitBoard(boardCoordinateViews: List, board: Board) { + board.stones.values.forEach { stone -> + val index = Converter.positionToIndex(stone.position) + val stoneImage = getStoneImage(stone.color) + boardCoordinateViews[index].setImageResource(stoneImage) + } + } + + private fun printStone(view: ImageView, stone: Stone) { + view.setImageResource(getStoneImage(stone.color)) + } + + private fun getStoneImage(color: Color): Int { + return when (color) { + Color.BLACK -> R.drawable.black_stone + Color.WHITE -> R.drawable.white_stone + } + } + + private fun printWinner(color: Color) { + when (color) { + Color.BLACK -> showToastMessage(WINNER_MESSAGE.format(BLACK)) + Color.WHITE -> showToastMessage(WINNER_MESSAGE.format(WHITE)) + } + } + + private fun showTurn(color: Color) { + turnTextView.text = TURN_MESSAGE.format(Converter.colorToString(color)) + } + + private fun reset(allCoordinate: List, omokGame: OmokGame) { + omokGame.resetGame() + allCoordinate.forEach { it.setImageResource(0) } + omokDbHelper.deleteOmokDatabase() + } + + private fun showToastMessage(message: String): Unit = + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + + private fun gameStart(): Unit = boardCoordinateViews.forEachIndexed { index, view -> + clickBoard(index, view) + } + + private fun clickBoard(index: Int, view: ImageView) { + view.setOnClickListener { + if (omokGame.isRunning()) gameRunning(index, view) + if (!omokGame.isRunning()) gameFinished() + } + } + + private fun gameRunning(index: Int, view: ImageView) { + val stone = omokGame.getStone(Converter.indexToPosition(index)) + val isSuccess = omokGame.placeTo(stone) + if (isSuccess) { + printStone(view, stone) + omokDbHelper.updateOmokDatabase(stone) + } + showTurn(omokGame.currentColor) + } + + private fun gameFinished() { + omokGame.getWinnerColor()?.let { printWinner(it) } + reset(boardCoordinateViews, omokGame) + showTurn(omokGame.currentColor) + } + + companion object { + private const val WINNER_MESSAGE = "%s의 승리입니다" + private const val TURN_MESSAGE = "%s의 차례입니다" + private const val BLACK = "흑" + private const val WHITE = "백" + } +} diff --git a/app/src/main/java/woowacourse/omok/OmokContract.kt b/app/src/main/java/woowacourse/omok/OmokContract.kt new file mode 100644 index 000000000..d9157e987 --- /dev/null +++ b/app/src/main/java/woowacourse/omok/OmokContract.kt @@ -0,0 +1,10 @@ +package woowacourse.omok + +import android.provider.BaseColumns + +object OmokContract : BaseColumns { + const val TABLE_NAME = "omok" + const val TABLE_COLUMN_COLOR = "color" + const val TABLE_COLUMN_X = "x" + const val TABLE_COLUMN_Y = "y" +} diff --git a/app/src/main/java/woowacourse/omok/OmokDbHelper.kt b/app/src/main/java/woowacourse/omok/OmokDbHelper.kt new file mode 100644 index 000000000..41a70d7dd --- /dev/null +++ b/app/src/main/java/woowacourse/omok/OmokDbHelper.kt @@ -0,0 +1,88 @@ +package woowacourse.omok + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import domain.* // ktlint-disable no-wildcard-imports + +class OmokDbHelper(context: Context?) : + SQLiteOpenHelper(context, "${OmokContract.TABLE_NAME}", null, 1) { + + private val omokWritableDb: SQLiteDatabase = this.writableDatabase + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE ${OmokContract.TABLE_NAME} (" + + "${OmokContract.TABLE_COLUMN_COLOR} varchar(2) not null," + + "${OmokContract.TABLE_COLUMN_X} int not null," + + "${OmokContract.TABLE_COLUMN_Y} int not null," + + "UNIQUE (x,y)" + + ");", + ) + } + + override fun onUpgrade(db: SQLiteDatabase?, p1: Int, p2: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${OmokContract.TABLE_NAME}") + onCreate(db) + } + + fun getOmokGame(rule: Rule): OmokGame { + val stones = getAllStonesInDatabase() + return OmokGame(Board(stones, rule)) + } + + fun updateOmokDatabase(stone: Stone) { + val contentValues = getStoneContentValues(stone) + omokWritableDb.insert(OmokContract.TABLE_NAME, null, contentValues) + } + + fun deleteOmokDatabase() { + omokWritableDb.delete(OmokContract.TABLE_NAME, null, null) + } + + private fun getAllStonesSearchCursor(): Cursor { + return omokWritableDb.rawQuery("SELECT * FROM ${OmokContract.TABLE_NAME}", null) + } + + private fun getAllStonesInDatabase(): Stones { + val cursor = getAllStonesSearchCursor() + var stones = Stones(listOf()) + while (cursor.moveToNext()) { + val color = + cursor.getString(cursor.getColumnIndexOrThrow(OmokContract.TABLE_COLUMN_COLOR)) + val x = cursor.getInt(cursor.getColumnIndexOrThrow(OmokContract.TABLE_COLUMN_X)) + val y = cursor.getInt(cursor.getColumnIndexOrThrow(OmokContract.TABLE_COLUMN_Y)) + stones = stones.addStone(Stone(stringToColorInDb(color), Position(x, y))) + } + return stones + } + + private fun getStoneContentValues(stone: Stone): ContentValues { + val values = ContentValues() + values.put("color", colorToStringInDb(stone.color)) + values.put("x", stone.position.x) + values.put("y", stone.position.y) + return values + } + + private fun colorToStringInDb(color: Color): String { + return when (color) { + Color.BLACK -> BLACK_STONE_COLOR + Color.WHITE -> WHITE_STONE_COLOR + } + } + + private fun stringToColorInDb(message: String): Color { + return when (message) { + BLACK_STONE_COLOR -> Color.BLACK + WHITE_STONE_COLOR -> Color.WHITE + else -> throw IllegalArgumentException("잘못된 색") + } + } + + companion object { + const val BLACK_STONE_COLOR = "흑돌" + const val WHITE_STONE_COLOR = "백돌" + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..7706ab9e6 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/black_stone.xml b/app/src/main/res/drawable/black_stone.xml new file mode 100644 index 000000000..e41ab996a --- /dev/null +++ b/app/src/main/res/drawable/black_stone.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/board_bottom.xml b/app/src/main/res/drawable/board_bottom.xml new file mode 100644 index 000000000..bdaed0648 --- /dev/null +++ b/app/src/main/res/drawable/board_bottom.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_bottom_left.xml b/app/src/main/res/drawable/board_bottom_left.xml new file mode 100644 index 000000000..9a84958f5 --- /dev/null +++ b/app/src/main/res/drawable/board_bottom_left.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_bottom_right.xml b/app/src/main/res/drawable/board_bottom_right.xml new file mode 100644 index 000000000..7bfc5610f --- /dev/null +++ b/app/src/main/res/drawable/board_bottom_right.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_center.xml b/app/src/main/res/drawable/board_center.xml new file mode 100644 index 000000000..160d32551 --- /dev/null +++ b/app/src/main/res/drawable/board_center.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_left.xml b/app/src/main/res/drawable/board_left.xml new file mode 100644 index 000000000..77ea5a9b7 --- /dev/null +++ b/app/src/main/res/drawable/board_left.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_right.xml b/app/src/main/res/drawable/board_right.xml new file mode 100644 index 000000000..c7ba218de --- /dev/null +++ b/app/src/main/res/drawable/board_right.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_top.xml b/app/src/main/res/drawable/board_top.xml new file mode 100644 index 000000000..b57c40320 --- /dev/null +++ b/app/src/main/res/drawable/board_top.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_top_left.xml b/app/src/main/res/drawable/board_top_left.xml new file mode 100644 index 000000000..329e5f399 --- /dev/null +++ b/app/src/main/res/drawable/board_top_left.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/board_top_right.xml b/app/src/main/res/drawable/board_top_right.xml new file mode 100644 index 000000000..0d5b25d3b --- /dev/null +++ b/app/src/main/res/drawable/board_top_right.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/white_stone.xml b/app/src/main/res/drawable/white_stone.xml new file mode 100644 index 000000000..5682b872f --- /dev/null +++ b/app/src/main/res/drawable/white_stone.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..7962c6dc8 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,1195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml new file mode 100644 index 000000000..b3e26b4c6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..c209e78ec Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..b2dfe3d1b Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..4f0f1d64e Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..62b611da0 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..948a3070f Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..1b9a6956b Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..28d4b77f9 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9287f5083 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..aa7d6427e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..9126ae37c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..9cc0341f6 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..ca1931bca --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..ea74e9730 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Omok + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..2735be95d --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..148c18b65 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..0c4f95cab --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/test/kotlin/.gitkeep b/app/src/test/java/woowacourse/omok/.gitkeep similarity index 100% rename from src/test/kotlin/.gitkeep rename to app/src/test/java/woowacourse/omok/.gitkeep diff --git a/build.gradle.kts b/build.gradle.kts index e78e72956..bf92af74f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,32 +1,15 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - kotlin("jvm") version "1.8.10" - id("org.jlleitschuh.gradle.ktlint") version "10.3.0" -} - -group = "camp.nextstep.edu" -version = "1.0-SNAPSHOT" + val agpVersion = "7.4.0" + id("com.android.application") version agpVersion apply false + id("com.android.library") version agpVersion apply false -repositories { - mavenCentral() -} - -dependencies { - testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2") - testImplementation("org.assertj", "assertj-core", "3.22.0") - testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3") + val kotlinVersion = "1.8.10" + kotlin("android") version kotlinVersion apply false + kotlin("jvm") version kotlinVersion apply false + id("org.jlleitschuh.gradle.ktlint") version "10.3.0" } -tasks { - compileKotlin { - kotlinOptions.jvmTarget = "11" - } - compileTestKotlin { - kotlinOptions.jvmTarget = "11" - } - test { - useJUnitPlatform() - } - ktlint { - verbose.set(true) - } +allprojects { + apply(plugin = "org.jlleitschuh.gradle.ktlint") } diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts new file mode 100644 index 000000000..b314ca2af --- /dev/null +++ b/domain/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + kotlin("jvm") +} + +group = "camp.nextstep.edu" +version = "1.0-SNAPSHOT" + +dependencies { + testImplementation("org.junit.jupiter", "junit-jupiter", "5.8.2") + testImplementation("org.assertj", "assertj-core", "3.22.0") + testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3") +} + +tasks { + compileKotlin { + kotlinOptions.jvmTarget = "11" + } + compileTestKotlin { + kotlinOptions.jvmTarget = "11" + } + test { + useJUnitPlatform() + } + ktlint { + verbose.set(true) + } +} diff --git a/src/main/kotlin/Application.kt b/domain/src/main/kotlin/Application.kt similarity index 70% rename from src/main/kotlin/Application.kt rename to domain/src/main/kotlin/Application.kt index 8ffb22a6b..a1d60e5ac 100644 --- a/src/main/kotlin/Application.kt +++ b/domain/src/main/kotlin/Application.kt @@ -1,5 +1,3 @@ -package domain - fun main() { Controller().run() } diff --git a/domain/src/main/kotlin/Controller.kt b/domain/src/main/kotlin/Controller.kt new file mode 100644 index 000000000..aa14cc270 --- /dev/null +++ b/domain/src/main/kotlin/Controller.kt @@ -0,0 +1,21 @@ +import domain.Board +import domain.OmokGame +import domain.RenjuRuleAdapter +import view.InputView +import view.OutputView + +class Controller { + fun run() { + OutputView.printStart() + val omokGame = OmokGame(Board(rule = RenjuRuleAdapter())) + OutputView.printCurrentState(omokGame) + while (omokGame.isRunning()) { + val position = InputView.inputPosition() + val stone = omokGame.getStone(position) + val isSuccess = omokGame.placeTo(stone) + if (isSuccess) OutputView.printCurrentState(omokGame) + } + val winner = omokGame.getWinnerColor() + if (winner != null) OutputView.printResult(winner, omokGame.board) + } +} diff --git a/domain/src/main/kotlin/domain/Board.kt b/domain/src/main/kotlin/domain/Board.kt new file mode 100644 index 000000000..bb9fafc54 --- /dev/null +++ b/domain/src/main/kotlin/domain/Board.kt @@ -0,0 +1,30 @@ +package domain + +class Board( + initStones: Stones = Stones(listOf()), + private val rule: Rule, +) { + var stones: Stones = initStones + private set + + fun placeStone(stone: Stone) { + stones = stones.addStone(stone) + } + + fun canPlace(stone: Stone): Boolean { + return !stones.isContainSamePositionStone(stone.position) + } + + fun getWinnerColor(): Color? { + return rule.getWinner(stones) + } + + fun removeAllStones() { + stones = Stones(listOf()) + } + + companion object { + private const val BOARD_SIZE = 15 + fun getSize(): Int = BOARD_SIZE + } +} diff --git a/src/main/kotlin/domain/Color.kt b/domain/src/main/kotlin/domain/Color.kt similarity index 100% rename from src/main/kotlin/domain/Color.kt rename to domain/src/main/kotlin/domain/Color.kt diff --git a/domain/src/main/kotlin/domain/OmokGame.kt b/domain/src/main/kotlin/domain/OmokGame.kt new file mode 100644 index 000000000..56d29bc9e --- /dev/null +++ b/domain/src/main/kotlin/domain/OmokGame.kt @@ -0,0 +1,55 @@ +package domain + +class OmokGame(val board: Board) { + var currentColor: Color = INITIAL_COLOR + private set + private var currentState: State = State.Running + + init { + changeCurrentColor() + } + + private fun changeStateFinished() { + if (board.getWinnerColor() != null) currentState = State.Finished + } + + fun getWinnerColor(): Color? { + return board.getWinnerColor() + } + + fun placeTo(stone: Stone): Boolean { + if (!board.canPlace(stone)) return false + board.placeStone(stone) + changeCurrentColor() + changeStateFinished() + return true + } + + fun getStone(position: Position): Stone { + return Stone(currentColor, position) + } + + private fun changeCurrentColor() { + val lastStone = board.stones.getLastStone() + lastStone?.let { lastStone -> + currentColor = when (lastStone.color) { + Color.BLACK -> Color.WHITE + Color.WHITE -> Color.BLACK + } + } + } + + fun isRunning(): Boolean { + return currentState == State.Running + } + + fun resetGame() { + board.removeAllStones() + currentState = State.Running + currentColor = INITIAL_COLOR + } + + companion object { + private val INITIAL_COLOR = Color.BLACK + } +} diff --git a/src/main/kotlin/domain/Position.kt b/domain/src/main/kotlin/domain/Position.kt similarity index 100% rename from src/main/kotlin/domain/Position.kt rename to domain/src/main/kotlin/domain/Position.kt diff --git a/domain/src/main/kotlin/domain/RenjuRuleAdapter.kt b/domain/src/main/kotlin/domain/RenjuRuleAdapter.kt new file mode 100644 index 000000000..cab16e1ea --- /dev/null +++ b/domain/src/main/kotlin/domain/RenjuRuleAdapter.kt @@ -0,0 +1,79 @@ +package domain + +import library.* // ktlint-disable no-wildcard-imports + +class RenjuRuleAdapter() : Rule { + + private val fourFourRule = FourFourRule(Board.getSize()) + private val threeThreeRule = ThreeThreeRule(Board.getSize()) + private val blackWinRule = BlackWinRule(Board.getSize()) + private val whiteWinRule = WhiteWinRule(Board.getSize()) + private val moreThanFiveRule = MoreThanFiveRule(Board.getSize()) + + override fun getWinner(stones: Stones): Color? { + val board = generateCustomBoard(stones) + val lastPlacedStone = stones.getLastStone() ?: return null + if (isBlackWin(board, lastPlacedStone)) return Color.BLACK + if (isWhiteWin(board, lastPlacedStone)) return Color.WHITE + return null + } + + private fun isBlackWin(board: List>, lastPlacedStone: Stone): Boolean { + if (lastPlacedStone.color != Color.BLACK) return false + return blackWinRule.validate( + board, + Pair(lastPlacedStone.position.x, lastPlacedStone.position.y), + ) + } + + private fun isWhiteWin(board: List>, lastPlacedStone: Stone): Boolean { + if (lastPlacedStone.color == Color.WHITE) { + return whiteWinRule.validate( + board, + Pair(lastPlacedStone.position.x, lastPlacedStone.position.y), + ) + } + return isBlackLose(board, lastPlacedStone) + } + + private fun isBlackLose(board: List>, lastPlacedStone: Stone): Boolean { + return isFourFour(board, lastPlacedStone) or isThreeThree( + board, + lastPlacedStone, + ) or isMoreThanFive(board, lastPlacedStone) + } + + private fun isFourFour(board: List>, stone: Stone): Boolean { + return fourFourRule.validate( + board, + Pair(stone.position.x, stone.position.y), + ) + } + + private fun isThreeThree(board: List>, stone: Stone): Boolean { + return threeThreeRule.validate( + board, + Pair(stone.position.x, stone.position.y), + ) + } + + private fun isMoreThanFive(board: List>, stone: Stone): Boolean { + return moreThanFiveRule.validate( + board, + Pair(stone.position.x, stone.position.y), + ) + } + + private fun generateCustomBoard(stones: Stones): List> { + val libraryBoard = List(Board.getSize()) { + MutableList(Board.getSize()) { 0 } + } + stones.values.forEach { + when (it.color) { + Color.BLACK -> libraryBoard[it.position.y][it.position.x] = 1 + Color.WHITE -> libraryBoard[it.position.y][it.position.x] = 2 + } + } + return libraryBoard + } +} diff --git a/domain/src/main/kotlin/domain/Rule.kt b/domain/src/main/kotlin/domain/Rule.kt new file mode 100644 index 000000000..2153fb08e --- /dev/null +++ b/domain/src/main/kotlin/domain/Rule.kt @@ -0,0 +1,5 @@ +package domain + +interface Rule { + fun getWinner(stones: Stones): Color? +} diff --git a/domain/src/main/kotlin/domain/State.kt b/domain/src/main/kotlin/domain/State.kt new file mode 100644 index 000000000..5089c5fe6 --- /dev/null +++ b/domain/src/main/kotlin/domain/State.kt @@ -0,0 +1,6 @@ +package domain + +sealed class State { + object Running : State() + object Finished : State() +} diff --git a/domain/src/main/kotlin/domain/Stone.kt b/domain/src/main/kotlin/domain/Stone.kt new file mode 100644 index 000000000..888c5af81 --- /dev/null +++ b/domain/src/main/kotlin/domain/Stone.kt @@ -0,0 +1,3 @@ +package domain + +data class Stone(val color: Color, val position: Position) diff --git a/domain/src/main/kotlin/domain/Stones.kt b/domain/src/main/kotlin/domain/Stones.kt new file mode 100644 index 000000000..53482bdef --- /dev/null +++ b/domain/src/main/kotlin/domain/Stones.kt @@ -0,0 +1,33 @@ +package domain + +class Stones(val values: List) { + + init { + checkHasDuplicatePosition() + } + + fun addStone(stone: Stone): Stones { + return Stones(values.plus(stone)) + } + + fun isContainSamePositionStone(position: Position): Boolean { + return values.any { it.position == position } + } + + fun getLastStone(): Stone? { + return values.lastOrNull() + } + + private fun checkHasDuplicatePosition() { + check(values.all { stone -> getCountSamePositionStone(stone) == ONLY_ONE_POSITION }) { DUPLICATE_POSITION_ERROR } + } + + private fun getCountSamePositionStone(stone: Stone): Int { + return values.count { it.position == stone.position } + } + + companion object { + private const val DUPLICATE_POSITION_ERROR = "같은 위치에 있는 돌이 존재할 수 없어요!" + private const val ONLY_ONE_POSITION = 1 + } +} diff --git a/domain/src/main/kotlin/library/BlackWinRule.kt b/domain/src/main/kotlin/library/BlackWinRule.kt new file mode 100644 index 000000000..2462440bf --- /dev/null +++ b/domain/src/main/kotlin/library/BlackWinRule.kt @@ -0,0 +1,22 @@ +package library + +import domain.Rule + +class BlackWinRule(boardSize: Int) : OmokRule(boardSize) { + override fun validate(board: List>, position: Pair): Boolean = + directions.map { direction -> checkWhiteWin(board, position, direction) }.contains(true) + + private fun checkWhiteWin( + board: List>, + position: Pair, + direction: Pair + ): Boolean { + val oppositeDirection = direction.let { (dx, dy) -> Pair(-dx, -dy) } + val (stone1, blink1) = search(board, position, oppositeDirection) + val (stone2, blink2) = search(board, position, direction) + return when { + blink1 + blink2 == 0 && stone1 + stone2 == 4 -> true + else -> false + } + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/library/FourFourRule.kt b/domain/src/main/kotlin/library/FourFourRule.kt new file mode 100644 index 000000000..6f016cd2d --- /dev/null +++ b/domain/src/main/kotlin/library/FourFourRule.kt @@ -0,0 +1,46 @@ +package library + +class FourFourRule(boardSize: Int) : OmokRule(boardSize) { + override fun validate(board: List>, position: Pair): Boolean = + countOpenThrees(board, position) >= 2 + + private fun countOpenThrees(board: List>, position: Pair): Int = + directions.sumOf { direction -> checkOpenFour(board, position, direction) } + + private fun checkOpenFour( + board: List>, + position: Pair, + direction: Pair, + ): Int { + val (x, y) = position + val (dx, dy) = direction + val oppositeDirection = direction.let { (dx, dy) -> Pair(-dx, -dy) } + val (stone1, blink1) = search(board, position, oppositeDirection) + val (stone2, blink2) = search(board, position, direction) + val leftDown = stone1 + blink1 + val left = dx * (leftDown + 1) + val down = dy * (leftDown + 1) + val rightUp = stone2 + blink2 + val right = dx * (rightUp + 1) + val up = dy * (rightUp + 1) + when { + blink1 + blink2 == 2 && stone1 + stone2 == 4 -> return 2 + blink1 + blink2 == 2 && stone1 + stone2 == 5 -> return 2 + stone1 + stone2 != 3 -> return 0 + blink1 + blink2 == 2 -> return 0 + } + val leftDownValid = when { + dx != 0 && x - dx * leftDown in xEdge -> 0 + dy != 0 && y - dy * leftDown in yEdge -> 0 + board[y - down][x - left] == opponentStone -> 0 + else -> 1 + } + val rightUpValid = when { + dx != 0 && x + (dx * rightUp) in xEdge -> 0 + dy != 0 && y + (dy * rightUp) in yEdge -> 0 + board[y + up][x + right] == opponentStone -> 0 + else -> 1 + } + return if (leftDownValid + rightUpValid >= 1) 1 else 0 + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/library/MoreThanFiveRule.kt b/domain/src/main/kotlin/library/MoreThanFiveRule.kt new file mode 100644 index 000000000..c47b528fc --- /dev/null +++ b/domain/src/main/kotlin/library/MoreThanFiveRule.kt @@ -0,0 +1,20 @@ +package library + +class MoreThanFiveRule(boardSize: Int) : OmokRule(boardSize) { + override fun validate(board: List>, position: Pair): Boolean = + directions.map { direction -> checkBlackWin(board, position, direction) }.contains(true) + + private fun checkBlackWin( + board: List>, + position: Pair, + direction: Pair + ): Boolean { + val oppositeDirection = direction.let { (dx, dy) -> Pair(-dx, -dy) } + val (stone1, blink1) = search(board, position, oppositeDirection) + val (stone2, blink2) = search(board, position, direction) + return when { + blink1 + blink2 == 0 && stone1 + stone2 > 4 -> true + else -> false + } + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/library/OmokRule.kt b/domain/src/main/kotlin/library/OmokRule.kt new file mode 100644 index 000000000..e3a0df30a --- /dev/null +++ b/domain/src/main/kotlin/library/OmokRule.kt @@ -0,0 +1,80 @@ +package library + +abstract class OmokRule( + boardSize: Int, + private val currentStone: Int = BLACK_STONE, + val opponentStone: Int = WHITE_STONE, +) { + + private val maxX = boardSize - 1 + private val maxY = boardSize - 1 + protected val xEdge = listOf(MIN_X, maxX) + protected val yEdge = listOf(MIN_Y, maxY) + + abstract fun validate(board: List>, position: Pair): Boolean + protected val directions = listOf(Pair(1, 0), Pair(1, 1), Pair(0, 1), Pair(1, -1)) + protected fun search( + board: List>, + position: Pair, + direction: Pair, + ): Pair { + var (x, y) = position + val (dx, dy) = direction + var stone = 0 + var blink = 0 + var blinkCount = 0 + while (willExceedBounds(x, y, dx, dy).not()) { + x += dx + y += dy + when (board[y][x]) { + currentStone -> { + stone++ + blink = blinkCount + } + opponentStone -> break + EMPTY_STONE -> { + if (blink == 1) break + if (blinkCount++ == 1) break + } + else -> throw IllegalArgumentException("스톤 케이스를 에러") + } + } + return Pair(stone, blink) + } + + protected fun countToWall( + board: List>, + position: Pair, + direction: Pair, + ): Int { + var (x, y) = position + val (dx, dy) = direction + var distance = 0 + while (willExceedBounds(x, y, dx, dy).not()) { + x += dx + y += dy + when (board[y][x]) { + in listOf(currentStone, EMPTY_STONE) -> distance++ + opponentStone -> break + else -> throw IllegalArgumentException() + } + } + return distance + } + + private fun willExceedBounds(x: Int, y: Int, dx: Int, dy: Int): Boolean = when { + dx > 0 && x == maxX -> true + dx < 0 && x == MIN_X -> true + dy > 0 && y == maxY -> true + dy < 0 && y == MIN_Y -> true + else -> false + } + + companion object { + protected const val EMPTY_STONE = 0 + const val BLACK_STONE = 1 + const val WHITE_STONE = 2 + const val MIN_X = 0 + const val MIN_Y = 0 + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/library/ThreeThreeRule.kt b/domain/src/main/kotlin/library/ThreeThreeRule.kt new file mode 100644 index 000000000..575517b30 --- /dev/null +++ b/domain/src/main/kotlin/library/ThreeThreeRule.kt @@ -0,0 +1,43 @@ +package library + +class ThreeThreeRule(boardSize: Int) : OmokRule(boardSize) { + override fun validate(board: List>, position: Pair): Boolean = + countOpenThrees(board, position) >= 2 + + private fun countOpenThrees(board: List>, position: Pair): Int = + directions.sumOf { direction -> checkOpenThree(board, position, direction) } + + private fun checkOpenThree( + board: List>, + position: Pair, + direction: Pair, + ): Int { + val (x, y) = position + val (dx, dy) = direction + val oppositeDirection = direction.let { (dx, dy) -> Pair(-dx, -dy) } + val (stone1, blink1) = search(board, position, oppositeDirection) + val (stone2, blink2) = search(board, position, direction) + val leftDown = stone1 + blink1 + val left = dx * (leftDown + 1) + val down = dy * (leftDown + 1) + val rightUp = stone2 + blink2 + val right = dx * (rightUp + 1) + val up = dy * (rightUp + 1) + return when { + stone1 + stone2 != 2 -> 0 + blink1 + blink2 == 2 -> 0 + dx != 0 && x - dx * leftDown in xEdge -> 0 + dy != 0 && y - dy * leftDown in yEdge -> 0 + dx != 0 && x + dx * rightUp in xEdge -> 0 + dy != 0 && y + dy * rightUp in yEdge -> 0 + board[y - down][x - left] == WHITE_STONE -> 0 + board[y + up][x + right] == WHITE_STONE -> 0 + countToWall(board, position, oppositeDirection) + countToWall( + board, + position, + direction + ) <= 5 -> 0 + else -> 1 + } + } +} \ No newline at end of file diff --git a/domain/src/main/kotlin/library/WhiteWinRule.kt b/domain/src/main/kotlin/library/WhiteWinRule.kt new file mode 100644 index 000000000..14f89315a --- /dev/null +++ b/domain/src/main/kotlin/library/WhiteWinRule.kt @@ -0,0 +1,20 @@ +package library + +class WhiteWinRule(boardSize: Int) : OmokRule(boardSize,WHITE_STONE, BLACK_STONE) { + override fun validate(board: List>, position: Pair): Boolean = + directions.map { direction -> checkBlackWin(board, position, direction) }.contains(true) + + private fun checkBlackWin( + board: List>, + position: Pair, + direction: Pair + ): Boolean { + val oppositeDirection = direction.let { (dx, dy) -> Pair(-dx, -dy) } + val (stone1, blink1) = search(board, position, oppositeDirection) + val (stone2, blink2) = search(board, position, direction) + return when { + blink1 + blink2 == 0 && stone1 + stone2 >= 4 -> true + else -> false + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/view/AlphabetCoordinate.kt b/domain/src/main/kotlin/view/AlphabetCoordinate.kt similarity index 100% rename from src/main/kotlin/view/AlphabetCoordinate.kt rename to domain/src/main/kotlin/view/AlphabetCoordinate.kt diff --git a/src/main/kotlin/view/BoardParts.kt b/domain/src/main/kotlin/view/BoardParts.kt similarity index 100% rename from src/main/kotlin/view/BoardParts.kt rename to domain/src/main/kotlin/view/BoardParts.kt diff --git a/src/main/kotlin/view/InputView.kt b/domain/src/main/kotlin/view/InputView.kt similarity index 100% rename from src/main/kotlin/view/InputView.kt rename to domain/src/main/kotlin/view/InputView.kt diff --git a/src/main/kotlin/view/OutputView.kt b/domain/src/main/kotlin/view/OutputView.kt similarity index 80% rename from src/main/kotlin/view/OutputView.kt rename to domain/src/main/kotlin/view/OutputView.kt index 96da44aeb..f314b546c 100644 --- a/src/main/kotlin/view/OutputView.kt +++ b/domain/src/main/kotlin/view/OutputView.kt @@ -1,23 +1,20 @@ package view -import domain.Board -import domain.Color -import domain.Position -import domain.Stones +import domain.* object OutputView { - fun printCurrentState(board: Board) { - printBoard(board) - printTurn(board.getCurrentTurn()) - printLastPosition(board.getLastPosition()) + fun printCurrentState(omokGame: OmokGame) { + printBoard(omokGame.board) + printTurn(omokGame.currentColor) + printLastPosition(omokGame.board.stones.getLastStone()) } fun printStart() { println("오목 게임을 시작합니다.") } - fun printBoard(board: Board) { + private fun printBoard(board: Board) { val customBoard = generateCustomBoard(board.stones) customBoard.forEachIndexed { y, colors -> print("${Board.getSize() - y} ".padStart(4, ' ')) @@ -40,19 +37,19 @@ object OutputView { println() } - fun printTurn(color: Color) { + private fun printTurn(color: Color) { when (color) { Color.BLACK -> print("흑의 차례입니다.") Color.WHITE -> print("백의 차례입니다.") } } - fun printLastPosition(position: Position?) { - if (position == null) { + private fun printLastPosition(stone: Stone?) { + if (stone == null) { println() return } - println(" (마지막 돌의 위치: ${AlphabetCoordinate.convertAlphabet(position.x)}${position.y})") + println(" (마지막 돌의 위치: ${AlphabetCoordinate.convertAlphabet(stone.position.x)}${stone.position.y})") } fun printResult(color: Color, board: Board) { @@ -68,7 +65,7 @@ object OutputView { MutableList(Board.getSize()) { 0 } } stones.values.forEach { - if (it.isBlack()) { + if (it.color== Color.BLACK) { initBoard[Board.getSize() - it.position.y][it.position.x] = BLACK } else { initBoard[Board.getSize() - it.position.y][it.position.x] = WHITE diff --git a/domain/src/test/kotlin/domain/BoardTest.kt b/domain/src/test/kotlin/domain/BoardTest.kt new file mode 100644 index 000000000..f981641fe --- /dev/null +++ b/domain/src/test/kotlin/domain/BoardTest.kt @@ -0,0 +1,218 @@ +package domain + +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class BoardTest { + private fun Stone(color: Color, vararg positions: Int): Stone { + return Stone(color, Position(positions[0], positions[1])) + } + + @Test + fun `보드에 바둑돌을 놓으면,보드에 바둑돌이 추가된다`() { + // given + val board = Board(rule = RenjuRuleAdapter()) + val newStone = Stone(Color.WHITE, Position(1, 2)) + // when + board.placeStone(newStone) + // then + val actual = board.stones.values + val expected = listOf(newStone) + Assertions.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `같은 위치에 바둑돌은 놓을 수 없다`() { + // given + val stone = Stone(Color.WHITE, Position(1, 2)) + val board = Board(Stones(listOf(stone)), rule = RenjuRuleAdapter()) + val samePositionStone = Stone(Color.BLACK, Position(1, 2)) + // when + board.placeStone(samePositionStone) + val actual = board.stones.values.size + val expected = 1 + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `다른 위치에 바둑돌을 놓을 수 있다`() { + // given + val stone = Stone(Color.WHITE, Position(1, 2)) + val board = Board(Stones(listOf(stone)), rule = RenjuRuleAdapter()) + val differentPositionStone = Stone(Color.WHITE, Position(2, 3)) + // when + board.placeStone(differentPositionStone) + val actual = board.stones.values.size + val expected = 2 + // then + assertThat(actual).isEqualTo(expected) + } + + // 1 + // 2 ◎ ● + // 3 ◎ ● + // 4 ◎ ● ● + // 5 ◎ + // 6 ? + // 1 2 3 4 5 6 + @Test + fun `흑의 오목이 완성되면 흑의의 승리이다`() { + // given + val board = generateBlackWinOmokBoard() + val stone = Stone(Color.BLACK, 1, 6) + // when + val actual = board.getWinnerColor() + // then + assertThat(actual).isEqualTo(Color.BLACK) + } + + private fun generateBlackWinOmokBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 1, 2)) + placeStone(Stone(Color.WHITE, 2, 2)) + placeStone(Stone(Color.BLACK, 1, 3)) + placeStone(Stone(Color.WHITE, 2, 3)) + placeStone(Stone(Color.BLACK, 1, 4)) + placeStone(Stone(Color.WHITE, 2, 4)) + placeStone(Stone(Color.BLACK, 1, 5)) + placeStone(Stone(Color.WHITE, 4, 8)) + placeStone(Stone(Color.BLACK, 1, 6)) + } + return board + } + + // 1 + // 2 ● ◎ + // 3 ● ◎ + // 4 ● ◎ ◎ ◎ + // 5 ● + // 6 ? + // 1 2 3 4 5 6 + @Test + fun `백의 오목이 완성되면 백의 승리이다`() { + // given + val board = generateWhiteWinOmokBoard() + // when + val actual = board.getWinnerColor() + // then + assertThat(actual).isEqualTo(Color.WHITE) + } + + private fun generateWhiteWinOmokBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 2, 2)) + placeStone(Stone(Color.WHITE, 1, 2)) + placeStone(Stone(Color.BLACK, 2, 3)) + placeStone(Stone(Color.WHITE, 1, 3)) + placeStone(Stone(Color.BLACK, 2, 4)) + placeStone(Stone(Color.WHITE, 1, 4)) + placeStone(Stone(Color.BLACK, 2, 5)) + placeStone(Stone(Color.WHITE, 1, 5)) + placeStone(Stone(Color.BLACK, 2, 10)) + placeStone(Stone(Color.WHITE, 1, 6)) + } + return board + } + + // 1 + // 2 ● ◎ + // 3 ● ◎ + // 4 ● ? ◎ ◎ ● + // 5 + // 6 + // 1 2 3 4 5 6 + @Test + fun `흑이 33이면 백의 승리이다(흑의 패배이다)`() { + // given + val board = generateThreeThreeBoard() + // when + val actual = board.getWinnerColor() + // then + assertThat(actual).isEqualTo(Color.WHITE) + } + + private fun generateThreeThreeBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 2, 2)) + placeStone(Stone(Color.WHITE, 4, 7)) + placeStone(Stone(Color.BLACK, 2, 3)) + placeStone(Stone(Color.WHITE, 4, 8)) + placeStone(Stone(Color.BLACK, 3, 4)) + placeStone(Stone(Color.WHITE, 5, 13)) + placeStone(Stone(Color.BLACK, 4, 4)) + placeStone(Stone(Color.WHITE, 6, 10)) + placeStone(Stone(Color.BLACK, 2, 4)) + } + return board + } + + // 3 ? ◎ ◎ ◎ ● + // 4 ◎ + // 5 + // 6 ◎ + // 7 ● ● ● ● ◎ ● + // 3 4 5 6 7 8 9 10 + @Test + fun `흑이 44이면 백의 승리이다(흑의 패배이다)`() { + // given + val board = generateFourFourBoard() + // when + val actual = board.getWinnerColor() + // then + assertThat(actual).isEqualTo(Color.WHITE) + } + + private fun generateFourFourBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 4, 3)) + placeStone(Stone(Color.WHITE, 3, 7)) + placeStone(Stone(Color.BLACK, 6, 3)) + placeStone(Stone(Color.WHITE, 4, 7)) + placeStone(Stone(Color.BLACK, 7, 3)) + placeStone(Stone(Color.WHITE, 5, 7)) + placeStone(Stone(Color.BLACK, 4, 4)) + placeStone(Stone(Color.WHITE, 6, 7)) + placeStone(Stone(Color.BLACK, 6, 6)) + placeStone(Stone(Color.WHITE, 8, 7)) + placeStone(Stone(Color.BLACK, 7, 7)) + placeStone(Stone(Color.WHITE, 8, 3)) + placeStone(Stone(Color.BLACK, 3, 3)) + } + return board + } + + // 3 ◎ ◎ ? ◎ ◎ ◎ + // 4 + // 5 + // 6 + // 7 ● ● ● ● ● + // 3 4 5 6 7 8 9 10 + @Test + fun `흑이 장목이면 백의 승리이다(흑의 패배이다)`() { + // given + val board = generateMoreFiveBoard() + // when + val actual = board.getWinnerColor() + // then + assertThat(actual).isEqualTo(Color.WHITE) + } + + private fun generateMoreFiveBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 3, 3)) + placeStone(Stone(Color.WHITE, 3, 7)) + placeStone(Stone(Color.BLACK, 4, 3)) + placeStone(Stone(Color.WHITE, 4, 7)) + placeStone(Stone(Color.BLACK, 6, 3)) + placeStone(Stone(Color.WHITE, 5, 7)) + placeStone(Stone(Color.BLACK, 7, 3)) + placeStone(Stone(Color.WHITE, 6, 7)) + placeStone(Stone(Color.BLACK, 8, 3)) + placeStone(Stone(Color.WHITE, 8, 7)) + placeStone(Stone(Color.BLACK, 5, 3)) + } + return board + } +} diff --git a/domain/src/test/kotlin/domain/OmokGameTest.kt b/domain/src/test/kotlin/domain/OmokGameTest.kt new file mode 100644 index 000000000..f31686f58 --- /dev/null +++ b/domain/src/test/kotlin/domain/OmokGameTest.kt @@ -0,0 +1,120 @@ +package domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class OmokGameTest { + private fun Stone(color: Color, vararg positions: Int): Stone { + return Stone(color, Position(positions[0], positions[1])) + } + + @Test + fun `흑이 이겼을 경우 검정색을 반환한다`() { + // given + val board = generateBlackWinOmokBoard() + val omokGame = OmokGame(board) + // when + val stone = omokGame.getStone(Position(1, 6)) + omokGame.placeTo(stone) + val actual = omokGame.getWinnerColor() + val expected = Color.BLACK + // then + assertThat(actual).isEqualTo(expected) + } + + private fun generateBlackWinOmokBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 1, 2)) + placeStone(Stone(Color.WHITE, 2, 2)) + placeStone(Stone(Color.BLACK, 1, 3)) + placeStone(Stone(Color.WHITE, 2, 3)) + placeStone(Stone(Color.BLACK, 1, 4)) + placeStone(Stone(Color.WHITE, 2, 4)) + placeStone(Stone(Color.BLACK, 1, 5)) + placeStone(Stone(Color.WHITE, 4, 8)) + } + return board + } + + @Test + fun `백이 이겼을 경우 하얀색을 반환한다`() { + // given + val board = generateWhiteWinOmokBoard() + val omokGame = OmokGame(board) + // when + val stone = omokGame.getStone(Position(1, 6)) + omokGame.placeTo(stone) + val actual = omokGame.getWinnerColor() + val expected = Color.WHITE + // then + assertThat(actual).isEqualTo(expected) + } + + private fun generateWhiteWinOmokBoard(): Board { + val board = Board(rule = RenjuRuleAdapter()).apply { + placeStone(Stone(Color.BLACK, 2, 2)) + placeStone(Stone(Color.WHITE, 1, 2)) + placeStone(Stone(Color.BLACK, 2, 3)) + placeStone(Stone(Color.WHITE, 1, 3)) + placeStone(Stone(Color.BLACK, 2, 4)) + placeStone(Stone(Color.WHITE, 1, 4)) + placeStone(Stone(Color.BLACK, 2, 5)) + placeStone(Stone(Color.WHITE, 1, 5)) + placeStone(Stone(Color.BLACK, 2, 10)) + } + return board + } + + @Test + fun `게임을 초기화 하면 돌의 개수는 0 이된다`() { + // given + val board = generateWhiteWinOmokBoard() + val omokGame = OmokGame(board) + + // when + omokGame.resetGame() + val actual = omokGame.board.stones.values.size + val expected = 0 + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `게임을 초기화 하면 게임의 턴은 흑이된다`() { + // given + val board = generateWhiteWinOmokBoard() + val omokGame = OmokGame(board) + // when + omokGame.resetGame() + val actual = omokGame.currentColor + val expected = Color.BLACK + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `마지막에 위치한 돌이 검정색이라면, 다음 차례는 흰색이다`() { + val board = Board(Stones(listOf(Stone(Color.BLACK, 1, 1))), RenjuRuleAdapter()) + val omokGame = OmokGame(board) + // when + val actual = omokGame.currentColor + val expected = Color.WHITE + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `마지막에 위치한 돌이 흰색이라면, 다음 차례는 검정색이다`() { + val board = Board(Stones(listOf(Stone(Color.WHITE, 1, 1))), RenjuRuleAdapter()) + val omokGame = OmokGame(board) + // when + val actual = omokGame.currentColor + val expected = Color.BLACK + + // then + assertThat(actual).isEqualTo(expected) + } +} diff --git a/src/test/kotlin/domain/PositionTest.kt b/domain/src/test/kotlin/domain/PositionTest.kt similarity index 100% rename from src/test/kotlin/domain/PositionTest.kt rename to domain/src/test/kotlin/domain/PositionTest.kt diff --git a/domain/src/test/kotlin/domain/StoneTest.kt b/domain/src/test/kotlin/domain/StoneTest.kt new file mode 100644 index 000000000..10f31da38 --- /dev/null +++ b/domain/src/test/kotlin/domain/StoneTest.kt @@ -0,0 +1,17 @@ +package domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class StoneTest { + @Test + fun `바둑돌은 색상과 위치를 갖는다`() { + // given + val position = Position(1, 1) + val color = Color.BLACK + // when + val actual = Stone(color, position) + // then + assertThat(actual).isInstanceOf(Stone::class.java) + } +} diff --git a/domain/src/test/kotlin/domain/StonesTest.kt b/domain/src/test/kotlin/domain/StonesTest.kt new file mode 100644 index 000000000..8f09fbbfc --- /dev/null +++ b/domain/src/test/kotlin/domain/StonesTest.kt @@ -0,0 +1,36 @@ +package domain + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class StonesTest { + @Test + fun `다른 위치에 있는 바둑돌을 추가 할 수 있다`() { + // given + val stones = makeStones() + val newStone = Stone(Color.BLACK, Position(5, 6)) + // when + val actual = stones.addStone(newStone).values + val expected = makeStones().values + newStone + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `같은 위치의 바둑돌을 포함하고 있을 수 없다`() { + assertThrows { makeDuplicateStones() } + } + + private fun makeStones(): Stones { + val blackStone = Stone(Color.BLACK, Position(1, 2)) + val whiteStone = Stone(Color.WHITE, Position(2, 3)) + return Stones(listOf(blackStone, whiteStone)) + } + + private fun makeDuplicateStones(): Stones { + val blackStone = Stone(Color.BLACK, Position(1, 2)) + val whiteStone = Stone(Color.WHITE, Position(2, 3)) + return Stones(listOf(blackStone, whiteStone)) + } +} diff --git a/gradle.properties b/gradle.properties index 7fc6f1ff2..2cbd6d19d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# 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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d04cb9e8..5c61d823d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} rootProject.name = "kotlin-omok" +include(":app") +include(":domain") diff --git a/src/main/kotlin/Controller.kt b/src/main/kotlin/Controller.kt deleted file mode 100644 index 3caf9c867..000000000 --- a/src/main/kotlin/Controller.kt +++ /dev/null @@ -1,13 +0,0 @@ -package domain - -import view.InputView -import view.OutputView - -class Controller { - fun run() { - OutputView.printStart() - val omokGame = OmokGame(Board()) - val winnerColor = omokGame.getWinnerColor(OutputView::printCurrentState, InputView::inputPosition) - OutputView.printResult(winnerColor, omokGame.board) - } -} diff --git a/src/main/kotlin/domain/Board.kt b/src/main/kotlin/domain/Board.kt deleted file mode 100644 index 13460af0a..000000000 --- a/src/main/kotlin/domain/Board.kt +++ /dev/null @@ -1,42 +0,0 @@ -package domain - -class Board( - initStones: Stones = Stones(listOf()), -) { - var stones: Stones = initStones - private set - private val rule: Rule - get() = RuleAdapter(stones, getCurrentTurn()) - - fun placeStone(stone: Stone) { - stones = stones.addStone(stone) - } - - fun isEmpty(stone: Stone): Boolean { - return !stones.isContainSamePositionStone(stone.position) - } - - fun getLastPosition(): Position? { - if (stones.values.isEmpty()) return null - return stones.values.last().position - } - - fun isBlackWin(stone: Stone): Boolean { - return rule.checkBlackWin(stone) - } - - fun isWhiteWin(stone: Stone): Boolean { - if (rule.checkInvalid(stone)) return true - return rule.checkWhiteWin(stone) - } - - fun getCurrentTurn(): Color { - if (stones.getBlackStonesCount() > stones.getWhiteStonesCount()) return Color.WHITE - return Color.BLACK - } - - companion object { - private const val BOARD_SIZE = 15 - fun getSize(): Int = BOARD_SIZE - } -} diff --git a/src/main/kotlin/domain/OmokGame.kt b/src/main/kotlin/domain/OmokGame.kt deleted file mode 100644 index 178f2a39a..000000000 --- a/src/main/kotlin/domain/OmokGame.kt +++ /dev/null @@ -1,25 +0,0 @@ -package domain - -class OmokGame(val board: Board) { - fun getWinnerColor(showCurrentState: (Board) -> Unit, getPosition: () -> Position): Color { - val stone = getStone(showCurrentState, getPosition) - val winnerColor = judgeWinner(stone) - board.placeStone(stone) - return winnerColor ?: return getWinnerColor(showCurrentState, getPosition) - } - - private fun getStone(showCurrentState: (Board) -> Unit, getPosition: () -> Position): Stone { - showCurrentState(board) - val stone = Stone(board.getCurrentTurn(), getPosition()) - if (!board.isEmpty(stone)) return getStone(showCurrentState, getPosition) - return stone - } - - private fun judgeWinner(stone: Stone): Color? { - when { - board.isBlackWin(stone) -> return Color.BLACK - board.isWhiteWin(stone) -> return Color.WHITE - } - return null - } -} diff --git a/src/main/kotlin/domain/Rule.kt b/src/main/kotlin/domain/Rule.kt deleted file mode 100644 index 4b8e869bd..000000000 --- a/src/main/kotlin/domain/Rule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package domain - -interface Rule { - fun checkThreeThree(stone: Stone): Boolean - fun checkFourFour(stone: Stone): Boolean - fun checkBlackWin(stone: Stone): Boolean - fun checkWhiteWin(stone: Stone): Boolean - fun checkMoreThanFive(stone: Stone): Boolean - fun checkInvalid(stone: Stone): Boolean -} diff --git a/src/main/kotlin/domain/RuleAdapter.kt b/src/main/kotlin/domain/RuleAdapter.kt deleted file mode 100644 index 5d435b9be..000000000 --- a/src/main/kotlin/domain/RuleAdapter.kt +++ /dev/null @@ -1,64 +0,0 @@ -package domain - -import library.OmokRule - -class RuleAdapter(stones: Stones, currentColor: Color) : Rule { - private val omokRule = - OmokRule( - generateCustomBoard(stones), - colorToInt(currentColor), - getOtherColorToInt(currentColor), - Board.getSize(), - ) - - override fun checkThreeThree(stone: Stone): Boolean { - return omokRule.checkThreeThree(stone.position.x, stone.position.y) - } - - override fun checkFourFour(stone: Stone): Boolean { - return omokRule.countFourFour(stone.position.x, stone.position.y) - } - - override fun checkBlackWin(stone: Stone): Boolean { - if (stone.isBlack()) return omokRule.validateBlackWin(stone.position.x, stone.position.y) - return false - } - - override fun checkWhiteWin(stone: Stone): Boolean { - if (stone.isWhite()) return omokRule.validateWhiteWin(stone.position.x, stone.position.y) - return false - } - - override fun checkMoreThanFive(stone: Stone): Boolean { - return omokRule.checkMoreThanFive(stone.position.x, stone.position.y) - } - - override fun checkInvalid(stone: Stone): Boolean { - if (stone.isBlack()) return checkThreeThree(stone) || checkFourFour(stone) || checkMoreThanFive(stone) - return false - } - - private fun generateCustomBoard(stones: Stones): List> { - val libraryBoard = List(Board.getSize()) { - MutableList(Board.getSize()) { 0 } - } - stones.values.forEach { - if (it.isBlack()) { - libraryBoard[it.position.y][it.position.x] = 1 - } else { - libraryBoard[it.position.y][it.position.x] = 2 - } - } - return libraryBoard - } - - private fun colorToInt(color: Color): Int { - if (color == Color.BLACK) return 1 - return 2 - } - - private fun getOtherColorToInt(color: Color): Int { - if (color == Color.BLACK) return 2 - return 1 - } -} diff --git a/src/main/kotlin/domain/Stone.kt b/src/main/kotlin/domain/Stone.kt deleted file mode 100644 index 0a17d8f76..000000000 --- a/src/main/kotlin/domain/Stone.kt +++ /dev/null @@ -1,12 +0,0 @@ -package domain - -data class Stone(val color: Color, val position: Position) { - - fun isBlack(): Boolean { - return color == Color.BLACK - } - - fun isWhite(): Boolean { - return color == Color.WHITE - } -} diff --git a/src/main/kotlin/domain/Stones.kt b/src/main/kotlin/domain/Stones.kt deleted file mode 100644 index 2847354f7..000000000 --- a/src/main/kotlin/domain/Stones.kt +++ /dev/null @@ -1,28 +0,0 @@ -package domain - -class Stones(values: List) { - private val _values = values.deepCopy() - val values: List - get() = _values.deepCopy() - - private fun List.deepCopy(): List = map { it.copy() } - fun addStone(stone: Stone): Stones { - val newStones = values.toMutableList() - newStones.add(stone) - return Stones(newStones) - } - - fun isContainSamePositionStone(position: Position): Boolean { - return values.any { it.position == position } - } - - fun getBlackStonesCount(): Int { - if (values.isEmpty()) return 0 - return values.count { it.isBlack() } - } - - fun getWhiteStonesCount(): Int { - if (values.isEmpty()) return 0 - return values.count { it.isWhite() } - } -} diff --git a/src/main/kotlin/library/OmokRule.kt b/src/main/kotlin/library/OmokRule.kt deleted file mode 100644 index c7b52da79..000000000 --- a/src/main/kotlin/library/OmokRule.kt +++ /dev/null @@ -1,174 +0,0 @@ -package library - -class OmokRule( - private val board: List>, - private val currentStone: Int = BLACK_STONE, - private val otherStone: Int = WHITE_STONE, - private val boardSize: Int, -) { - private val directions = listOf(listOf(1, 0), listOf(1, 1), listOf(0, 1), listOf(1, -1)) - fun checkThreeThree(x: Int, y: Int): Boolean = - directions.sumOf { direction -> checkOpenThree(x, y, direction[0], direction[1]) } >= 2 - - fun countFourFour(x: Int, y: Int): Boolean = - directions.sumOf { direction -> checkOpenFour(x, y, direction[0], direction[1]) } >= 2 - - fun checkMoreThanFive(x: Int, y: Int): Boolean = - directions.map { direction -> checkMoreThanFive(x, y, direction[0], direction[1]) }.contains(true) - - fun validateWhiteWin(x: Int, y: Int): Boolean = - directions.map { direction -> checkWhiteWin(x, y, direction[0], direction[1]) }.contains(true) - - fun validateBlackWin(x: Int, y: Int): Boolean = - directions.map { direction -> checkBlackWin(x, y, direction[0], direction[1]) }.contains(true) - - private fun checkOpenThree(x: Int, y: Int, dx: Int, dy: Int): Int { - val (stone1, blink1) = search(x, y, -dx, -dy) - val (stone2, blink2) = search(x, y, dx, dy) - - val leftDown = stone1 + blink1 - val left = dx * (leftDown + 1) - val down = dy * (leftDown + 1) - - val rightUp = stone2 + blink2 - val right = dx * (rightUp + 1) - val up = dy * (rightUp + 1) - - return when { - stone1 + stone2 != 2 -> 0 - blink1 + blink2 == 2 -> 0 - dx != 0 && x - leftDown in listOf(MIN_X, boardSize - 1) -> 0 - dy != 0 && y - leftDown in listOf(MIN_Y, boardSize - 1) -> 0 - dx != 0 && x + rightUp in listOf(MIN_X, boardSize - 1) -> 0 - dy != 0 && y + rightUp in listOf(MIN_Y, boardSize - 1) -> 0 - board[y - down][x - left] == otherStone -> 0 - board[y + up][x + right] == otherStone -> 0 - countToWall(x, y, -dx, -dy) + countToWall(x, y, dx, dy) <= 5 -> 0 - else -> 1 - } - } - - private fun countToWall(x: Int, y: Int, dx: Int, dy: Int): Int { - var toRight = x - var toTop = y - var distance = 0 - while (true) { - if (dx > 0 && toRight == boardSize - 1) break - if (dx < 0 && toRight == MIN_X) break - if (dy > 0 && toTop == boardSize - 1) break - if (dy < 0 && toTop == MIN_X) break - toRight += dx - toTop += dy - when (board[toTop][toRight]) { - in listOf(currentStone, EMPTY_STONE) -> distance++ - otherStone -> break - else -> throw IllegalArgumentException() - } - } - return distance - } - - private fun checkOpenFour(x: Int, y: Int, dx: Int, dy: Int): Int { - val (stone1, blink1) = search(x, y, -dx, -dy) - val (stone2, blink2) = search(x, y, dx, dy) - - val leftDown = stone1 + blink1 - val left = dx * (leftDown + 1) - val down = dy * (leftDown + 1) - - val rightUp = stone2 + blink2 - val right = dx * (rightUp + 1) - val up = dy * (rightUp + 1) - - when { - blink1 + blink2 == 2 && stone1 + stone2 == 4 -> return 2 - blink1 + blink2 == 2 && stone1 + stone2 == 5 -> return 2 - stone1 + stone2 != 3 -> return 0 - blink1 + blink2 == 2 -> return 0 - } - - val leftDownValid = when { - dx != 0 && x - dx * leftDown in listOf(MIN_X, boardSize - 1) -> 0 - dy != 0 && y - dy * leftDown in listOf(MIN_Y, boardSize - 1) -> 0 - board[y - down][x - left] == otherStone -> 0 - else -> 1 - } - val rightUpValid = when { - dx != 0 && x + (dx * rightUp) in listOf(MIN_X, boardSize - 1) -> 0 - dy != 0 && y + (dy * rightUp) in listOf(MIN_Y, boardSize - 1) -> 0 - board[y + up][x + right] == otherStone -> 0 - else -> 1 - } - - return if (leftDownValid + rightUpValid >= 1) 1 else 0 - } - - private fun checkMoreThanFive(x: Int, y: Int, dx: Int, dy: Int): Boolean { - val (stone1, blink1) = search(x, y, -dx, -dy) - val (stone2, blink2) = search(x, y, dx, dy) - - return when { - blink1 + blink2 == 0 && stone1 + stone2 > 4 -> true - else -> false - } - } - - private fun checkBlackWin(x: Int, y: Int, dx: Int, dy: Int): Boolean { - val (stone1, blink1) = search(x, y, -dx, -dy) - val (stone2, blink2) = search(x, y, dx, dy) - - return when { - blink1 + blink2 == 0 && stone1 + stone2 == 4 -> true - else -> false - } - } - - private fun checkWhiteWin(x: Int, y: Int, dx: Int, dy: Int): Boolean { - val (stone1, blink1) = search(x, y, -dx, -dy) - val (stone2, blink2) = search(x, y, dx, dy) - - return when { - blink1 + blink2 == 0 && stone1 + stone2 >= 4 -> true - else -> false - } - } - - private fun search(x: Int, y: Int, dx: Int, dy: Int): Pair { - var toRight = x - var toTop = y - var stone = 0 - var blink = 0 - var blinkCount = 0 - while (true) { - if (dx > 0 && toRight == boardSize - 1) break - if (dx < 0 && toRight == MIN_X) break - if (dy > 0 && toTop == boardSize - 1) break - if (dy < 0 && toTop == MIN_X) break - toRight += dx - toTop += dy - when (board[toTop][toRight]) { - currentStone -> { - stone++ - blink = blinkCount - } - - otherStone -> break - EMPTY_STONE -> { - if (blink == 1) break - if (blinkCount++ == 1) break - } - - else -> throw IllegalArgumentException() - } - } - return Pair(stone, blink) - } - - companion object { - private const val EMPTY_STONE = 0 - private const val BLACK_STONE = 1 - private const val WHITE_STONE = 2 - private const val MIN_X = 0 - private const val MIN_Y = 0 - } -} diff --git a/src/test/kotlin/domain/BoardTest.kt b/src/test/kotlin/domain/BoardTest.kt deleted file mode 100644 index e99fd51e9..000000000 --- a/src/test/kotlin/domain/BoardTest.kt +++ /dev/null @@ -1,236 +0,0 @@ -package domain - -import org.assertj.core.api.Assertions -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class BoardTest { - private fun Stone(color: Color, vararg positions: Int): Stone { - return Stone(color, Position(positions[0], positions[1])) - } - - @Test - fun `보드에 바둑돌을 놓으면,보드에 바둑돌이 추가된다`() { - // given - val board = Board() - val newStone = Stone(Color.WHITE, Position(1, 2)) - // when - board.placeStone(newStone) - // then - val actual = board.stones.values - val expected = listOf(newStone) - Assertions.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `같은 위치에 바둑돌은 놓을 수 없다`() { - // given - val stone = Stone(Color.WHITE, Position(1, 2)) - val board = Board(Stones(listOf(stone))) - val samePositionStone = Stone(Color.BLACK, Position(1, 2)) - // when - val actual = board.isEmpty(samePositionStone) - // then - Assertions.assertThat(actual).isFalse - } - - @Test - fun `다른 위치에 바둑돌을 놓을 수 있다`() { - // given - val stone = Stone(Color.WHITE, Position(1, 2)) - val board = Board() - val differentPositionStone = Stone(Color.WHITE, Position(2, 3)) - // when - val actual = board.isEmpty(differentPositionStone) - // then - Assertions.assertThat(actual).isTrue - } - - @Test - fun `보드에 바둑돌이 없다면, 마지막 바둑돌의 위치를 가져오려고 할 때 null을 반환한다`() { - // given - val board = Board() - // when - val actual = board.getLastPosition() - val expected = null - // then - Assertions.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `마지막으로 둔 바둑돌의 위치를 알 수 있다`() { - // given - val stone = Stone(Color.WHITE, Position(1, 2)) - val board = Board(Stones(listOf(stone))) - // when - val actual = board.getLastPosition() - val expected = Position(1, 2) - // then - Assertions.assertThat(actual).isEqualTo(expected) - } - - // 1 - // 2 ◎ ● - // 3 ◎ ● - // 4 ◎ ● ● - // 5 ◎ - // 6 ? - // 1 2 3 4 5 6 - @Test - fun `흑의 오목이 완성되면 흑의의 승리이다`() { - // given - val board = generateBlackWinOmokBoard() - val stone = Stone(Color.BLACK, 1, 6) - // when - val actual = board.isBlackWin(stone) - // then - assertThat(actual).isEqualTo(true) - } - - private fun generateBlackWinOmokBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 1, 2)) - placeStone(Stone(getCurrentTurn(), 2, 2)) - placeStone(Stone(getCurrentTurn(), 1, 3)) - placeStone(Stone(getCurrentTurn(), 2, 3)) - placeStone(Stone(getCurrentTurn(), 1, 4)) - placeStone(Stone(getCurrentTurn(), 2, 4)) - placeStone(Stone(getCurrentTurn(), 1, 5)) - placeStone(Stone(getCurrentTurn(), 4, 8)) - } - return board - } - - // 1 - // 2 ● ◎ - // 3 ● ◎ - // 4 ● ◎ ◎ ◎ - // 5 ● - // 6 ? - // 1 2 3 4 5 6 - @Test - fun `백의 오목이 완성되면 백의 승리이다`() { - // given - val board = generateWhiteWinOmokBoard() - val stone = Stone(Color.WHITE, 1, 6) - // when - val actual = board.isWhiteWin(stone) - // then - assertThat(actual).isEqualTo(true) - } - - private fun generateWhiteWinOmokBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 2, 2)) - placeStone(Stone(getCurrentTurn(), 1, 2)) - placeStone(Stone(getCurrentTurn(), 2, 3)) - placeStone(Stone(getCurrentTurn(), 1, 3)) - placeStone(Stone(getCurrentTurn(), 2, 4)) - placeStone(Stone(getCurrentTurn(), 1, 4)) - placeStone(Stone(getCurrentTurn(), 2, 5)) - placeStone(Stone(getCurrentTurn(), 1, 5)) - placeStone(Stone(getCurrentTurn(), 2, 10)) - } - return board - } - - // 1 - // 2 ● ◎ - // 3 ● ◎ - // 4 ● ? ◎ ◎ ● - // 5 - // 6 - // 1 2 3 4 5 6 - @Test - fun `흑이 33이면 백의 승리이다(흑의 패배이다)`() { - // given - val board = generateThreeThreeBoard() - val stone = Stone(Color.BLACK, 2, 4) - // when - val actual = board.isWhiteWin(stone) - // then - assertThat(actual).isEqualTo(true) - } - - private fun generateThreeThreeBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 2, 2)) - placeStone(Stone(getCurrentTurn(), 4, 7)) - placeStone(Stone(getCurrentTurn(), 2, 3)) - placeStone(Stone(getCurrentTurn(), 4, 8)) - placeStone(Stone(getCurrentTurn(), 3, 4)) - placeStone(Stone(getCurrentTurn(), 5, 13)) - placeStone(Stone(getCurrentTurn(), 4, 4)) - placeStone(Stone(getCurrentTurn(), 6, 10)) - } - return board - } - - // 3 ? ◎ ◎ ◎ ● - // 4 ◎ - // 5 - // 6 ◎ - // 7 ● ● ● ● ◎ ● - // 3 4 5 6 7 8 9 10 - @Test - fun `흑이 44이면 백의 승리이다(흑의 패배이다)`() { - // given - val board = generateFourFourBoard() - val stone = Stone(Color.BLACK, 3, 3) - // when - val actual = board.isWhiteWin(stone) - // then - assertThat(actual).isEqualTo(true) - } - - private fun generateFourFourBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 4, 3)) - placeStone(Stone(getCurrentTurn(), 3, 7)) - placeStone(Stone(getCurrentTurn(), 6, 3)) - placeStone(Stone(getCurrentTurn(), 4, 7)) - placeStone(Stone(getCurrentTurn(), 7, 3)) - placeStone(Stone(getCurrentTurn(), 5, 7)) - placeStone(Stone(getCurrentTurn(), 4, 4)) - placeStone(Stone(getCurrentTurn(), 6, 7)) - placeStone(Stone(getCurrentTurn(), 6, 6)) - placeStone(Stone(getCurrentTurn(), 8, 7)) - placeStone(Stone(getCurrentTurn(), 7, 7)) - placeStone(Stone(getCurrentTurn(), 8, 3)) - } - return board - } - - // 3 ◎ ◎ ? ◎ ◎ ◎ - // 4 - // 5 - // 6 - // 7 ● ● ● ● ● - // 3 4 5 6 7 8 9 10 - @Test - fun `흑이 장목이면 백의 승리이다(흑의 패배이다)`() { - // given - val board = generateMoreFiveBoard() - val stone = Stone(Color.BLACK, 5, 3) - // when - val actual = board.isWhiteWin(stone) - // then - assertThat(actual).isEqualTo(true) - } - - private fun generateMoreFiveBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 3, 3)) - placeStone(Stone(getCurrentTurn(), 3, 7)) - placeStone(Stone(getCurrentTurn(), 4, 3)) - placeStone(Stone(getCurrentTurn(), 4, 7)) - placeStone(Stone(getCurrentTurn(), 6, 3)) - placeStone(Stone(getCurrentTurn(), 5, 7)) - placeStone(Stone(getCurrentTurn(), 7, 3)) - placeStone(Stone(getCurrentTurn(), 6, 7)) - placeStone(Stone(getCurrentTurn(), 8, 3)) - placeStone(Stone(getCurrentTurn(), 8, 7)) - } - return board - } -} diff --git a/src/test/kotlin/domain/OmokGameTest.kt b/src/test/kotlin/domain/OmokGameTest.kt deleted file mode 100644 index ee76adc3e..000000000 --- a/src/test/kotlin/domain/OmokGameTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -package domain - -import org.assertj.core.api.Assertions -import org.junit.jupiter.api.Test - -internal class OmokGameTest { - private fun Stone(color: Color, vararg positions: Int): Stone { - return Stone(color, Position(positions[0], positions[1])) - } - - @Test - fun `흑이 이겼을 경우 검정색을 반환한다`() { - // given - val board = generateBlackWinOmokBoard() - val omokGame = OmokGame(board) - // when - val actual = omokGame.getWinnerColor({}, { Position(1, 6) }) - val expected = Color.BLACK - // then - Assertions.assertThat(actual).isEqualTo(expected) - } - - private fun generateBlackWinOmokBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 1, 2)) - placeStone(Stone(getCurrentTurn(), 2, 2)) - placeStone(Stone(getCurrentTurn(), 1, 3)) - placeStone(Stone(getCurrentTurn(), 2, 3)) - placeStone(Stone(getCurrentTurn(), 1, 4)) - placeStone(Stone(getCurrentTurn(), 2, 4)) - placeStone(Stone(getCurrentTurn(), 1, 5)) - placeStone(Stone(getCurrentTurn(), 4, 8)) - } - return board - } - - @Test - fun `백이 이겼을 경우 하얀색을 반환한다`() { - // given - val board = generateWhiteWinOmokBoard() - val omokGame = OmokGame(board) - // when - val actual = omokGame.getWinnerColor({}, { Position(1, 6) }) - val expected = Color.WHITE - // then - Assertions.assertThat(actual).isEqualTo(expected) - } - - private fun generateWhiteWinOmokBoard(): Board { - val board = Board().apply { - placeStone(Stone(getCurrentTurn(), 2, 2)) - placeStone(Stone(getCurrentTurn(), 1, 2)) - placeStone(Stone(getCurrentTurn(), 2, 3)) - placeStone(Stone(getCurrentTurn(), 1, 3)) - placeStone(Stone(getCurrentTurn(), 2, 4)) - placeStone(Stone(getCurrentTurn(), 1, 4)) - placeStone(Stone(getCurrentTurn(), 2, 5)) - placeStone(Stone(getCurrentTurn(), 1, 5)) - placeStone(Stone(getCurrentTurn(), 2, 10)) - } - return board - } - - @Test - fun `처음 수와 두번째 수에 아무도 이기지못하고 3번째수에 백이 이겼을 때 하얀색을 반환한다`() { - // given - val board = generateWhiteWinOmokBoard() - val omokGame = OmokGame(board) - // when - val actual = omokGame.getWinnerColor({}, { getPosition() }) - val expected = Color.WHITE - // then - Assertions.assertThat(actual).isEqualTo(expected) - } - - private var count = 0 - private fun getPosition(): Position { - count++ - if (count == 1) return Position(5, 10) - if (count == 2) return Position(5, 11) - return Position(1, 6) - } -} diff --git a/src/test/kotlin/domain/StoneTest.kt b/src/test/kotlin/domain/StoneTest.kt deleted file mode 100644 index f0b172295..000000000 --- a/src/test/kotlin/domain/StoneTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource - -class StoneTest { - @Test - fun `바둑돌은 색상과 위치를 갖는다`() { - // given - val position = Position(1, 1) - val color = Color.BLACK - // when - val actual = Stone(color, position) - // then - assertThat(actual).isInstanceOf(Stone::class.java) - } - - @CsvSource(value = ["BLACK,true", "WHITE,false"]) - @ParameterizedTest - fun `바둑돌이 검정색인지 확인한다`(color: Color, expected: Boolean) { - // given - val stone = Stone(color, Position(1, 1)) - // when - val actual = stone.isBlack() - // then - assertThat(actual).isEqualTo(expected) - } - - @CsvSource(value = ["BLACK,false", "WHITE,true"]) - @ParameterizedTest - fun `바둑돌이 흰색인지 확인한다`(color: Color, expected: Boolean) { - // given - val stone = Stone(color, Position(1, 1)) - // when - val actual = stone.isWhite() - // then - assertThat(actual).isEqualTo(expected) - } -} diff --git a/src/test/kotlin/domain/StonesTest.kt b/src/test/kotlin/domain/StonesTest.kt deleted file mode 100644 index d48848b66..000000000 --- a/src/test/kotlin/domain/StonesTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class StonesTest { - @Test - fun `바둑돌을 추가 할 수 있다`() { - // given - val stones = makeStones() - val newStone = Stone(Color.BLACK, Position(5, 6)) - // when - stones.addStone(newStone) - val actual = stones - val expected = stones.values + newStone - // then - assertThat(actual).isEqualTo(expected) - } - - @Test - fun `같은 위치의 바둑돌을 포함하고 있으면 True 이다`() { - // given - val stones = makeStones() - val samePosition = Position(1, 2) - // when - val actual = stones.isContainSamePositionStone(samePosition) - // then - assertThat(actual).isTrue - } - - @Test - fun `같은 위치의 바둑돌을 포함하고 있지 않다면 false 이다`() { - // given - val stones = makeStones() - val differentPosition = Position(7, 9) - // when - val actual = stones.isContainSamePositionStone(differentPosition) - // then - assertThat(actual).isFalse - } - - private fun makeStones(): Stones { - val blackStone = Stone(Color.BLACK, Position(1, 2)) - val whiteStone = Stone(Color.WHITE, Position(2, 3)) - return Stones(listOf(blackStone, whiteStone)) - } -}