commit 6efa7f9f697090ca47805cfbbf8de5916e703a1a Author: ocean <503259349@qq.com> Date: Fri Dec 5 17:45:05 2025 +0800 first commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..60ad445 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(cp:*)", + "Bash(rm:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..0922920 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Minesweeper Go \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..14be325 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /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 0000000..a3a5650 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,124 @@ +import groovy.xml.XmlParser +import java.text.SimpleDateFormat +import java.util.Date + + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") +} +// 生成时间戳(格式 MMddHHmm) +val timestamp: String = SimpleDateFormat("MMddHHmm").format(Date()) + +fun getAppNameFromStringsFile(): String { + val stringsFile = file("src/main/res/values/strings.xml") + if (!stringsFile.exists()) { + return "UnknownApp" + } + + val xmlParser = XmlParser() + val rootNode = xmlParser.parse(stringsFile) + + return rootNode.children() + .filterIsInstance() + .find { it.attribute("name") == "app_name" } + ?.text() ?: "UnknownApp" +} +android { + namespace = "com.gogame.minesweeper" + compileSdk = 36 + + defaultConfig { + applicationId = "com.gogame.minesweeper" + minSdk = 24 + targetSdk = 36 + versionCode = 3 + versionName = "1.0.3" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + setProperty("archivesBaseName", "${getAppNameFromStringsFile()}-$versionName($versionCode)-${timestamp}") + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(files("libs/UpLoadLibrary_12_03_15_13-release.aar")) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.gson) + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + // Import the Firebase BoM + implementation(platform("com.google.firebase:firebase-bom:34.6.0")) + implementation("com.google.firebase:firebase-crashlytics-ndk") + implementation("com.google.firebase:firebase-analytics") + // google ads + implementation ("com.google.android.gms:play-services-ads-identifier:18.0.1") + // okhttp + implementation ("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + + //TU (Necessary) + api("com.thinkup.sdk:core-tpn:6.5.16") + api("com.thinkup.sdk:nativead-tpn:6.5.16") + api("com.thinkup.sdk:banner-tpn:6.5.16") + api("com.thinkup.sdk:interstitial-tpn:6.5.16") + api("com.thinkup.sdk:rewardedvideo-tpn:6.5.16") + api("com.thinkup.sdk:splash-tpn:6.5.16") +//Androidx (Necessary) + api("androidx.appcompat:appcompat:1.6.1") + api("androidx.browser:browser:1.4.0") +//Vungle + api("com.thinkup.sdk:adapter-tpn-vungle:6.5.16") + api("com.vungle:vungle-ads:7.5.0") + api("com.google.android.gms:play-services-basement:18.1.0") + api("com.google.android.gms:play-services-ads-identifier:18.0.1") +//UnityAds + api("com.thinkup.sdk:adapter-tpn-unityads:6.5.16") + api("com.unity3d.ads:unity-ads:4.14.0") +//Ironsource + api("com.thinkup.sdk:adapter-tpn-ironsource:6.5.16") + api("com.ironsource.sdk:mediationsdk:8.7.0") + api("com.google.android.gms:play-services-appset:16.0.2") + api("com.google.android.gms:play-services-ads-identifier:18.0.1") + api("com.google.android.gms:play-services-basement:18.1.0") +//Bigo + api("com.thinkup.sdk:adapter-tpn-bigo:6.5.16.1") + api("com.bigossp:bigo-ads:5.5.1") +//Mintegral + api("com.thinkup.sdk:adapter-tpn-mintegral:6.5.16.1") + api("com.mbridge.msdk.oversea:mbridge_android_sdk:16.9.91") + api("androidx.recyclerview:recyclerview:1.1.0") +//Pangle + api("com.thinkup.sdk:adapter-tpn-pangle:6.5.16.2") + api("com.pangle.global:pag-sdk:7.6.0.2") + api("com.google.android.gms:play-services-ads-identifier:18.0.1") +} \ No newline at end of file diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..8baf96e --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "500252155158", + "project_id": "minesweeper-go-1e611", + "storage_bucket": "minesweeper-go-1e611.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:500252155158:android:9296fdaa061e12de05c437", + "android_client_info": { + "package_name": "com.gogame.minesweeper" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDdKXNF1RXKY_9WbrQYKZbWFs4tMr8ajKI" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/libs/UpLoadLibrary_12_03_15_13-release.aar b/app/libs/UpLoadLibrary_12_03_15_13-release.aar new file mode 100644 index 0000000..a3e30aa Binary files /dev/null and b/app/libs/UpLoadLibrary_12_03_15_13-release.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..9711844 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,93 @@ +#start topon 聚合混淆 +# Vungle +-dontwarn com.vungle.ads.** +-keepclassmembers class com.vungle.ads.** { + *; +} +-keep class com.vungle.ads.** + + + +# Google +-keep class com.google.android.gms.** { *; } +-dontwarn com.google.android.gms.** + + + + +# START OkHttp + Okio +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + + +# A resource is loaded with a relative path so the package of this class must be preserved. +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + + +# END OkHttp + Okio + + +# START Protobuf +-dontwarn com.google.protobuf.** +-keepclassmembers class com.google.protobuf.** { + *; +} +-keep class * extends com.google.protobuf.GeneratedMessageLite { *; } + + +# END Protobuf +-keepclassmembers class com.ironsource.sdk.controller.IronSourceWebView$JSInterface { + public *; +} +-keepclassmembers class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} +-keep public class com.google.android.gms.ads.** { + public *; +} +-keep class com.ironsource.adapters.** { *; +} +-dontwarn com.ironsource.mediationsdk.** +-dontwarn com.ironsource.adapters.** +-keepattributes JavascriptInterface +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} +-keepattributes Signature +-keepattributes *Annotation* +-keep class com.mbridge.** {*; } +-keep interface com.mbridge.** {*; } +-dontwarn com.mbridge.** +-keepclassmembers class **.R$* { public static final int mbridge*; } + +-keep public class com.mbridge.* extends androidx.** { *; } +-keep public class androidx.viewpager.widget.PagerAdapter{*;} +-keep public class androidx.viewpager.widget.ViewPager.OnPageChangeListener{*;} +-keep interface androidx.annotation.IntDef{*;} +-keep interface androidx.annotation.Nullable{*;} +-keep interface androidx.annotation.CheckResult{*;} +-keep interface androidx.annotation.NonNull{*;} +-keep public class androidx.fragment.app.Fragment{*;} +-keep public class androidx.core.content.FileProvider{*;} +-keep public class androidx.core.app.NotificationCompat{*;} +-keep public class androidx.appcompat.widget.AppCompatImageView {*;} +-keep public class androidx.recyclerview.*{*;} +-keep class com.mbridge.msdk.foundation.tools.FastKV{*;} +-keep class com.mbridge.msdk.foundation.tools.FastKV$Builder{*;} +-keep class com.bytedance.sdk.** { *; } +#end topon 聚合混淆 diff --git a/app/release/Minesweeper Go-1.0.3(3)-12041449-release.apk b/app/release/Minesweeper Go-1.0.3(3)-12041449-release.apk new file mode 100644 index 0000000..2a4bc99 Binary files /dev/null and b/app/release/Minesweeper Go-1.0.3(3)-12041449-release.apk differ diff --git a/app/release/Minesweeper Go-1.0.3(3)-12041501-release.aab b/app/release/Minesweeper Go-1.0.3(3)-12041501-release.aab new file mode 100644 index 0000000..a1cc058 Binary files /dev/null and b/app/release/Minesweeper Go-1.0.3(3)-12041501-release.aab differ diff --git a/app/release/baselineProfiles/0/Minesweeper Go-1.0.3(3)-12041449-release.dm b/app/release/baselineProfiles/0/Minesweeper Go-1.0.3(3)-12041449-release.dm new file mode 100644 index 0000000..70e29ee Binary files /dev/null and b/app/release/baselineProfiles/0/Minesweeper Go-1.0.3(3)-12041449-release.dm differ diff --git a/app/release/baselineProfiles/1/Minesweeper Go-1.0.3(3)-12041449-release.dm b/app/release/baselineProfiles/1/Minesweeper Go-1.0.3(3)-12041449-release.dm new file mode 100644 index 0000000..8787bff Binary files /dev/null and b/app/release/baselineProfiles/1/Minesweeper Go-1.0.3(3)-12041449-release.dm differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..91e0f50 --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,37 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.gogame.minesweeper", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 3, + "versionName": "1.0.3", + "outputFile": "Minesweeper Go-1.0.3(3)-12041449-release.apk" + } + ], + "elementType": "File", + "baselineProfiles": [ + { + "minApi": 28, + "maxApi": 30, + "baselineProfiles": [ + "baselineProfiles/1/Minesweeper Go-1.0.3(3)-12041449-release.dm" + ] + }, + { + "minApi": 31, + "maxApi": 2147483647, + "baselineProfiles": [ + "baselineProfiles/0/Minesweeper Go-1.0.3(3)-12041449-release.dm" + ] + } + ], + "minSdkVersionForDexing": 24 +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/gogame/minesweeper/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/gogame/minesweeper/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..de99528 --- /dev/null +++ b/app/src/androidTest/java/com/gogame/minesweeper/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.gogame.minesweeper + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.gogame.minesweeper", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..47de22e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/app_logo-playstore.png b/app/src/main/app_logo-playstore.png new file mode 100644 index 0000000..60ce9e7 Binary files /dev/null and b/app/src/main/app_logo-playstore.png differ diff --git a/app/src/main/java/com/gogame/minesweeper/App.kt b/app/src/main/java/com/gogame/minesweeper/App.kt new file mode 100644 index 0000000..5adee3c --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/App.kt @@ -0,0 +1,28 @@ +package com.gogame.minesweeper + +import android.app.Application +import android.content.Context +import com.thinkup.core.api.TUSDK +import com.up.uploadlibrary.UpLoadManager + +class App : Application() { + + companion object { + private lateinit var instance: App + + fun getInstance(): App = instance + + fun getContext(): Context = instance.applicationContext + } + + override fun onCreate() { + super.onCreate() + // 上传 + UpLoadManager.init(context = this, tag = "PRApp_minesweeper_task") { _, _ -> } + initAd() + } + + private fun initAd() { + TUSDK.init(this,"h692ffae5586bf","a764e3497a06ccd54a03aef53f022ef74") + } +} diff --git a/app/src/main/java/com/gogame/minesweeper/BaseActivity.kt b/app/src/main/java/com/gogame/minesweeper/BaseActivity.kt new file mode 100644 index 0000000..6a025d1 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/BaseActivity.kt @@ -0,0 +1,36 @@ +package com.gogame.minesweeper + +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity + +open class BaseActivity: AppCompatActivity() { + protected var backPressedCallback: OnBackPressedCallback? = null + /** 子类是否需要拦截返回 */ + protected open fun shouldInterceptBackPress(): Boolean = false + + /** 子类定义拦截后的操作(例如弹窗) */ + protected open fun onInterceptBackPressed() {} + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setupBackPressedCallback()//初始化back事件 + } + private fun setupBackPressedCallback() { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (shouldInterceptBackPress()) { + // 由子类处理拦截动作 + onInterceptBackPressed() + } else { + // 不拦截:关闭自己 + isEnabled = false + onBackPressedDispatcher.onBackPressed() + } + } + } + + onBackPressedDispatcher.addCallback(this, backPressedCallback!!) + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/DifficultyLevel.kt b/app/src/main/java/com/gogame/minesweeper/DifficultyLevel.kt new file mode 100644 index 0000000..e0ee87d --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/DifficultyLevel.kt @@ -0,0 +1,8 @@ +package com.gogame.minesweeper + +enum class DifficultyLevel(val xLength: Int, val yLength: Int, val mineNum: Int) { + BEGINNER(9, 9, 10), // 新手难度 + INTERMEDIATE(16, 16, 40), // 中等难度 + EXPERT(24, 24, 99), // 专家难度 + MASTER(50, 50, 450) // 大师难度 +} diff --git a/app/src/main/java/com/gogame/minesweeper/MainActivity.kt b/app/src/main/java/com/gogame/minesweeper/MainActivity.kt new file mode 100644 index 0000000..c0eb1ff --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/MainActivity.kt @@ -0,0 +1,114 @@ +package com.gogame.minesweeper + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.gogame.minesweeper.bean.GameState +import com.gogame.minesweeper.databinding.ActivityMainBinding +import com.gogame.minesweeper.swipe.SwipeActivity +import com.google.gson.Gson + +class MainActivity : BaseActivity() { + + private lateinit var binding: ActivityMainBinding + + private lateinit var difficultyLevels: Array + private var currentDifficultyIndex = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + immersionBar() + initView() + } + + private fun immersionBar() { + //自适应沉浸式 + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.isAppearanceLightStatusBars = true//状态栏文字颜色 + } + + private fun initView() { + // 设置难度级别 + difficultyLevels = DifficultyLevel.entries.toTypedArray() // 获取所有的难度级别 + + // 显示初始难度 + binding.tvDifficulty.text = difficultyLevels[currentDifficultyIndex].name + + binding.btnPrevious.setOnClickListener { + if (currentDifficultyIndex > 0) { + currentDifficultyIndex-- + binding.tvDifficulty.text = difficultyLevels[currentDifficultyIndex].name + } + updateContinueState() + } + + binding.btnNext.setOnClickListener { + if (currentDifficultyIndex < difficultyLevels.size - 1) { + currentDifficultyIndex++ + binding.tvDifficulty.text = difficultyLevels[currentDifficultyIndex].name + } + updateContinueState() + } + + binding.newGameBtn.setOnClickListener { + startGameWithDifficulty() + } + binding.continueBtn.setOnClickListener { + continuePreviousGame() + } + } + + override fun onResume() { + super.onResume() + updateContinueState() + } + + private fun startGameWithDifficulty() { + val intent = Intent(this, SwipeActivity::class.java) + // 传递当前难度给 SwipeActivity + intent.putExtra( + "difficulty", + difficultyLevels[currentDifficultyIndex] + ) // 直接传递 DifficultyLevel + intent.putExtra("isNewGame", true) + startActivity(intent) + } + + private fun continuePreviousGame() { + val intent = Intent(this, SwipeActivity::class.java) + // 传递当前难度给 SwipeActivity + intent.putExtra( + "difficulty", + difficultyLevels[currentDifficultyIndex] + ) // 直接传递 DifficultyLevel + intent.putExtra("isNewGame", false) + startActivity(intent) + } + + private fun updateContinueState() { + val sharedPreferences = getSharedPreferences("minesweeper_prefs", Context.MODE_PRIVATE) + val gameStateJson = sharedPreferences.getString("game_state", null) + if (gameStateJson.isNullOrEmpty()) { + binding.continueBtn.visibility = View.GONE + return + } + val gameState: GameState = Gson().fromJson(gameStateJson, GameState::class.java) + if (gameState.difficulty == difficultyLevels[currentDifficultyIndex]) { + binding.continueBtn.visibility = View.VISIBLE + } else { + binding.continueBtn.visibility = View.GONE + } + } +} diff --git a/app/src/main/java/com/gogame/minesweeper/SplashActivity.kt b/app/src/main/java/com/gogame/minesweeper/SplashActivity.kt new file mode 100644 index 0000000..96873eb --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/SplashActivity.kt @@ -0,0 +1,107 @@ +package com.gogame.minesweeper + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.gogame.minesweeper.ad.AdShowFailed +import com.gogame.minesweeper.ad.AdsInsUtil +import com.gogame.minesweeper.ad.LoadListener +import com.gogame.minesweeper.ad.ShowListener +import com.gogame.minesweeper.databinding.ActivitySplashBinding +import com.thinkup.core.api.TUAdInfo +import kotlin.random.Random + +class SplashActivity : BaseActivity() { + + companion object { + private const val AD_TIMEOUT = 15000L // 广告加载超时时间 15 秒 + } + + private lateinit var binding: ActivitySplashBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySplashBinding.inflate(layoutInflater) + enableEdgeToEdge() + setContentView(binding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.isAppearanceLightStatusBars = true//状态栏文字颜色 + loadSplashAd() + } + + + private val adHandler = Handler(Looper.getMainLooper()) + private val adTimeoutRunnable = Runnable { + navigateToNext() // 超时直接跳转 + } + + private fun loadSplashAd() { + adHandler.postDelayed(adTimeoutRunnable, AD_TIMEOUT) + + // 生成一个随机索引 + val randomIndex = Random.nextInt(AdsInsUtil.Placement.adPlaceAllList.size) + // 获取随机的广告位 + val randomAdPlace = AdsInsUtil.Placement.adPlaceAllList[randomIndex] + AdsInsUtil.loadAd( + act = this, + adID =randomAdPlace, + loadListener = object : LoadListener { + override fun loaded(ad: TUAdInfo) { + adHandler.removeCallbacks(adTimeoutRunnable) + showSplashAd(randomAdPlace) + } + + override fun loadFailed(error: String) { + adHandler.removeCallbacks(adTimeoutRunnable) + navigateToNext() // 加载失败直接跳转 + } + }) + } + + private fun showSplashAd(randomAdPlace: String) { + AdsInsUtil.showAd( + act = this, + adID = randomAdPlace, + listener = object : ShowListener { + override fun onAdShown(ad: TUAdInfo?) { + } + + override fun onAdShowFailed(error: AdShowFailed?) { + navigateToNext() // 展示失败也直接跳转 + } + + override fun onAdClosed() { + navigateToNext() // 广告关闭后跳转 + } + }) + } + + private var hasNavigated = false + + //保证只跳转一次 + private fun navigateToNext() { + if (hasNavigated) return + hasNavigated = true + startActivity(Intent(this, MainActivity::class.java)) + finish() + } + + override fun shouldInterceptBackPress(): Boolean = true + override fun onInterceptBackPressed() { + + } + + override fun onDestroy() { + super.onDestroy() + adHandler.removeCallbacks(adTimeoutRunnable) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/AdInstLoad.kt b/app/src/main/java/com/gogame/minesweeper/ad/AdInstLoad.kt new file mode 100644 index 0000000..5d15209 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/AdInstLoad.kt @@ -0,0 +1,68 @@ +package com.gogame.minesweeper.ad + +import android.app.Activity +import android.util.Log +import com.thinkup.core.api.AdError +import com.thinkup.core.api.TUAdInfo +import com.thinkup.interstitial.api.TUInterstitial +import com.thinkup.interstitial.api.TUInterstitialListener + +class AdInstLoad { + private var mPlace: String + private var adLoadListener: LoadListener? = null + private var activity: Activity? = null + + constructor(activity: Activity, place: String, listener: LoadListener?) { + this.mPlace = place + this.adLoadListener = listener + this.activity = activity + init() + } + + constructor(place: String, listener: LoadListener?) { + this.mPlace = place + this.adLoadListener = listener + init() + } + + private fun init() { + val mInterstitialAd = TUInterstitial(activity,mPlace) + mInterstitialAd.setAdListener(object : TUInterstitialListener{ + override fun onInterstitialAdLoaded() { + Log.d("ocean", "load ad loaded") + val info = mInterstitialAd.checkAdStatus().tuTopAdInfo + if (info != null) { + InstAdCacheManager.instance.setAdCache(mPlace, mInterstitialAd) + adLoadListener?.loaded(info) + } else { + adLoadListener?.loadFailed("AdInfo null") + } + } + + override fun onInterstitialAdLoadFail(adError: AdError?) { + adLoadListener?.loadFailed("code:${adError?.code}") + Log.d("ocean", "load ad onError->${adError.toString()}") + } + + override fun onInterstitialAdClicked(p0: TUAdInfo?) { + } + + override fun onInterstitialAdShow(p0: TUAdInfo?) { + } + + override fun onInterstitialAdClose(p0: TUAdInfo?) { + } + + override fun onInterstitialAdVideoStart(p0: TUAdInfo?) { + } + + override fun onInterstitialAdVideoEnd(p0: TUAdInfo?) { + } + + override fun onInterstitialAdVideoError(p0: AdError?) { + } + + }) + mInterstitialAd.load() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/AdInstShower.kt b/app/src/main/java/com/gogame/minesweeper/ad/AdInstShower.kt new file mode 100644 index 0000000..27e3b3e --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/AdInstShower.kt @@ -0,0 +1,71 @@ +package com.gogame.minesweeper.ad + +import android.app.Activity +import android.util.Log +import com.thinkup.core.api.AdError +import com.thinkup.core.api.TUAdInfo +import com.thinkup.interstitial.api.TUInterstitialListener + +class AdInstShower { + private var mPlace: String + private var showListener: ShowListener? = null + private var activity: Activity? = null + + constructor(activity: Activity, place: String, showListener: ShowListener?) { + this.mPlace = place + this.showListener = showListener + this.activity = activity + init() + } + + constructor(place: String, showListener: ShowListener?) { + this.mPlace = place + this.showListener = showListener + init() + } + + private fun init() { + val atInterstitial = InstAdCacheManager.instance.getAdCache(mPlace) + atInterstitial?.setAdListener(object : TUInterstitialListener { + override fun onInterstitialAdLoaded() { + + } + + override fun onInterstitialAdLoadFail(p0: AdError?) { + + } + + override fun onInterstitialAdClicked(p0: TUAdInfo?) { + showListener?.onAdClicked() + Log.d("ocean", "广告点击回调") + } + + override fun onInterstitialAdShow(p0: TUAdInfo?) { + showListener?.onAdShown(p0) + Log.d("ocean", "广告展示回调") + } + + override fun onInterstitialAdClose(p0: TUAdInfo?) { + showListener?.onAdClosed() + Log.d("ocean", "广告关闭回调") + } + + override fun onInterstitialAdVideoStart(p0: TUAdInfo?) { + Log.d("ocean", "视频广告开始播放回调") + } + + override fun onInterstitialAdVideoEnd(p0: TUAdInfo?) { + Log.d("ocean", "视频广告播放结束回调") + } + + override fun onInterstitialAdVideoError(adError: AdError?) { + Log.d("ocean", "视频广告播放失败回调->${adError.toString()}") + showListener?.onAdShowFailed(AdShowFailed(adError.toString())) + } + + }) + if (atInterstitial?.isAdReady == true) { + atInterstitial.show(activity) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/AdShowFailed.kt b/app/src/main/java/com/gogame/minesweeper/ad/AdShowFailed.kt new file mode 100644 index 0000000..3060321 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/AdShowFailed.kt @@ -0,0 +1,5 @@ +package com.gogame.minesweeper.ad + +data class AdShowFailed( + val msg: String = "", +) \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/AdsInsUtil.kt b/app/src/main/java/com/gogame/minesweeper/ad/AdsInsUtil.kt new file mode 100644 index 0000000..8ef7996 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/AdsInsUtil.kt @@ -0,0 +1,34 @@ +package com.gogame.minesweeper.ad + +import android.app.Activity + +object AdsInsUtil { + + object Placement { + const val TOP_ON_AD_ONE = "n692ffb09edbb6" + const val TOP_ON_AD_TOW = "n692ffb09bc89d" + const val TOP_ON_AD_THREE = "n692ffb098d0d5" + + val adPlaceAllList = listOf( + TOP_ON_AD_ONE, + TOP_ON_AD_TOW, + TOP_ON_AD_THREE + ) + } + + fun loadAd( + act: Activity, + adID: String, + loadListener: LoadListener? + ): AdInstLoad { + return AdInstLoad(act, adID, loadListener) + } + + fun showAd( + act: Activity, + adID: String, + listener: ShowListener? + ): AdInstShower { + return AdInstShower(act, adID, listener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/InstAdCacheManager.kt b/app/src/main/java/com/gogame/minesweeper/ad/InstAdCacheManager.kt new file mode 100644 index 0000000..144a4e6 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/InstAdCacheManager.kt @@ -0,0 +1,36 @@ +package com.gogame.minesweeper.ad + +import com.thinkup.interstitial.api.TUInterstitial + + +class InstAdCacheManager { + private val mAdCacheDict: MutableMap = mutableMapOf() + + companion object { + val instance: InstAdCacheManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { + InstAdCacheManager() + } + } + + fun setAdCache(place: String, adCache: TUInterstitial) { + mAdCacheDict[place] = adCache + } + + fun getAdCache(place: String): TUInterstitial? { + return mAdCacheDict[place] + } + + fun getLoadedInstCount(): Int { + var count = 0 + try { + mAdCacheDict.forEach { (key, value) -> + if (value.isAdReady) { + count += 1 + } + } + } catch (_: Exception) { + + } + return count + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/LoadListener.kt b/app/src/main/java/com/gogame/minesweeper/ad/LoadListener.kt new file mode 100644 index 0000000..2c69cf1 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/LoadListener.kt @@ -0,0 +1,8 @@ +package com.gogame.minesweeper.ad + +import com.thinkup.core.api.TUAdInfo + +interface LoadListener { + fun loadFailed(error: String) {} + fun loaded(ad: TUAdInfo) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/ad/ShowListener.kt b/app/src/main/java/com/gogame/minesweeper/ad/ShowListener.kt new file mode 100644 index 0000000..d8171c6 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/ad/ShowListener.kt @@ -0,0 +1,10 @@ +package com.gogame.minesweeper.ad + +import com.thinkup.core.api.TUAdInfo + +interface ShowListener { + fun onAdShown(ad: TUAdInfo?) {} + fun onAdShowFailed(error: AdShowFailed?) {} + fun onAdClosed() {} + fun onAdClicked() {} +} diff --git a/app/src/main/java/com/gogame/minesweeper/bean/CellState.kt b/app/src/main/java/com/gogame/minesweeper/bean/CellState.kt new file mode 100644 index 0000000..0d36061 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/bean/CellState.kt @@ -0,0 +1,7 @@ +package com.gogame.minesweeper.bean + +data class CellState( + var logicState: Int, // 逻辑状态 + var viewState: Int, // 视图状态 + var around: Int // 周围雷数 +) diff --git a/app/src/main/java/com/gogame/minesweeper/bean/GameState.kt b/app/src/main/java/com/gogame/minesweeper/bean/GameState.kt new file mode 100644 index 0000000..9a444b0 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/bean/GameState.kt @@ -0,0 +1,32 @@ +package com.gogame.minesweeper.bean + +import com.gogame.minesweeper.DifficultyLevel + +data class GameState( + val grid: Array>, // 游戏网格 + val remainingMines: Int, // 剩余雷数 + val difficulty: DifficultyLevel, // 难度 + val minutes : Int, + val seconds : Int, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GameState + + if (!grid.contentDeepEquals(other.grid)) return false + if (remainingMines != other.remainingMines) return false + if (difficulty != other.difficulty) return false + + return true + } + + override fun hashCode(): Int { + var result = grid.contentDeepHashCode() + result = 31 * result + remainingMines + result = 31 * result + difficulty.hashCode() + return result + } + +} diff --git a/app/src/main/java/com/gogame/minesweeper/swipe/LogicHelper.kt b/app/src/main/java/com/gogame/minesweeper/swipe/LogicHelper.kt new file mode 100644 index 0000000..a077fe5 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/swipe/LogicHelper.kt @@ -0,0 +1,66 @@ +package com.gogame.minesweeper.swipe + +import android.annotation.SuppressLint +import java.util.* + +object LogicHelper { + fun randomMines(xlength: Int, ylength: Int, x: Int, y: Int, count: Int): ArrayList { + val total = xlength * ylength - 1 + if (count >= total || count <= 0) { + throw IllegalArgumentException("Invalid mine count: $count") + } + + val random = Random() + val randoms = hashSetOf(getIndexByXY(xlength, ylength, x, y)) // 用HashSet防止重复 + + while (randoms.size < count + 1) { + val next = random.nextInt(xlength * ylength) + randoms.add(next) // HashSet 自动忽略重复的数字 + } + + randoms.remove(getIndexByXY(xlength, ylength, x, y)) // 移除点击位置 + return ArrayList(randoms) + } + + + fun getIndexByXY(xlength: Int, ylength: Int, x: Int, y: Int): Int { + return x + y * xlength + } + + fun getXYbyIndex(xlength:Int,ylength:Int,index:Int):IntArray{ + val x = index%ylength + val y = index/ylength + return intArrayOf(x,y) + } + + fun getAroundItems(xLength: Int, yLength: Int, index: Int): List { + val result = mutableListOf() + val x = index % xLength // 根据列数获取x坐标 + val y = index / xLength // 根据行数获取y坐标 + + // 检查周围的8个格子 + for (i in -1..1) { + for (j in -1..1) { + if (i == 0 && j == 0) continue // 跳过当前格子 + val newX = x + i + val newY = y + j + // 检查边界 + if (newX in 0 until xLength && newY in 0 until yLength) { + result.add(newY * xLength + newX) + } + } + } + return result + } + + + + /** + * 转换成三位数文字 + */ + @SuppressLint("DefaultLocale") + fun int2String(int: Int): String { + return String.format("%03d", int) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gogame/minesweeper/swipe/SingleView.kt b/app/src/main/java/com/gogame/minesweeper/swipe/SingleView.kt new file mode 100644 index 0000000..bd4652d --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/swipe/SingleView.kt @@ -0,0 +1,134 @@ +package com.gogame.minesweeper.swipe + +import android.annotation.SuppressLint +import android.content.Context +import android.media.MediaPlayer +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.View.OnTouchListener +import android.widget.ImageView +import com.gogame.minesweeper.R + +@SuppressLint("AppCompatCustomView") +class SingleView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : + ImageView(context, attrs, defStyleAttr), View.OnClickListener, View.OnLongClickListener{ + + + companion object { + //常量定义 + const val VIEWSTATE_DEFAULT = 0 + const val VIEWSTATE_FLAG = 1//标记这里是雷 + const val VIEWSTATE_UNSURE = 2//? 不确定 + const val VIEWSTATE_TIP = 3//显示周围雷数 需要配合around参数 + const val VIEWSTATE_THUNDER = 4//游戏结束的时候显示这里的确是雷 + const val VIEWSTATE_BOMB = 5//游戏失败时最后点到的雷 + + const val LOGICSTATE_NOT_INITED = -1 + const val LOGICSTATE_BOMB = -2 + } + + // val mPaint = Paint() + var around = 0 + //周围有几颗雷 + set(value) { + if (value > 8) + throw IllegalArgumentException("around should not be this value:$value") + else + field = value + } + var viewState = 0 + var logicState = LOGICSTATE_NOT_INITED//-1 未初始化 0~8周围地雷数 -2雷 只有这几种可能 + var enable = true//点击过以后不能再次点击 或者也有可能设置不可点击就行了? + var gridX = -1 + var gridY = -1 + lateinit var mGrid: SwipeGridLayout + + + init { + isClickable = true + setOnClickListener(this) + setOnLongClickListener(this) + setBackgroundResource(R.drawable.sel_bg_single) + scaleType = ScaleType.CENTER_CROP + + updateInsideImg() + if (attrs != null) { + //nothing to do + } + } + + constructor(context: Context?, x: Int, y: Int, mGridLayout: SwipeGridLayout) : this(context) { + this.gridX = x + this.gridY = y + this.mGrid = mGridLayout + } + + constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) + constructor(context: Context?) : this(context, null) + + fun updateInsideImg(): Unit { + setImageResource( + when (viewState) { + VIEWSTATE_DEFAULT -> R.mipmap.tile + VIEWSTATE_BOMB -> R.mipmap.mine_exploded + VIEWSTATE_THUNDER -> R.mipmap.mine + VIEWSTATE_FLAG -> R.mipmap.flag + VIEWSTATE_UNSURE -> R.mipmap.unsuer + VIEWSTATE_TIP -> when (around) { + 0 -> R.mipmap.empty + 1 -> R.mipmap.n1 + 2 -> R.mipmap.n2 + 3 -> R.mipmap.n3 + 4 -> R.mipmap.n4 + 5 -> R.mipmap.n5 + 6 -> R.mipmap.n6 + 7 -> R.mipmap.n7 + 8 -> R.mipmap.n8 + else -> throw IllegalArgumentException("illegal around") + } + + else -> throw IllegalArgumentException("illegal viewState") + } + ) + invalidate()//理论上应该都是在主线程调用的该方法 + } + + override fun onClick(v: View?) { + if (!enable) + return + if (viewState == VIEWSTATE_FLAG)//fix 插旗以后应该不再能点开了,防误触 + return + + mGrid.onChildClicked(this, gridX, gridY) + } + + override fun onLongClick(v: View?): Boolean { + if (!enable) + return true + if (!mGrid.started) + return true + + viewState = when (viewState) { + VIEWSTATE_DEFAULT -> VIEWSTATE_FLAG + VIEWSTATE_FLAG -> VIEWSTATE_UNSURE + VIEWSTATE_UNSURE -> VIEWSTATE_DEFAULT + else -> viewState + } + updateInsideImg() + + mGrid.onChildLongClicked(this, gridX, gridY, viewState) + return true + } + + fun reset() { + viewState = VIEWSTATE_DEFAULT + logicState = LOGICSTATE_NOT_INITED + around = 0 + enable = true + } + + +} + + diff --git a/app/src/main/java/com/gogame/minesweeper/swipe/SwipeActivity.kt b/app/src/main/java/com/gogame/minesweeper/swipe/SwipeActivity.kt new file mode 100644 index 0000000..2d9353f --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/swipe/SwipeActivity.kt @@ -0,0 +1,368 @@ +package com.gogame.minesweeper.swipe + +import android.app.Dialog +import android.content.Context +import android.media.MediaPlayer +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import com.gogame.minesweeper.DifficultyLevel +import com.gogame.minesweeper.R +import com.gogame.minesweeper.databinding.ActivitySwipeBinding +import com.gogame.minesweeper.BaseActivity +import com.gogame.minesweeper.ad.AdShowFailed +import com.gogame.minesweeper.ad.AdsInsUtil +import com.gogame.minesweeper.ad.LoadListener +import com.gogame.minesweeper.ad.ShowListener +import com.gogame.minesweeper.bean.GameState +import com.google.gson.Gson +import com.ironsource.fa +import com.thinkup.core.api.TUAdInfo +import kotlin.random.Random + +class SwipeActivity : BaseActivity() { + + private lateinit var binding: ActivitySwipeBinding + + private var minutes = 0 + private var seconds = 0 + private var isRunning = false + private var pausedTime = 0L // 暂停时保存的时间戳 + private var isGameStarted = false //游戏是否开始了 + private var isGameOver = false //游戏是否结束了(不管成功还是失败) + private val handler = Handler(Looper.getMainLooper()) + private lateinit var runnable: Runnable + private lateinit var winSoundPlayer: MediaPlayer + private lateinit var loseSoundPlayer: MediaPlayer + private var isNewGame = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySwipeBinding.inflate(layoutInflater) + setContentView(binding.root) + loadAd() + immersionBar() + winSoundPlayer = MediaPlayer.create(this, R.raw.win) + loseSoundPlayer = MediaPlayer.create(this, R.raw.explosion) + + var difficulty = intent.getSerializableExtra("difficulty") as? DifficultyLevel + if (difficulty == null) { + difficulty = DifficultyLevel.BEGINNER + } + isNewGame = intent.getBooleanExtra("isNewGame", true) + initTimeRun() + + resetTimer() + + val sharedPreferences = getSharedPreferences("minesweeper_prefs", Context.MODE_PRIVATE) + val gameStateJson = sharedPreferences.getString("game_state", null) + + binding.gridSwipe.postDelayed({ + binding.gridSwipe.setDifficulty(difficulty) + setNumberImage(binding.gridSwipe.mineNum, binding.hundreds, binding.tens, binding.ones) + binding.gridSwipe.fillAll() + + if (!isNewGame) { + if (!gameStateJson.isNullOrEmpty()) { + val gameState: GameState = Gson().fromJson(gameStateJson, GameState::class.java) + minutes = gameState.minutes + seconds = gameState.seconds + + startTimeRun() + } + binding.gridSwipe.loadGameState() + } + }, 120) + + binding.btnStart.setOnClickListener { + handleButtonClick() + } + binding.gridSwipe.listener = object : SwipeGridLayout.GameStateChange { + override fun onStart(rest: Int) { + isGameStarted = true + binding.stateImg.setImageResource(R.mipmap.smile) + startTimeRun() + } + + override fun onFlagsChange(flags: Int, rest: Int) { + setNumberImage(rest, binding.hundreds, binding.tens, binding.ones) + } + + override fun victory() { + isGameOver = true + playWinSound() + showResultDialog(true) + stopTimer() + + binding.gridSwipe.clearGameStateSP() + } + + override fun defeat() { + isGameOver = true + playLoseSound() + binding.stateImg.setImageResource(R.mipmap.sorrow) + showResultDialog(false) + stopTimer() + + binding.gridSwipe.clearGameStateSP() + } + + } + } + + private fun immersionBar() { + //自适应沉浸式 + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView) + windowInsetsController.isAppearanceLightStatusBars = true//状态栏文字颜色 + } + + private val successImages = arrayOf( + R.drawable.win_emoji_partying_face, // 替换为你的成功图片资源 + R.drawable.win_emoji_cowboy_hat_face, + R.drawable.win_emoji_smiling_face_with_sunglasses, + R.drawable.win_emoji_beaming_face_with_smiling_eyes + ) + + private val failureImages = arrayOf( + R.drawable.lose_emoji_bomb, // 替换为你的失败图片资源 + R.drawable.lose_emoji_confounded_face, + R.drawable.lose_emoji_anguished_face, + R.drawable.lose_emoji_exploding_head, // 替换为你的失败图片资源 + R.drawable.lose_emoji_grimacing_face, + R.drawable.lose_emoji_downcast_face_with_sweat, + R.drawable.lose_emoji_face_with_head_bandage, + R.drawable.lose_emoji_sad_but_relieved_face + ) + + private fun showResultDialog(isVictory: Boolean) { + val dialog = Dialog(this) + dialog.setContentView(R.layout.dialog_result) + + val imageView = dialog.findViewById(R.id.imageView) + val titleTextView = dialog.findViewById(R.id.textViewTitle) + val messageTextView = dialog.findViewById(R.id.textViewMessage) + + if (isVictory) { + val randomSuccessImage = successImages[Random.nextInt(successImages.size)] + imageView.setImageResource(randomSuccessImage) + titleTextView.text = getString(R.string.you_win) + messageTextView.text = getString(R.string.win_desc) + } else { + val randomFailureImage = failureImages[Random.nextInt(failureImages.size)] + imageView.setImageResource(randomFailureImage) + titleTextView.text = getString(R.string.you_lose) + messageTextView.text = getString(R.string.lose_desc) + } + + val btn = dialog.findViewById(R.id.newGameBtn) + btn.setOnClickListener { + handleButtonClick() + dialog.dismiss() + + showAd(randomAdPlace) + } + dialog.setCancelable(false) + dialog.show() + } + + + private fun playWinSound() { + if (!winSoundPlayer.isPlaying) { + winSoundPlayer.start() + } + } + + private fun playLoseSound() { + if (!loseSoundPlayer.isPlaying) { + loseSoundPlayer.start() + } + } + + private fun handleButtonClick() { + binding.stateImg.setImageResource(R.mipmap.smile) + binding.gridSwipe.reset() // 复位逻辑 + setNumberImage( + binding.gridSwipe.mineNum, binding.hundreds, binding.tens, binding.ones + ) // 设置数字图片 + stopTimer() // 停止计时器 + resetTimer() // 重置计时器 + } + + private fun startTimeRun() { + if (!isRunning) { + isRunning = true + handler.post(runnable) + } + } + + // 停止计时并移除 runnable + private fun stopTimer() { + isRunning = false + handler.removeCallbacks(runnable) // 移除 runnable,防止继续执行 + } + + private fun initTimeRun() { + // 初始化计时器 + runnable = object : Runnable { + override fun run() { + if (isRunning) { + incrementTime() + updateTimeImage() + handler.postDelayed(this, 1000) // 每秒更新一次 + } + } + } + + } + + private fun setNumberImage( + number: Int, + hundredsImageView: ImageView, + tensImageView: ImageView, + onesImageView: ImageView + ) { + // 数字限制在1-999 + if (number < 1 || number > 999) return + + // 获取百位、十位、个位 + val hundreds = number / 100 + val tens = (number % 100) / 10 + val ones = number % 10 + + // 根据数字设置图片资源ID,假设图片资源名为c0到c9 + val resIdHundreds = getResourceIdForNumber(hundreds) + val resIdTens = getResourceIdForNumber(tens) + val resIdOnes = getResourceIdForNumber(ones) + + // 设置ImageView的图片资源 + hundredsImageView.setImageResource(resIdHundreds) + tensImageView.setImageResource(resIdTens) + onesImageView.setImageResource(resIdOnes) + } + + // 获取数字对应的资源ID + private fun getResourceIdForNumber(num: Int): Int { + return when (num) { + 0 -> R.mipmap.c0 + 1 -> R.mipmap.c1 + 2 -> R.mipmap.c2 + 3 -> R.mipmap.c3 + 4 -> R.mipmap.c4 + 5 -> R.mipmap.c5 + 6 -> R.mipmap.c6 + 7 -> R.mipmap.c7 + 8 -> R.mipmap.c8 + 9 -> R.mipmap.c9 + else -> R.mipmap.c0 // 默认值,数字应为0-9 + } + } + + // 增加时间 + private fun incrementTime() { + seconds++ + if (seconds == 60) { + seconds = 0 + minutes++ + } + + binding.gridSwipe.setTime(minutes, seconds) + } + + // 更新时间的图片显示 + private fun updateTimeImage() { + val minuteTens = minutes / 10 + val minuteOnes = minutes % 10 + val secondTens = seconds / 10 + val secondOnes = seconds % 10 + + binding.minuteTens.setImageResource(getResourceIdForNumber(minuteTens)) + binding.minuteOnes.setImageResource(getResourceIdForNumber(minuteOnes)) + binding.secondTens.setImageResource(getResourceIdForNumber(secondTens)) + binding.secondOnes.setImageResource(getResourceIdForNumber(secondOnes)) + } + + override fun onResume() { + super.onResume() + //如果游戏开始了并且游戏没有结束 + if (isGameStarted && !isGameOver) { + if (pausedTime > 0L) { + // 继续计时 + handler.post(runnable) + isRunning = true + } + } + } + + override fun onPause() { + super.onPause() + + // 保存当前的计时状态 + pausedTime = System.currentTimeMillis() + isRunning = false + handler.removeCallbacks(runnable) // 停止计时 + } + + override fun onDestroy() { + super.onDestroy() + // 重置计时器 + resetTimer() + //释放资源 + binding.gridSwipe.onPlayerRelease() + winSoundPlayer.release() + loseSoundPlayer.release() + } + + // 重置计时器为00:00 + private fun resetTimer() { + minutes = 0 + seconds = 0 + updateTimeImage() + } + + private var randomAdPlace = "" + private fun loadAd() { + // 生成一个随机索引 + val randomIndex = Random.nextInt(AdsInsUtil.Placement.adPlaceAllList.size) + // 获取随机的广告位 + randomAdPlace = AdsInsUtil.Placement.adPlaceAllList[randomIndex] + AdsInsUtil.loadAd( + act = this, + adID = randomAdPlace, + loadListener = object : LoadListener { + override fun loaded(ad: TUAdInfo) { + } + + override fun loadFailed(error: String) { + } + }) + } + private fun showAd(randomAdPlace: String) { + AdsInsUtil.showAd( + act = this, + adID = randomAdPlace, + listener = object : ShowListener { + override fun onAdShown(ad: TUAdInfo?) { + } + + override fun onAdShowFailed(error: AdShowFailed?) { + loadAd() + } + + override fun onAdClosed() { + loadAd() + } + }) + } +} diff --git a/app/src/main/java/com/gogame/minesweeper/swipe/SwipeGridLayout.kt b/app/src/main/java/com/gogame/minesweeper/swipe/SwipeGridLayout.kt new file mode 100644 index 0000000..fb73868 --- /dev/null +++ b/app/src/main/java/com/gogame/minesweeper/swipe/SwipeGridLayout.kt @@ -0,0 +1,518 @@ +package com.gogame.minesweeper.swipe + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.media.MediaPlayer +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.ScaleGestureDetector +import android.widget.GridLayout +import com.gogame.minesweeper.bean.CellState +import com.gogame.minesweeper.DifficultyLevel +import com.gogame.minesweeper.bean.GameState +import com.gogame.minesweeper.R +import com.google.gson.Gson +import kotlin.math.abs + + +class SwipeGridLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + var xLength: Int = 9, // 网格的行数 + var yLength: Int = 9, // 网格的列数 + var mineNum: Int = 10 // 地雷的数量 +) : GridLayout(context, attrs, defStyleAttr) { + private val sharedPreferences = + context.getSharedPreferences("minesweeper_prefs", Context.MODE_PRIVATE) + private var myLevel = DifficultyLevel.BEGINNER + private var minutes = 0 + private var seconds = 0 + + // 固定格子初始大小 + private var fixedSize = 98 + + // 点击音效的MediaPlayer + private var clickSoundPlayer: MediaPlayer = MediaPlayer.create(context, R.raw.tile) + + // 长按音效的MediaPlayer + private var clickLongSoundPlayer: MediaPlayer = MediaPlayer.create(context, R.raw.flag) + + private fun playClickSound() { + if (!clickSoundPlayer.isPlaying) { + clickSoundPlayer.start() + } + } + + private fun playLongClickSound() { + if (!clickLongSoundPlayer.isPlaying) { + clickLongSoundPlayer.start() + } + } + + var goods: Int//标记剩余未点开的非雷格子,用于判断游戏胜利 + var started = false//是否已经点击过第一个格子 + private var scaleFactor = 1.0f // 初始缩放比例 + private var scaleGestureDetector: ScaleGestureDetector + private var isScaling = false // 是否正在缩放 + + init { + goods = xLength * yLength - mineNum// 初始化非雷格子数 + + // 初始化缩放手势检测器 + scaleGestureDetector = ScaleGestureDetector(context, ScaleListener()) + } + + + private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { + // 缩放开始时,设置标志位为 true + isScaling = true + return super.onScaleBegin(detector) + } + + override fun onScale(detector: ScaleGestureDetector): Boolean { + // 更新缩放比例 + scaleFactor *= detector.scaleFactor + + // 限制缩放比例在 0.5x 到 3x 之间 + scaleFactor = scaleFactor.coerceIn(0.5f, 1.5f) + + // 请求重新布局 + requestLayout() + return true + } + + override fun onScaleEnd(detector: ScaleGestureDetector) { + // 缩放结束时,重置标志位为 false + isScaling = false + super.onScaleEnd(detector) + } + } + + + // 在填充格子时,初始化为固定大小 + fun fillAll() { + removeAllViews() + rowCount = xLength + columnCount = yLength + for (j in 0 until xLength) { + for (i in 0 until yLength) { + val one = SingleView(context, i, j, this) + addView(one, fixedSize, fixedSize) // 使用固定大小 + } + } + } + + private var lastX = 0f + private var lastY = 0f + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + lastX = x + lastY = y + + Log.d("ocean", "onInterceptTouchEvent ACTION_DOWN") + Log.d( + "ocean", + "onInterceptTouchEvent ACTION_DOWN width->${width} height->${height}" + ) + } + + MotionEvent.ACTION_MOVE -> { + val deltaX = x - lastX + val deltaY = y - lastY + + // 判断是否已经拖动,并且是否超过最小拖动距离 + if (abs(deltaX) > 10 || abs(deltaY) > 10) { + Log.d("ocean", "onInterceptTouchEvent ACTION_MOVE") + // 开始处理拖动时,拦截事件 + return true + } + } + } + return super.onInterceptTouchEvent(event) + } + + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + super.onLayout(changed, left, top, right, bottom) + + // 遍历所有的子视图,调整每个格子的大小 + val childSize = (fixedSize * scaleFactor).toInt() // 缩放后的格子大小 + for (i in 0 until childCount) { + val child = getChildAt(i) + val row = i / columnCount + val col = i % columnCount + + // 根据缩放后的大小设置每个子视图的位置和大小 + val childLeft = col * childSize + val childTop = row * childSize + child.layout( + childLeft, + childTop, + childLeft + childSize, + childTop + childSize + ) + } + } + + override fun requestLayout() { + super.requestLayout() + invalidate() + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + + // 让 scaleGestureDetector 处理缩放事件 + scaleGestureDetector.onTouchEvent(event) + // 如果正在缩放,阻止其他事件处理 + if (isScaling) { + return true + } + + val x = event.x + val y = event.y + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + Log.d("ocean", "onTouchEvent ACTION_DOWN") + lastX = x + lastY = y + } + + MotionEvent.ACTION_MOVE -> { + Log.d("ocean", "onTouchEvent ACTION_MOVE x->${event.x} y->${event.y}") + Log.d("ocean", "onTouchEvent ACTION_MOVE lastX->${lastX} lastY->${lastY}") + val dx = event.x - lastX + val dy = event.y - lastY + // 移动整个布局 + scrollBy(-dx.toInt(), -dy.toInt()) + lastX = event.x + lastY = event.y + + Log.d("ocean", "onTouchEvent ACTION_MOVE dx->$dx dy->$dy") + return true + } + + MotionEvent.ACTION_UP -> { + Log.d("ocean", "onTouchEvent ACTION_UP") + } + + MotionEvent.ACTION_CANCEL -> { + Log.d("ocean", "onTouchEvent ACTION_CANCEL") + } + } + return super.onTouchEvent(event) + } + + // 处理网格点击事件 + fun onChildClicked(v: SingleView, x: Int, y: Int) { + + playClickSound() + // 如果这是第一个点击,初始化游戏 + if (!started) { + initCurGame(x, y) + started = true + listener?.onStart(mineNum) + } + // 初始化完游戏之后 每一次(包括第一次)按下之后的逻辑 + when (v.logicState) { + SingleView.LOGICSTATE_BOMB -> { + doEachSingle { singleView -> + if (singleView.logicState == SingleView.LOGICSTATE_BOMB) { + singleView.viewState = SingleView.VIEWSTATE_THUNDER + } + singleView.enable = false + } + v.viewState = SingleView.VIEWSTATE_BOMB + listener?.defeat() + + clearGameStateSP() + } + + in 1..8 -> { // 显示周围的雷数 + v.viewState = SingleView.VIEWSTATE_TIP + goods-- + } + + 0 -> {// 如果点击的格子周围没有雷,展开周围的格子 + openAround(v, LogicHelper.getIndexByXY(xLength, yLength, x, y)) + } + } + + v.enable = false // 禁止再次点击已经翻开的格子 + doEachSingle { singleView -> singleView.updateInsideImg() } + // 判断是否胜利 + if (goods <= 0) { + listener?.victory() + doEachSingle { singleView -> singleView.enable = false } + + clearGameStateSP() + } + } + + // 打开周围的非雷格子 + private fun openAround(v: SingleView, index: Int) { + v.enable = false + v.viewState = SingleView.VIEWSTATE_TIP + goods-- + for (around in LogicHelper.getAroundItems(xLength, yLength, index)) { + val item = getChildByIndex(around) + if (item.viewState == SingleView.VIEWSTATE_DEFAULT && item.logicState >= 0) { + if (item.around == 0) { + openAround(getChildByIndex(around), around) + } else { + item.enable = false + item.viewState = SingleView.VIEWSTATE_TIP + goods-- + } + } + } + } + + // 初始化当前游戏,设置地雷 + private fun initCurGame(firstX: Int, firstY: Int) { + goods = xLength * yLength - mineNum + //初始化所有格子 + initAllChildBefore() + // 随机布雷,确保第一个点击的不是雷 + val randomMines = LogicHelper.randomMines(xLength, yLength, firstX, firstY, mineNum) + for (i: Int in randomMines) { + val singleView = getChildByIndex(i) + singleView.logicState = SingleView.LOGICSTATE_BOMB + singleView.viewState = SingleView.VIEWSTATE_DEFAULT//test 显示雷 + singleView.updateInsideImg() + } + calMinesAround(randomMines) + } + + // 计算每个格子周围的雷数 + private fun calMinesAround(randomMines: ArrayList) { + for (i: Int in randomMines) { + for (j in LogicHelper.getAroundItems(xLength, yLength, i)) { + val view = getChildByIndex(j) + view.around++ + if (view.logicState >= 0)//如果不是雷的话 + view.logicState = view.around + view.updateInsideImg() + } + } + } + + private fun getChildByIndex(index: Int): SingleView { + return getChildAt(index) as SingleView + } + + private fun initAllChildBefore() { + doEachSingle { singleView: SingleView -> + singleView.logicState = 0//初始化逻辑状态 Default + singleView.viewState = SingleView.VIEWSTATE_DEFAULT// + singleView.around = 0 + singleView.updateInsideImg() + } + } + + var flags = 0 + fun onChildLongClicked(v: SingleView, x: Int, y: Int, viewState: Int) { + playLongClickSound() + when (viewState) { + SingleView.VIEWSTATE_FLAG -> flags++ + SingleView.VIEWSTATE_UNSURE -> flags-- + } + listener?.onFlagsChange(flags, mineNum - flags) + } + + fun getChildByXY(x: Int, y: Int): SingleView { + return getChildAt(y * columnCount + x) as SingleView + } + + private fun doEachSingle(m: (singleView: SingleView) -> Unit) { + for (i in 0..childCount - 1) { + m(getChildByIndex(i)) + } + } + + //传送难度数据 + fun setDifficulty(level: DifficultyLevel) { + this.xLength = level.xLength + this.yLength = level.yLength + this.mineNum = level.mineNum + this.myLevel = level + } + + //传送时间 + fun setTime(min: Int, sec: Int) { + this.minutes = min + this.seconds = sec + + //每一秒都进行存储 + saveGameState() + } + + /** + * 开始 + */ + fun reset() { + doEachSingle { singleView -> + flags = 0 + goods = xLength * yLength - mineNum + started = false//要点击第一个之后才会变成true + singleView.reset() + singleView.updateInsideImg() + } + } + + + var listener: GameStateChange? = null + + interface GameStateChange { + fun onStart(rest: Int) + fun onFlagsChange(flags: Int, rest: Int) + fun victory() + fun defeat() + } + + /** + * 画分割线 + */ + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + if (childCount <= 0) return + val column = columnCount + val childCount = childCount + val localPaint = Paint() + localPaint.style = Paint.Style.STROKE + localPaint.setColor(Color.parseColor("#303030")) + localPaint.isAntiAlias = true + localPaint.strokeWidth = 1f + for (i in 0 until childCount) { + val cellView = getChildAt(i) + if (i < columnCount) { + canvas.drawLine( + cellView.left.toFloat(), + cellView.top.toFloat(), + cellView.right.toFloat(), + cellView.top.toFloat(), + localPaint + ) + } + if (i % columnCount == 0) { + canvas.drawLine( + cellView.left.toFloat(), + cellView.top.toFloat(), + cellView.left.toFloat(), + cellView.bottom.toFloat(), + localPaint + ) + } + canvas.drawLine( + cellView.right.toFloat(), + cellView.top.toFloat(), + cellView.right.toFloat(), + cellView.bottom.toFloat(), + localPaint + ) + canvas.drawLine( + cellView.left.toFloat(), + cellView.bottom.toFloat(), + cellView.right.toFloat(), + cellView.bottom.toFloat(), + localPaint + ) + } + if (childCount % column != 0) { + for (j in 0 until column - childCount % column) { + val lastView = getChildAt(childCount - 1) + canvas.drawLine( + (lastView.right + lastView.width * j).toFloat(), + lastView.top.toFloat(), + (lastView.right + lastView.width * j).toFloat(), + lastView.bottom.toFloat(), + localPaint + ) + } + } + } + + fun onPlayerRelease() { + clickSoundPlayer.release() // 释放资源 + clickLongSoundPlayer.release() + } + + fun clearGameStateSP() { + val editor = sharedPreferences.edit() + editor.remove("game_state") + editor.apply() + } + + fun saveGameState() { + val gridState = Array(xLength) { + Array(yLength) { + CellState( + 0, + SingleView.VIEWSTATE_DEFAULT, 0 + ) + } + } + + // 填充格子的状态 + for (y in 0 until xLength) { + for (x in 0 until yLength) { + val singleView = getChildByXY(x, y) + gridState[y][x] = + CellState(singleView.logicState, singleView.viewState, singleView.around) + } + } + + val gameState = GameState(gridState, goods, myLevel, minutes, seconds) + val json = Gson().toJson(gameState) + + + val editor = sharedPreferences.edit() + // 保存的游戏状态json + editor.putString("game_state", json) + // 提交保存 + editor.apply() + } + + fun loadGameState() { + // 获取保存的游戏状态 JSON 字符串 + val gameStateJson = sharedPreferences.getString("game_state", null) + if (gameStateJson.isNullOrEmpty()) { + return + } + val gameState: GameState = Gson().fromJson(gameStateJson, GameState::class.java) + //如果保存的难度和进来的难度不一致则不往下执行。 + if (gameState.difficulty != myLevel) { + return + } + // 恢复格子的状态 + for (y in 0 until xLength) { + for (x in 0 until yLength) { + val singleView = getChildByXY(x, y) + val cellState = gameState.grid[y][x] + singleView.logicState = cellState.logicState + singleView.viewState = cellState.viewState + singleView.around = cellState.around + singleView.updateInsideImg() // 更新格子显示 + } + } + goods = gameState.remainingMines + mineNum = gameState.difficulty.mineNum + started = true + } + +} \ No newline at end of file diff --git a/app/src/main/res/color/text_color_selector.xml b/app/src/main/res/color/text_color_selector.xml new file mode 100644 index 0000000..aa5b9e9 --- /dev/null +++ b/app/src/main/res/color/text_color_selector.xml @@ -0,0 +1,7 @@ + + + + + + + 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 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_bg_selector.xml b/app/src/main/res/drawable/btn_bg_selector.xml new file mode 100644 index 0000000..bff1a7a --- /dev/null +++ b/app/src/main/res/drawable/btn_bg_selector.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/drw_black_btn_bg.xml b/app/src/main/res/drawable/drw_black_btn_bg.xml new file mode 100644 index 0000000..b847226 --- /dev/null +++ b/app/src/main/res/drawable/drw_black_btn_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drw_dialog_bg.xml b/app/src/main/res/drawable/drw_dialog_bg.xml new file mode 100644 index 0000000..992ad75 --- /dev/null +++ b/app/src/main/res/drawable/drw_dialog_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drw_frame_grey_btn_bg.xml b/app/src/main/res/drawable/drw_frame_grey_btn_bg.xml new file mode 100644 index 0000000..62cd44b --- /dev/null +++ b/app/src/main/res/drawable/drw_frame_grey_btn_bg.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drw_grey_btn_bg.xml b/app/src/main/res/drawable/drw_grey_btn_bg.xml new file mode 100644 index 0000000..634db10 --- /dev/null +++ b/app/src/main/res/drawable/drw_grey_btn_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/drw_red_btn_bg.xml b/app/src/main/res/drawable/drw_red_btn_bg.xml new file mode 100644 index 0000000..8c7e62d --- /dev/null +++ b/app/src/main/res/drawable/drw_red_btn_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file 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 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_anguished_face.xml b/app/src/main/res/drawable/lose_emoji_anguished_face.xml new file mode 100644 index 0000000..03949ac --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_anguished_face.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_bomb.xml b/app/src/main/res/drawable/lose_emoji_bomb.xml new file mode 100644 index 0000000..ec8ed91 --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_bomb.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_confounded_face.xml b/app/src/main/res/drawable/lose_emoji_confounded_face.xml new file mode 100644 index 0000000..f0c2ce4 --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_confounded_face.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_downcast_face_with_sweat.xml b/app/src/main/res/drawable/lose_emoji_downcast_face_with_sweat.xml new file mode 100644 index 0000000..46f063d --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_downcast_face_with_sweat.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_exploding_head.xml b/app/src/main/res/drawable/lose_emoji_exploding_head.xml new file mode 100644 index 0000000..0508b66 --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_exploding_head.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_face_with_head_bandage.xml b/app/src/main/res/drawable/lose_emoji_face_with_head_bandage.xml new file mode 100644 index 0000000..b6e4ddf --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_face_with_head_bandage.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_grimacing_face.xml b/app/src/main/res/drawable/lose_emoji_grimacing_face.xml new file mode 100644 index 0000000..ca67564 --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_grimacing_face.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/lose_emoji_sad_but_relieved_face.xml b/app/src/main/res/drawable/lose_emoji_sad_but_relieved_face.xml new file mode 100644 index 0000000..5ce13e0 --- /dev/null +++ b/app/src/main/res/drawable/lose_emoji_sad_but_relieved_face.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/sel_bg_single.xml b/app/src/main/res/drawable/sel_bg_single.xml new file mode 100644 index 0000000..359edc0 --- /dev/null +++ b/app/src/main/res/drawable/sel_bg_single.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/win_emoji_beaming_face_with_smiling_eyes.xml b/app/src/main/res/drawable/win_emoji_beaming_face_with_smiling_eyes.xml new file mode 100644 index 0000000..33b902d --- /dev/null +++ b/app/src/main/res/drawable/win_emoji_beaming_face_with_smiling_eyes.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/win_emoji_cowboy_hat_face.xml b/app/src/main/res/drawable/win_emoji_cowboy_hat_face.xml new file mode 100644 index 0000000..62bed1f --- /dev/null +++ b/app/src/main/res/drawable/win_emoji_cowboy_hat_face.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/win_emoji_partying_face.xml b/app/src/main/res/drawable/win_emoji_partying_face.xml new file mode 100644 index 0000000..1ef150f --- /dev/null +++ b/app/src/main/res/drawable/win_emoji_partying_face.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/win_emoji_smiling_face_with_sunglasses.xml b/app/src/main/res/drawable/win_emoji_smiling_face_with_sunglasses.xml new file mode 100644 index 0000000..b331e0a --- /dev/null +++ b/app/src/main/res/drawable/win_emoji_smiling_face_with_sunglasses.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + 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 0000000..7ce2288 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + +