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 { val shuffled = BOT_NAMES.shuffled() val names = mutableListOf() 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 { val json = context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE) .getString(PREFS_KEY, null) ?: return emptyList() return try { gson.fromJson(json, object : TypeToken>() {}.type) ?: emptyList() } catch (_: Exception) { emptyList() } } fun saveScores(context: Context, scores: List) { 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 = 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() 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(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(null) } var pendingDrawnCardIndex by remember { mutableIntStateOf(-1) } var pendingDrawIsWild by remember { mutableStateOf(false) } var showDrawPlayWildPicker by remember { mutableStateOf(false) } var pendingDrawStateForWild by remember { mutableStateOf(null) } var pendingDrawCardIdxForWild by remember { mutableIntStateOf(-1) } var gameLog by remember { mutableStateOf(listOf()) } var showLogDialog by remember { mutableStateOf(false) } var showSwapTargetPicker by remember { mutableStateOf(false) } var pendingSwapState by remember { mutableStateOf(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? { 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 } } ) } }