完成组合掷骰功能和 Android 支持

This commit is contained in:
2026-10-07 17:23:47 +08:00
parent bfdff74c9d
commit 79b4f12a14
11 changed files with 328 additions and 39 deletions
+6 -6
View File
@@ -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分 根据实现方法,代码格式化等的特殊得分
+42
View File
@@ -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)
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="组合掷骰子"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name="cn.itouc.git.diceroller.android.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -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()
}
}
}
}
+3 -1
View File
@@ -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
}
}
+3 -1
View File
@@ -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
org.gradle.caching=true
android.useAndroidX=true
android.overridePathCheck=true
+6 -1
View File
@@ -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" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+2 -2
View File
@@ -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
zipStorePath=wrapper/dists
+2 -1
View File
@@ -34,4 +34,5 @@ plugins {
}
include(":desktopApp")
include(":shared")
include(":shared")
include(":androidApp")
+18 -1
View File
@@ -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)
}
}
}
}
@@ -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<Int>
) {
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<DiceGroup>())
}
var results by remember {
mutableStateOf(emptyList<DiceGroupResult>())
}
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
)
}
}
}
}