From 79b4f12a14f9d36b76dc223f675ce4ff16b890ea Mon Sep 17 00:00:00 2001
From: day_3 <3229208173@qq.com>
Date: Wed, 7 Oct 2026 17:23:47 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=BB=84=E5=90=88=E6=8E=B7?=
=?UTF-8?q?=E9=AA=B0=E5=8A=9F=E8=83=BD=E5=92=8C=20Android=20=E6=94=AF?=
=?UTF-8?q?=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 12 +-
androidApp/build.gradle.kts | 42 ++++
androidApp/src/main/AndroidManifest.xml | 17 ++
.../git/diceroller/android/MainActivity.kt | 30 +++
build.gradle.kts | 4 +-
gradle.properties | 4 +-
gradle/libs.versions.toml | 7 +-
gradle/wrapper/gradle-wrapper.properties | 4 +-
settings.gradle.kts | 3 +-
shared/build.gradle.kts | 19 +-
.../kotlin/cn/itouc/git/diceroller/App.kt | 225 ++++++++++++++++--
11 files changed, 328 insertions(+), 39 deletions(-)
create mode 100644 androidApp/build.gradle.kts
create mode 100644 androidApp/src/main/AndroidManifest.xml
create mode 100644 androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt
diff --git a/README.md b/README.md
index 745446e..9376968 100644
--- a/README.md
+++ b/README.md
@@ -26,17 +26,17 @@
- [x] 50分 至少可以 roll D6 任意次
-- [ ] 30分 可以实现 roll D4,D6,D8,D12(或者更多)任意切换
+- [x] 30分 可以实现 roll D4,D6,D8,D12(或者更多)任意切换
-- [ ] 20分 可以实现 roll n D6(指同时掷出 n 个六面骰并将点数加和)
+- [x] 20分 可以实现 roll n D6(指同时掷出 n 个六面骰并将点数加和)
-- [ ] 20分 可以实现 roll n D m(指同时掷出 n 个 m 面骰并将点数加和)
+- [x] 20分 可以实现 roll n D m(指同时掷出 n 个 m 面骰并将点数加和)
-- [ ] 20分 可以实现 roll $n_1$ D $m_1$ , $n_2$ D $m_2$ ...... 任意组合,最后点数取和
+- [x] 20分 可以实现 roll $n_1$ D $m_1$ , $n_2$ D $m_2$ ...... 任意组合,最后点数取和
### 额外 共35分
-- [ ] 5分 支持 Android 平台的编译与安装
+- [x] 5分 支持 Android 平台的编译与安装
- [ ] 5分 支持浏览器 WASM 编译
@@ -46,7 +46,7 @@
- [ ] 5分 有应用图标
-- [ ] 5分 好看,达到我的审美
+- [?] 5分 好看,达到我的审美
- [ ] 5分 根据实现方法,代码格式化等的特殊得分
diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts
new file mode 100644
index 0000000..861404f
--- /dev/null
+++ b/androidApp/build.gradle.kts
@@ -0,0 +1,42 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.composeMultiplatform)
+ alias(libs.plugins.composeCompiler)
+}
+
+android {
+ namespace = "cn.itouc.git.diceroller.android"
+ compileSdk = 37
+
+ defaultConfig {
+ applicationId = "cn.itouc.git.diceroller"
+ minSdk = 24
+ targetSdk = 37
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+}
+
+dependencies {
+ implementation(project(":shared"))
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.compose.runtime)
+ implementation(libs.compose.foundation)
+}
diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..2f9568b
--- /dev/null
+++ b/androidApp/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt b/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt
new file mode 100644
index 0000000..ee3f1cc
--- /dev/null
+++ b/androidApp/src/main/kotlin/cn/itouc/git/diceroller/android/MainActivity.kt
@@ -0,0 +1,30 @@
+package cn.itouc.git.diceroller.android
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawingPadding
+import androidx.compose.ui.Modifier
+import cn.itouc.git.diceroller.App
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ setContent {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .safeDrawingPadding()
+ .imePadding()
+ ) {
+ App()
+ }
+ }
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 627d2c9..5ecb6db 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,8 +1,10 @@
plugins {
+ alias(libs.plugins.androidApplication) apply false
+ alias(libs.plugins.androidKmpLibrary) apply false
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinJvm) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
-}
\ No newline at end of file
+}
diff --git a/gradle.properties b/gradle.properties
index 41d680b..7213fd4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,4 +5,6 @@ kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
-org.gradle.caching=true
\ No newline at end of file
+org.gradle.caching=true
+android.useAndroidX=true
+android.overridePathCheck=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 66e2535..874ca4f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,4 +1,6 @@
[versions]
+agp = "9.4.0"
+androidx-activity = "1.10.1"
androidx-lifecycle = "2.11.0"
composeMultiplatform = "1.12.0"
junit = "4.13.2"
@@ -7,6 +9,7 @@ kotlinx-coroutines = "1.11.0"
material3 = "1.12.0-alpha03"
[libraries]
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
@@ -21,7 +24,9 @@ compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-previ
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
[plugins]
+androidApplication = { id = "com.android.application", version.ref = "agp" }
+androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
-kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file
+kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 56eb7cc..760c459 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,10 +1,10 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
+distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
\ No newline at end of file
+zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 632e069..528e772 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,4 +34,5 @@ plugins {
}
include(":desktopApp")
-include(":shared")
\ No newline at end of file
+include(":shared")
+include(":androidApp")
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts
index 0d6dd37..48b0a01 100644
--- a/shared/build.gradle.kts
+++ b/shared/build.gradle.kts
@@ -1,4 +1,7 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
plugins {
+ alias(libs.plugins.androidKmpLibrary)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
@@ -7,6 +10,20 @@ plugins {
kotlin {
jvm()
+ android {
+ namespace = "cn.itouc.git.diceroller.shared"
+ compileSdk = 37
+ minSdk = 24
+
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_17)
+ }
+
+ androidResources {
+ enable = true
+ }
+ }
+
sourceSets {
commonMain.dependencies {
implementation(libs.compose.runtime)
@@ -22,4 +39,4 @@ kotlin {
implementation(libs.kotlin.test)
}
}
-}
\ No newline at end of file
+}
diff --git a/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt b/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt
index b596ffd..25c13f2 100644
--- a/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt
+++ b/shared/src/commonMain/kotlin/cn/itouc/git/diceroller/App.kt
@@ -1,56 +1,229 @@
package cn.itouc.git.diceroller
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import org.jetbrains.compose.resources.painterResource
-
-import diceroller.shared.generated.resources.Res
-import diceroller.shared.generated.resources.compose_multiplatform
+import androidx.compose.ui.unit.dp
import kotlin.random.Random
+data class DiceGroup(
+ val count: Int,
+ val sides: Int
+)
+
+data class DiceGroupResult(
+ val group: DiceGroup,
+ val points: List
+) {
+ val total: Long
+ get() = points.sumOf { it.toLong() }
+}
+
@Composable
-@Preview
fun App() {
MaterialTheme {
- var showContent by remember { mutableStateOf(false) }
- var randomInt by remember { mutableStateOf(Random.nextInt()) }
+ var sidesInput by remember { mutableStateOf("6") }
+ var countInput by remember { mutableStateOf("1") }
+
+ var groups by remember {
+ mutableStateOf(emptyList())
+ }
+
+ var results by remember {
+ mutableStateOf(emptyList())
+ }
+
+ val diceSides = sidesInput.toIntOrNull()
+ val diceCount = countInput.toIntOrNull()
+
+ val validSides = diceSides != null &&
+ diceSides >= 1 &&
+ diceSides < Int.MAX_VALUE
+
+ val validCount = diceCount != null &&
+ diceCount in 1..1000
+
+ val totalDiceCount = groups.sumOf { it.count }
+
+ val withinLimit = diceCount != null &&
+ diceCount in 1..1000 &&
+ totalDiceCount + diceCount <= 1000
Column(
modifier = Modifier
- .background(MaterialTheme.colorScheme.primaryContainer)
- .safeContentPadding()
- .fillMaxSize(),
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
+ Text(
+ text = "组合掷骰子",
+ style = MaterialTheme.typography.headlineMedium
+ )
+
+ OutlinedTextField(
+ value = sidesInput,
+ onValueChange = { sidesInput = it },
+ label = { Text("骰子面数") },
+ singleLine = true,
+ isError = !validSides
+ )
+
+ if (!validSides) {
+ Text(
+ text = "面数请输入 1~2147483646 的整数",
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+
+ OutlinedTextField(
+ value = countInput,
+ onValueChange = { countInput = it },
+ label = { Text("骰子数量") },
+ singleLine = true,
+ isError = !validCount
+ )
+
+ if (!validCount) {
+ Text(
+ text = "数量请输入 1~1000 的整数",
+ color = MaterialTheme.colorScheme.error
+ )
+ } else if (!withinLimit) {
+ Text(
+ text = "添加后将超过总数限制,最多允许 1000 个骰子",
+ color = MaterialTheme.colorScheme.error
+ )
+ }
+
Button(
+ enabled = validSides && validCount && withinLimit,
onClick = {
- showContent = !showContent
- if (showContent) randomInt = Random.nextInt()
+ val sides = diceSides
+ val count = diceCount
+
+ if (
+ sides != null &&
+ sides >= 1 &&
+ sides < Int.MAX_VALUE &&
+ count != null &&
+ count in 1..1000 &&
+ groups.sumOf { it.count } + count <= 1000
+ ) {
+ groups = groups + DiceGroup(
+ count = count,
+ sides = sides
+ )
+
+ results = emptyList()
+ }
}
) {
- Text("Click me!")
+ Text("添加骰子组")
}
- AnimatedVisibility(showContent) {
- Column(
- modifier = Modifier.fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(painterResource(Res.drawable.compose_multiplatform), null)
- Text("a random int: $randomInt")
+
+ Text(
+ text = "当前组合",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ if (groups.isEmpty()) {
+ Text("还没有骰子组,请先添加")
+ } else {
+ groups.forEachIndexed { index, group ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Text(
+ text = "第 ${index + 1} 组:${group.count}D${group.sides}",
+ modifier = Modifier.weight(1f)
+ )
+
+ TextButton(
+ onClick = {
+ groups = groups.filterIndexed { groupIndex, _ ->
+ groupIndex != index
+ }
+ results = emptyList()
+ }
+ ) {
+ Text("删除")
+ }
+ }
}
}
+
+ Text("共 ${groups.size} 组,$totalDiceCount / 1000 个骰子")
+
+ Button(
+ enabled = groups.isNotEmpty(),
+ onClick = {
+ results = groups.map { group ->
+ DiceGroupResult(
+ group = group,
+ points = List(group.count) {
+ Random.nextInt(1, group.sides + 1)
+ }
+ )
+ }
+ }
+ ) {
+ Text("全部掷骰")
+ }
+
+ if (results.isNotEmpty()) {
+ Text(
+ text = "掷骰结果",
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ results.forEachIndexed { index, result ->
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Text(
+ text = "第 ${index + 1} 组:" +
+ "${result.group.count}D${result.group.sides}",
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ val pointsText = result.points
+ .take(20)
+ .joinToString(" + ")
+
+ val remainingHint =
+ if (result.points.size > 20) {
+ " + …"
+ } else {
+ ""
+ }
+
+ Text("点数:$pointsText$remainingHint")
+ Text("小计:${result.total}")
+ }
+ }
+
+ val grandTotal = results.sumOf { it.total }
+
+ Text(
+ text = "总和:$grandTotal",
+ style = MaterialTheme.typography.headlineMedium
+ )
+ }
}
}
}
\ No newline at end of file