package com.unogame.ui.screens import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape 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.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight 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.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? = "" ) 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 = "" ) { 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 ) ) saveScores(context, scores.sortedByDescending { it.points }.take(50)) } } @Composable fun LocalGameScreen( totalPlayers: Int, humanPlayerName: String, mode: GameMode, 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) } val gameStartTime = remember { System.currentTimeMillis() } val players = remember { val botNames = pickUniqueBotNames(totalPlayers - 1) 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 = botNames[i], 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(", ") Scoreboard.addEntry( context = context, name = humanPlayerName, mode = mode.displayName, points = gamePoints, difficulty = gameDifficulty, duration = gameDuration, playerCount = totalPlayers, turnNumber = gameTurnNumber, scoreDetail = detailStr ) } } } 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 ) } // Game log dialog if (showLogDialog) { AlertDialog( onDismissRequest = { showLogDialog = false }, title = { Text("出牌记录", color = GoldAccent, fontWeight = FontWeight.Bold) }, text = { Column( modifier = Modifier .heightIn(max = 400.dp) .verticalScroll(rememberScrollState()) ) { if (gameLog.isEmpty()) { Text("暂无记录", color = Color.White.copy(alpha = 0.5f)) } gameLog.forEachIndexed { index, msg -> Row(modifier = Modifier.padding(vertical = 2.dp)) { Text( "${gameLog.size - index}. ", color = Color.White.copy(alpha = 0.3f), fontSize = 12.sp ) Text( msg, color = Color.White.copy(alpha = 0.8f), fontSize = 12.sp ) } } } }, confirmButton = { TextButton(onClick = { showLogDialog = false }) { Text("关闭", color = GoldAccent) } }, containerColor = DarkSurface ) } // 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, 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 } } ) } }