640 lines
29 KiB
Kotlin
640 lines
29 KiB
Kotlin
package com.unogame.ui.screens
|
||
|
||
import android.content.Context
|
||
import androidx.compose.animation.core.animateFloatAsState
|
||
import androidx.compose.animation.core.spring
|
||
import androidx.compose.foundation.background
|
||
import androidx.compose.foundation.layout.*
|
||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||
import androidx.compose.foundation.lazy.LazyColumn
|
||
import androidx.compose.foundation.lazy.itemsIndexed
|
||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||
import androidx.compose.foundation.rememberScrollState
|
||
import androidx.compose.foundation.verticalScroll
|
||
import androidx.compose.material.icons.Icons
|
||
import androidx.compose.material.icons.filled.*
|
||
import androidx.compose.material3.*
|
||
import androidx.compose.runtime.*
|
||
import androidx.compose.ui.Alignment
|
||
import androidx.compose.ui.Modifier
|
||
import androidx.compose.ui.draw.clip
|
||
import androidx.compose.ui.draw.scale
|
||
import androidx.compose.ui.graphics.Color
|
||
import androidx.compose.ui.platform.LocalContext
|
||
import androidx.compose.ui.text.SpanStyle
|
||
import androidx.compose.ui.text.buildAnnotatedString
|
||
import androidx.compose.ui.text.font.FontWeight
|
||
import androidx.compose.ui.text.withStyle
|
||
import androidx.compose.ui.unit.dp
|
||
import androidx.compose.ui.unit.sp
|
||
import com.google.gson.Gson
|
||
import com.google.gson.reflect.TypeToken
|
||
import com.unogame.game.AIDifficulty
|
||
import com.unogame.game.GameEngine
|
||
import com.unogame.game.GameMode
|
||
import com.unogame.game.GameRules
|
||
import com.unogame.game.SimpleAI
|
||
import com.unogame.model.*
|
||
import com.unogame.ui.components.ColorPickerDialog
|
||
import com.unogame.ui.components.getBotAvatar
|
||
import com.unogame.ui.theme.*
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.launch
|
||
|
||
val BOT_NAMES = listOf(
|
||
"赛博打工人", "机器人-钛君", "赛博牛马", "硅基生物", "电子包浆", "铁憨憨",
|
||
"充电宝精", "电子宠物", "终结者", "天网", "瓦力", "伊娃",
|
||
"人工智障", "GPT仔", "大模型基佬", "电耗子", "塑料脑", "波塔", "阿法狗"
|
||
)
|
||
|
||
fun pickUniqueBotNames(count: Int): List<String> {
|
||
val shuffled = BOT_NAMES.shuffled()
|
||
val names = mutableListOf<String>()
|
||
for (i in 0 until count) {
|
||
if (i < shuffled.size) names.add(shuffled[i])
|
||
else names.add("机器人${i + 1}")
|
||
}
|
||
return names
|
||
}
|
||
|
||
data class ScoreEntry(
|
||
val name: String,
|
||
val mode: String,
|
||
val points: Int,
|
||
val difficulty: String,
|
||
val duration: Int,
|
||
val playerCount: Int,
|
||
val turnNumber: Int,
|
||
val date: Long,
|
||
val scoreDetail: String? = "",
|
||
val opponentDetail: String? = ""
|
||
)
|
||
|
||
object Scoreboard {
|
||
private const val PREFS_KEY = "uno_scoreboard"
|
||
private val gson = Gson()
|
||
|
||
fun loadScores(context: Context): List<ScoreEntry> {
|
||
val json = context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE)
|
||
.getString(PREFS_KEY, null) ?: return emptyList()
|
||
return try {
|
||
gson.fromJson(json, object : TypeToken<List<ScoreEntry>>() {}.type) ?: emptyList()
|
||
} catch (_: Exception) { emptyList() }
|
||
}
|
||
|
||
fun saveScores(context: Context, scores: List<ScoreEntry>) {
|
||
context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE)
|
||
.edit().putString(PREFS_KEY, gson.toJson(scores)).apply()
|
||
}
|
||
|
||
fun addEntry(
|
||
context: Context,
|
||
name: String,
|
||
mode: String,
|
||
points: Int,
|
||
difficulty: String,
|
||
duration: Int,
|
||
playerCount: Int,
|
||
turnNumber: Int,
|
||
scoreDetail: String = "",
|
||
opponentDetail: String = ""
|
||
) {
|
||
val scores = loadScores(context).toMutableList()
|
||
scores.add(
|
||
ScoreEntry(
|
||
name = name,
|
||
mode = mode,
|
||
points = points,
|
||
difficulty = difficulty,
|
||
duration = duration,
|
||
playerCount = playerCount,
|
||
turnNumber = turnNumber,
|
||
date = System.currentTimeMillis(),
|
||
scoreDetail = scoreDetail,
|
||
opponentDetail = opponentDetail
|
||
)
|
||
)
|
||
saveScores(context, scores.sortedByDescending { it.points }.take(50))
|
||
}
|
||
}
|
||
|
||
@Composable
|
||
fun LocalGameScreen(
|
||
totalPlayers: Int,
|
||
humanPlayerName: String,
|
||
mode: GameMode,
|
||
botNames: List<String> = emptyList(),
|
||
handOffset: Float = 0f,
|
||
onBackToMenu: () -> Unit
|
||
) {
|
||
val scope = rememberCoroutineScope()
|
||
val context = LocalContext.current
|
||
val rules = remember { GameRules.forMode(mode, GameRules.loadMaxHandSize(context, mode)) }
|
||
val engine = remember { GameEngine(rules) }
|
||
val aiDiff = remember { AIDifficulty.load(context, mode) }
|
||
val gameStartTime = remember { System.currentTimeMillis() }
|
||
|
||
val players = remember {
|
||
val names = if (botNames.isEmpty()) pickUniqueBotNames(totalPlayers - 1) else botNames
|
||
val list = mutableListOf<Player>()
|
||
list.add(Player(id = "human", name = humanPlayerName, isCurrentTurn = true))
|
||
for (i in 0 until totalPlayers - 1) {
|
||
list.add(Player(id = "bot_${i + 2}", name = names.getOrElse(i) { "机器人${i + 1}" }, isCurrentTurn = false))
|
||
}
|
||
list
|
||
}
|
||
|
||
var gameState by remember { mutableStateOf(engine.createInitialState(players)) }
|
||
var myCards by remember { mutableStateOf(gameState.players.find { it.id == "human" }?.cards ?: emptyList()) }
|
||
var errorMessage by remember { mutableStateOf("") }
|
||
var selectedWildColor by remember { mutableStateOf<CardColor?>(null) }
|
||
var isGameOver by remember { mutableStateOf(false) }
|
||
var winnerName by remember { mutableStateOf("") }
|
||
var isYouWinner by remember { mutableStateOf(false) }
|
||
var scoreSaved by remember { mutableStateOf(false) }
|
||
var gamePoints by remember { mutableIntStateOf(0) }
|
||
var gameDuration by remember { mutableIntStateOf(0) }
|
||
var gameDifficulty by remember { mutableStateOf("") }
|
||
var gameTurnNumber by remember { mutableIntStateOf(0) }
|
||
var showDrawPlayPopup by remember { mutableStateOf(false) }
|
||
var pendingDrawState by remember { mutableStateOf<GameState?>(null) }
|
||
var pendingDrawnCardIndex by remember { mutableIntStateOf(-1) }
|
||
var pendingDrawIsWild by remember { mutableStateOf(false) }
|
||
var showDrawPlayWildPicker by remember { mutableStateOf(false) }
|
||
var pendingDrawStateForWild by remember { mutableStateOf<GameState?>(null) }
|
||
var pendingDrawCardIdxForWild by remember { mutableIntStateOf(-1) }
|
||
var gameLog by remember { mutableStateOf(listOf<String>()) }
|
||
var showLogDialog by remember { mutableStateOf(false) }
|
||
var showSwapTargetPicker by remember { mutableStateOf(false) }
|
||
var pendingSwapState by remember { mutableStateOf<GameState?>(null) }
|
||
|
||
val myPlayerId = "human"
|
||
val currentPlayer = gameState.currentPlayer
|
||
val isMyTurn = currentPlayer.id == myPlayerId && !isGameOver
|
||
|
||
fun updateState(state: GameState) {
|
||
gameState = state
|
||
myCards = state.players.find { it.id == myPlayerId }?.cards ?: myCards
|
||
errorMessage = ""
|
||
if (state.message.isNotEmpty()) {
|
||
// 倒序插入,最新在上
|
||
gameLog = listOf(state.message) + gameLog
|
||
}
|
||
if (state.isGameOver) {
|
||
isGameOver = true
|
||
winnerName = state.winner?.name ?: ""
|
||
isYouWinner = state.winner?.id == myPlayerId
|
||
if (isYouWinner && !scoreSaved) {
|
||
scoreSaved = true
|
||
gameTurnNumber = state.turnNumber
|
||
gameDuration = ((System.currentTimeMillis() - gameStartTime) / 1000).toInt()
|
||
gameDifficulty = aiDiff.displayName
|
||
gamePoints = state.players.filter { it.id != myPlayerId }.sumOf { player ->
|
||
player.cards.sumOf { card ->
|
||
val active = if (state.flipped && card.flipSide != null) card.flipSide else card
|
||
active.score
|
||
}
|
||
}
|
||
val scoreDetails = state.players.filter { it.id != myPlayerId }.flatMap { player ->
|
||
player.cards.map { card ->
|
||
val active = if (state.flipped && card.flipSide != null) card.flipSide else card
|
||
val label = if (active.type == CardType.NUMBER) "${active.color.displayName}${active.number}"
|
||
else "${active.color.displayName}${active.type.symbol}"
|
||
"${label}(${active.score}分)"
|
||
}
|
||
}
|
||
val detailStr = scoreDetails.joinToString(", ")
|
||
// 对手名和各自总分
|
||
val opponentDetails = state.players.filter { it.id != myPlayerId }.joinToString(";") { p ->
|
||
val pScore = p.cards.sumOf { card ->
|
||
val active = if (state.flipped && card.flipSide != null) card.flipSide else card
|
||
active.score
|
||
}
|
||
"${p.name}:${pScore}分"
|
||
}
|
||
Scoreboard.addEntry(
|
||
context = context,
|
||
name = humanPlayerName,
|
||
mode = mode.displayName,
|
||
points = gamePoints,
|
||
difficulty = gameDifficulty,
|
||
duration = gameDuration,
|
||
playerCount = totalPlayers,
|
||
turnNumber = gameTurnNumber,
|
||
scoreDetail = detailStr,
|
||
opponentDetail = opponentDetails
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun executePlay(cardIndex: Int, chosenColor: CardColor? = null, swapTargetId: String? = null) {
|
||
val result = engine.playCard(gameState, myPlayerId, cardIndex, chosenColor, swapTargetId)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> {
|
||
if (result.needsSwapTarget) {
|
||
pendingSwapState = result.state
|
||
showSwapTargetPicker = true
|
||
} else {
|
||
updateState(result.state)
|
||
}
|
||
}
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
}
|
||
|
||
fun executeDraw() {
|
||
val result = engine.drawCard(gameState, myPlayerId)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> {
|
||
if (result.drawnCardPlayableIndex >= 0) {
|
||
pendingDrawState = result.state
|
||
pendingDrawnCardIndex = result.drawnCardPlayableIndex
|
||
val drawnCard = result.state.players.find { it.id == myPlayerId }?.cards?.getOrNull(result.drawnCardPlayableIndex)
|
||
pendingDrawIsWild = drawnCard?.type?.isWild == true
|
||
showDrawPlayPopup = true
|
||
} else {
|
||
updateState(result.state)
|
||
}
|
||
}
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
}
|
||
|
||
LaunchedEffect(gameState.turnNumber) {
|
||
if (isGameOver) return@LaunchedEffect
|
||
val current = gameState.currentPlayer
|
||
if (current.id != myPlayerId) {
|
||
delay(1200L)
|
||
|
||
val move = SimpleAI.decideMove(
|
||
current, gameState.topCard, gameState.currentWildColor, gameState.flipped, aiDiff,
|
||
otherPlayers = gameState.players,
|
||
isSevenZero = mode == GameMode.SEVEN_ZERO
|
||
)
|
||
when (move.type) {
|
||
SimpleAI.MoveType.PLAY -> {
|
||
val result = engine.playCard(gameState, current.id, move.cardIndex, move.chosenColor, move.swapTargetId)
|
||
if (result is GameEngine.PlayResult.Success) {
|
||
updateState(result.state)
|
||
} else {
|
||
val drawResult = engine.drawCard(gameState, current.id)
|
||
if (drawResult is GameEngine.PlayResult.Success) updateState(drawResult.state)
|
||
}
|
||
}
|
||
SimpleAI.MoveType.DRAW -> {
|
||
val drawResult = engine.drawCard(gameState, current.id)
|
||
if (drawResult is GameEngine.PlayResult.Success) {
|
||
if (drawResult.drawnCardPlayableIndex >= 0) {
|
||
val playResult = engine.playCard(drawResult.state, current.id, drawResult.drawnCardPlayableIndex)
|
||
if (playResult is GameEngine.PlayResult.Success) {
|
||
updateState(playResult.state)
|
||
} else {
|
||
val skipResult = engine.skipAfterDraw(drawResult.state)
|
||
if (skipResult is GameEngine.PlayResult.Success) updateState(skipResult.state)
|
||
}
|
||
} else {
|
||
updateState(drawResult.state)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Show flip indicator
|
||
val displayCards = remember(gameState.flipped, myCards) {
|
||
myCards.map { it.activeCard(gameState.flipped) }
|
||
}
|
||
|
||
// Draw-then-play popup
|
||
if (showDrawPlayPopup && pendingDrawState != null) {
|
||
AlertDialog(
|
||
onDismissRequest = {
|
||
val state = pendingDrawState!!
|
||
val result = engine.skipAfterDraw(state)
|
||
if (result is GameEngine.PlayResult.Success) updateState(result.state)
|
||
showDrawPlayPopup = false
|
||
},
|
||
title = { Text("摸到了可出的牌", color = GoldAccent) },
|
||
text = { Text("刚摸到的牌可以打出去,要出这张牌吗?", color = Color.White.copy(alpha = 0.8f)) },
|
||
confirmButton = {
|
||
TextButton(onClick = {
|
||
val state = pendingDrawState!!
|
||
val cardIdx = pendingDrawnCardIndex
|
||
val isWild = pendingDrawIsWild
|
||
if (isWild) {
|
||
showDrawPlayPopup = false
|
||
selectedWildColor = null
|
||
showDrawPlayWildPicker = true
|
||
pendingDrawStateForWild = state
|
||
pendingDrawCardIdxForWild = cardIdx
|
||
} else {
|
||
showDrawPlayPopup = false
|
||
pendingDrawState = null
|
||
if (cardIdx >= 0) {
|
||
val result = engine.playCard(state, myPlayerId, cardIdx)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> updateState(result.state)
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
}
|
||
}
|
||
}) { Text("出牌", color = GoldAccent) }
|
||
},
|
||
dismissButton = {
|
||
TextButton(onClick = {
|
||
val state = pendingDrawState!!
|
||
val result = engine.skipAfterDraw(state)
|
||
if (result is GameEngine.PlayResult.Success) updateState(result.state)
|
||
showDrawPlayPopup = false
|
||
}) { Text("跳过", color = Color.White.copy(alpha = 0.5f)) }
|
||
},
|
||
containerColor = DarkSurface
|
||
)
|
||
}
|
||
|
||
// 出牌记录弹窗
|
||
if (showLogDialog) {
|
||
// 卡牌颜色名映射
|
||
val cardColorMap = mapOf(
|
||
"红" to UnoRed, "蓝" to UnoBlue,
|
||
"黄" to Color(0xFFFDD835), "绿" to UnoGreen,
|
||
"粉" to Color(0xFFE91E63), "紫" to Color(0xFF9C27B0),
|
||
"青" to Color(0xFF009688), "橙" to Color(0xFFFF9800),
|
||
"万能" to Color(0xFF616161), "黑" to Color(0xFF424242)
|
||
)
|
||
fun parsePlayerName(msg: String): String = msg.substringBefore(" 出了").substringBefore(" 摸了").substringBefore(" 选择").substringBefore(" 无法")
|
||
fun parseColorText(msg: String): String {
|
||
val after = msg.substringAfter(" 出了 ", "")
|
||
if (after.isEmpty()) return ""
|
||
val colorNames = listOf("万能", "红", "蓝", "黄", "绿", "粉", "紫", "青", "橙", "黑")
|
||
return colorNames.firstOrNull { after.startsWith(it) } ?: ""
|
||
}
|
||
fun parseArrowColor(msg: String): Pair<String, Color>? {
|
||
val arrowIdx = msg.indexOf(" → ")
|
||
if (arrowIdx < 0) return null
|
||
val afterArrow = msg.substring(arrowIdx + 3).trimStart()
|
||
val cn = listOf("红", "蓝", "黄", "绿", "粉", "紫", "青", "橙", "黑", "万能")
|
||
for (name in cn) {
|
||
if (afterArrow.startsWith(name)) {
|
||
return name to (cardColorMap[name] ?: Color.White)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
val creamBg = Color(0xFFFFF8E1) // 护眼米白
|
||
val dimText = Color(0xFF555555) // 浅色底上的暗灰文字
|
||
|
||
// 列表状态放在外层供标题栏和内容区共享
|
||
val listState = rememberLazyListState()
|
||
val canScrollForward by remember { derivedStateOf { listState.canScrollForward } }
|
||
val canScrollBackward by remember { derivedStateOf { listState.canScrollBackward } }
|
||
|
||
AlertDialog(
|
||
modifier = Modifier.fillMaxWidth(0.98f),
|
||
onDismissRequest = { showLogDialog = false },
|
||
title = {
|
||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||
Text("出牌记录", color = Color(0xFF333333), fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f))
|
||
if (gameLog.size > 4 && canScrollBackward) {
|
||
var topPressed by remember { mutableStateOf(false) }
|
||
val topScale by animateFloatAsState(if (topPressed) 1.4f else 1f, spring())
|
||
if (topPressed) {
|
||
LaunchedEffect(Unit) { kotlinx.coroutines.delay(400); topPressed = false }
|
||
}
|
||
IconButton(
|
||
onClick = { topPressed = true; scope.launch { listState.animateScrollToItem(0) } },
|
||
modifier = Modifier.size(28.dp).scale(topScale)
|
||
) { Icon(Icons.Default.KeyboardArrowUp, "到顶部", tint = Color(0xFF555555)) }
|
||
} else {
|
||
Spacer(modifier = Modifier.size(28.dp)) // 占位保持标题居中
|
||
}
|
||
}
|
||
},
|
||
text = {
|
||
// 记录内容
|
||
if (gameLog.isEmpty()) {
|
||
Text("暂无记录", color = dimText, fontSize = 13.sp)
|
||
} else {
|
||
Box(
|
||
modifier = Modifier
|
||
.height(450.dp)
|
||
.fillMaxWidth()
|
||
) {
|
||
// 滚动内容(纯文字区域)
|
||
LazyColumn(
|
||
state = listState,
|
||
modifier = Modifier.fillMaxSize()
|
||
) {
|
||
itemsIndexed(gameLog) { index, msg ->
|
||
val playerName = parsePlayerName(msg)
|
||
val avatar = getBotAvatar(playerName)
|
||
val colorText = parseColorText(msg)
|
||
val cardColor = cardColorMap[colorText]
|
||
val isPlay = " 出了 " in msg
|
||
val arrowColor = parseArrowColor(msg)
|
||
val afterPlayer = msg.substringAfter(playerName)
|
||
|
||
val annotated = buildAnnotatedString {
|
||
withStyle(SpanStyle(color = Color(0xFF999999), fontSize = 13.sp)) {
|
||
append("${gameLog.size - index}. ")
|
||
}
|
||
withStyle(SpanStyle(color = avatar.color, fontWeight = FontWeight.Bold, fontSize = 13.sp)) {
|
||
append(playerName)
|
||
}
|
||
if (isPlay && (colorText.isNotEmpty() || arrowColor != null)) {
|
||
if (arrowColor != null) {
|
||
val prefix = afterPlayer.substringBefore(" 万能")
|
||
val cardBlock = afterPlayer.substringAfter(" 出了 ").substringBefore(",")
|
||
val remain = afterPlayer.substringAfter(prefix + " 出了 " + cardBlock)
|
||
withStyle(SpanStyle(color = dimText, fontSize = 13.sp)) {
|
||
append(prefix); append(" 出了 ")
|
||
}
|
||
withStyle(SpanStyle(color = Color.Black, fontWeight = FontWeight.Black,
|
||
fontSize = 13.sp, background = arrowColor.second.copy(alpha = 0.9f))) {
|
||
append(cardBlock)
|
||
}
|
||
if (remain.isNotEmpty()) {
|
||
withStyle(SpanStyle(color = Color(0xFF777777), fontSize = 12.sp)) {
|
||
append(remain)
|
||
}
|
||
}
|
||
} else {
|
||
val prefix = afterPlayer.substringBefore(colorText)
|
||
val cardId = colorText + afterPlayer.substringAfter(colorText).substringBefore(",")
|
||
val remain = afterPlayer.substringAfter(prefix + cardId)
|
||
withStyle(SpanStyle(color = dimText, fontSize = 13.sp)) {
|
||
append(prefix)
|
||
}
|
||
withStyle(SpanStyle(color = Color.Black, fontWeight = FontWeight.Black,
|
||
fontSize = 13.sp, background = (cardColor ?: Color.Gray).copy(alpha = 0.9f))) {
|
||
append(cardId)
|
||
}
|
||
if (remain.isNotEmpty()) {
|
||
withStyle(SpanStyle(color = Color(0xFF777777), fontSize = 12.sp)) {
|
||
append(remain)
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
withStyle(SpanStyle(color = dimText, fontSize = 13.sp)) {
|
||
append(afterPlayer)
|
||
}
|
||
}
|
||
}
|
||
Text(annotated, modifier = Modifier.padding(vertical = 5.dp))
|
||
}
|
||
}
|
||
|
||
} // Box 结束
|
||
}
|
||
},
|
||
confirmButton = {
|
||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||
if (canScrollForward) {
|
||
var bottomPressed by remember { mutableStateOf(false) }
|
||
val bottomScale by animateFloatAsState(if (bottomPressed) 1.4f else 1f, spring())
|
||
if (bottomPressed) {
|
||
LaunchedEffect(Unit) { kotlinx.coroutines.delay(400); bottomPressed = false }
|
||
}
|
||
IconButton(
|
||
onClick = { bottomPressed = true; scope.launch { listState.animateScrollToItem(gameLog.size - 1) } },
|
||
modifier = Modifier.size(28.dp).scale(bottomScale)
|
||
) { Icon(Icons.Default.KeyboardArrowDown, "到底部", tint = Color(0xFF555555)) }
|
||
} else {
|
||
Spacer(modifier = Modifier.size(28.dp))
|
||
}
|
||
Spacer(modifier = Modifier.weight(1f))
|
||
TextButton(onClick = { showLogDialog = false }) {
|
||
Text("关闭", color = Color(0xFF333333))
|
||
}
|
||
}
|
||
},
|
||
containerColor = creamBg
|
||
)
|
||
}
|
||
|
||
// Color picker for draw-then-play wild card
|
||
if (showDrawPlayWildPicker) {
|
||
ColorPickerDialog(
|
||
flipped = gameState.flipped,
|
||
onColorSelected = { color ->
|
||
showDrawPlayWildPicker = false
|
||
val state = pendingDrawStateForWild!!
|
||
val cardIdx = pendingDrawCardIdxForWild
|
||
pendingDrawStateForWild = null
|
||
if (cardIdx >= 0) {
|
||
val result = engine.playCard(state, myPlayerId, cardIdx, color)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> updateState(result.state)
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
}
|
||
},
|
||
onDismiss = {
|
||
showDrawPlayWildPicker = false
|
||
// Fall back to skip on dismiss
|
||
val state = pendingDrawStateForWild!!
|
||
val result = engine.skipAfterDraw(state)
|
||
if (result is GameEngine.PlayResult.Success) updateState(result.state)
|
||
pendingDrawStateForWild = null
|
||
}
|
||
)
|
||
}
|
||
|
||
// 7-0 swap target picker
|
||
if (showSwapTargetPicker && pendingSwapState != null) {
|
||
val otherPlayers = gameState.players.filter { it.id != myPlayerId }
|
||
AlertDialog(
|
||
onDismissRequest = {
|
||
// Cancel swap, just finish the turn
|
||
if (pendingSwapState != null) {
|
||
gameState = pendingSwapState!!
|
||
updateState(gameState)
|
||
showSwapTargetPicker = false
|
||
}
|
||
},
|
||
title = { Text("选择交换对象", color = GoldAccent) },
|
||
text = {
|
||
Column {
|
||
Text("选择要交换手牌的玩家:", color = Color.White.copy(alpha = 0.7f), fontSize = 14.sp)
|
||
Spacer(modifier = Modifier.height(8.dp))
|
||
otherPlayers.forEach { p ->
|
||
TextButton(
|
||
onClick = {
|
||
val state = pendingSwapState!!
|
||
showSwapTargetPicker = false
|
||
pendingSwapState = null
|
||
val result = engine.finishSwap(state, myPlayerId, p.id)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> updateState(result.state)
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
},
|
||
modifier = Modifier.fillMaxWidth()
|
||
) {
|
||
Icon(Icons.Default.Person, null, tint = GoldAccent, modifier = Modifier.size(18.dp))
|
||
Spacer(modifier = Modifier.width(8.dp))
|
||
Text(p.name, color = Color.White, fontSize = 14.sp)
|
||
}
|
||
}
|
||
}
|
||
},
|
||
confirmButton = {
|
||
TextButton(onClick = {
|
||
if (pendingSwapState != null) {
|
||
gameState = pendingSwapState!!
|
||
updateState(gameState)
|
||
}
|
||
showSwapTargetPicker = false
|
||
}) { Text("取消", color = Color.White.copy(alpha = 0.5f)) }
|
||
},
|
||
containerColor = DarkSurface
|
||
)
|
||
}
|
||
|
||
if (isGameOver) {
|
||
GameOverScreen(
|
||
winnerName = winnerName,
|
||
isYouWinner = isYouWinner,
|
||
onBackToMenu = onBackToMenu,
|
||
points = gamePoints,
|
||
difficulty = gameDifficulty,
|
||
duration = gameDuration,
|
||
turnNumber = gameTurnNumber,
|
||
playerCount = totalPlayers
|
||
)
|
||
} else {
|
||
GameScreen(
|
||
gameState = gameState,
|
||
myCards = displayCards,
|
||
myPlayerId = myPlayerId,
|
||
isMyTurn = isMyTurn,
|
||
errorMessage = errorMessage,
|
||
handOffset = handOffset,
|
||
isSevenZeroMode = mode == GameMode.SEVEN_ZERO,
|
||
onPlayCard = { index -> executePlay(index, selectedWildColor) },
|
||
onPlaySeven = { index -> executePlay(index, null) },
|
||
onDrawCard = { executeDraw() },
|
||
onChooseColor = { selectedWildColor = it },
|
||
onShowLog = { showLogDialog = true },
|
||
onCallUno = {
|
||
// Mark player as having called UNO
|
||
val updated = gameState.players.map {
|
||
if (it.id == myPlayerId) it.copy(calledUno = true) else it
|
||
}
|
||
gameState = gameState.copy(players = updated)
|
||
},
|
||
onChallengeUno = { targetId ->
|
||
val result = engine.challengeUno(gameState, myPlayerId, targetId)
|
||
when (result) {
|
||
is GameEngine.PlayResult.Success -> updateState(result.state)
|
||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||
}
|
||
}
|
||
)
|
||
}
|
||
}
|