From 548fbef889c269d0c91edeca116ff93de5ae2fad Mon Sep 17 00:00:00 2001 From: flykhan Date: Sun, 26 Apr 2026 17:17:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=207-0=E8=87=AA=E7=94=B1=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E4=BA=A4=E6=8D=A2=E5=AF=B9=E8=B1=A1=E3=80=81=E8=A7=A3=E9=99=A4?= =?UTF-8?q?=E6=AD=BB=E9=94=81=E3=80=81=E5=87=BA=E7=89=8C=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E3=80=81=E4=B8=87=E8=83=BD=E7=89=8C=E6=91=B8?= =?UTF-8?q?=E7=89=8C=E9=80=89=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/unogame/MainActivity.kt | 1 + .../main/java/com/unogame/game/GameEngine.kt | 140 ++++++++++++++++-- .../main/java/com/unogame/game/SimpleAI.kt | 27 +++- .../java/com/unogame/ui/screens/GameScreen.kt | 4 + .../com/unogame/ui/screens/LocalGameScreen.kt | 136 +++++++++++++++-- 5 files changed, 279 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/unogame/MainActivity.kt b/app/src/main/java/com/unogame/MainActivity.kt index 2ca722e..9734028 100644 --- a/app/src/main/java/com/unogame/MainActivity.kt +++ b/app/src/main/java/com/unogame/MainActivity.kt @@ -432,6 +432,7 @@ fun UnoApp() { myPlayerId = myPlayerId, isMyTurn = state.currentPlayer.id == myPlayerId, errorMessage = errorMessage, + isSevenZeroMode = false, onPlayCard = { index -> errorMessage = "" val colorToSend = if (index >= 0 && index < myCards.size && myCards[index].type.isWild) { diff --git a/app/src/main/java/com/unogame/game/GameEngine.kt b/app/src/main/java/com/unogame/game/GameEngine.kt index 0b4b9ed..c2de12a 100644 --- a/app/src/main/java/com/unogame/game/GameEngine.kt +++ b/app/src/main/java/com/unogame/game/GameEngine.kt @@ -124,7 +124,8 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA sealed class PlayResult { data class Success( val state: GameState, - val drawnCardPlayableIndex: Int = -1 + val drawnCardPlayableIndex: Int = -1, + val needsSwapTarget: Boolean = false ) : PlayResult() data class Error(val message: String) : PlayResult() } @@ -133,7 +134,8 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA state: GameState, playerId: String, cardIndex: Int, - chosenColor: CardColor? = null + chosenColor: CardColor? = null, + swapTargetId: String? = null ): PlayResult { val playerIndex = state.players.indexOfFirst { it.id == playerId } if (playerIndex != state.currentPlayerIndex) @@ -162,7 +164,7 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA !card.matches(topCard, state.currentWildColor, state.flipped)) return PlayResult.Error("不能出这张牌") - return executeCardPlay(state, player, playerIndex, cardIndex, card, chosenColor, canStack) + return executeCardPlay(state, player, playerIndex, cardIndex, card, chosenColor, canStack, swapTargetId) } private fun executeCardPlay( @@ -172,7 +174,8 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA cardIndex: Int, card: Card, chosenColor: CardColor?, - isStacking: Boolean + isStacking: Boolean, + swapTargetId: String? = null ): PlayResult { val newDiscard = discardPile.toMutableList() newDiscard.add(card) @@ -187,7 +190,8 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA var nextIndex = state.nextPlayerIndex() var direction = state.direction var flipped = state.flipped - var message = "${player.name} 出了 ${card.displayText}" + val cardColorText = if (card.color == CardColor.WILD) "万能" else card.color.displayName + var message = "${player.name} 出了 ${cardColorText} ${card.displayText}" val activeCard = card.activeCard(flipped) @@ -303,13 +307,38 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA pendingDraw = 0 sevenZeroDone = true } else if (pendingDraw == -2) { - // 7: swap hand with the next player - val nextIdx = state.nextPlayerIndex() - val nextCards = state.players[nextIdx].cards + // 7: swap hand with player of choice + val targetIdx = if (swapTargetId != null) { + state.players.indexOfFirst { it.id == swapTargetId } + } else { + state.nextPlayerIndex() + } + // If target not specified and more than 2 players, need UI to choose + if (swapTargetId == null && state.players.size > 2) { + // Return state with 7 removed but no swap yet, flag for target selection + val partialPlayers = state.players.mapIndexed { i, p -> + if (i == playerIndex) p.copy(cards = newCards, cardCount = newCards.size) + else p.copy(isCurrentTurn = false) + } + return PlayResult.Success( + state.copy( + players = partialPlayers, + currentPlayerIndex = playerIndex, + discardPile = discardPile.toList(), + drawPileCount = drawPile.size, + pendingDrawCount = 0, + flipped = flipped, + turnNumber = state.turnNumber + 1, + message = message + ",选择交换对象" + ), + needsSwapTarget = true + ) + } + val nextCards = state.players[targetIdx].cards modifiedPlayers = state.players.mapIndexed { i, p -> when (i) { playerIndex -> p.copy(cards = nextCards, cardCount = nextCards.size) - nextIdx -> p.copy(cards = newCards, cardCount = newCards.size) + targetIdx -> p.copy(cards = newCards, cardCount = newCards.size) else -> p } } @@ -424,8 +453,28 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA if (drawPile.isNotEmpty()) drawPile.removeAt(0) else null } - if (drawnCards.isEmpty()) - return PlayResult.Error("牌堆已空,无法摸牌") + if (drawnCards.isEmpty()) { + // Can't draw - just pass turn + val nextIndex = state.nextPlayerIndex() + val updatedPlayers = state.players.mapIndexed { i, p -> + when (i) { + playerIndex -> p.copy(isCurrentTurn = false) + else -> if (i == nextIndex) p.copy(isCurrentTurn = true) else p.copy(isCurrentTurn = false) + } + } + return PlayResult.Success( + state.copy( + players = updatedPlayers, + currentPlayerIndex = nextIndex, + discardPile = discardPile.toList(), + drawPileCount = 0, + pendingDrawCount = if (state.pendingDrawCount > 0) 0 else state.pendingDrawCount, + flipped = state.flipped, + turnNumber = state.turnNumber + 1, + message = "${player.name} 无法摸牌,跳过回合" + ) + ) + } val newCards = player.cards.toMutableList() newCards.addAll(drawnCards) @@ -489,6 +538,69 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA ) } + fun finishSwap(state: GameState, playerId: String, swapTargetId: String): PlayResult { + val playerIndex = state.players.indexOfFirst { it.id == playerId } + val targetIdx = state.players.indexOfFirst { it.id == swapTargetId } + if (playerIndex < 0 || targetIdx < 0) return PlayResult.Error("玩家不存在") + if (playerIndex == targetIdx) return PlayResult.Error("不能和自己交换") + + val currentPlayer = state.players[playerIndex] + val targetPlayer = state.players[targetIdx] + + val updatedPlayers = state.players.mapIndexed { i, p -> + when (i) { + playerIndex -> p.copy( + cards = targetPlayer.cards, + cardCount = targetPlayer.cards.size, + isCurrentTurn = false + ) + targetIdx -> p.copy( + cards = currentPlayer.cards, + cardCount = currentPlayer.cards.size + ) + else -> p.copy(isCurrentTurn = false) + } + } + + val nextIndex = state.nextPlayerIndex() + val updatedWithTurn = updatedPlayers.mapIndexed { i, p -> + if (i == nextIndex) p.copy(isCurrentTurn = true) else p + } + + val isWinner = currentPlayer.cards.isEmpty() + if (isWinner) { + return PlayResult.Success( + state.copy( + players = updatedWithTurn, + isGameOver = true, + winner = currentPlayer.copy(cards = emptyList(), cardCount = 0), + discardPile = discardPile.toList(), + drawPileCount = drawPile.size, + turnNumber = state.turnNumber + 1, + message = "${currentPlayer.name} 与 ${targetPlayer.name} 交换手牌,${currentPlayer.name} 赢了!" + ) + ) + } + + val calledUno = currentPlayer.cards.size == 1 + val finalPlayers = updatedWithTurn.mapIndexed { i, p -> + if (i == playerIndex) p.copy(calledUno = calledUno) else p + } + + return PlayResult.Success( + state.copy( + players = finalPlayers, + currentPlayerIndex = nextIndex, + discardPile = discardPile.toList(), + drawPileCount = drawPile.size, + pendingDrawCount = 0, + flipped = state.flipped, + turnNumber = state.turnNumber + 1, + message = "${currentPlayer.name} 与 ${targetPlayer.name} 交换手牌" + ) + ) + } + fun skipAfterDraw(state: GameState): PlayResult { val nextIndex = state.nextPlayerIndex() val updatedPlayers = state.players.mapIndexed { i, p -> @@ -521,7 +633,13 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA private fun reshuffleDiscard() { if (discardPile.size <= 1 && drawPile.isNotEmpty()) return + if (discardPile.isEmpty()) return val topCard = discardPile.removeAt(discardPile.size - 1) + if (discardPile.isEmpty()) { + // Only the top card exists - put it back, can't reshuffle + discardPile.add(topCard) + return + } drawPile.addAll(discardPile) shuffle(drawPile) discardPile.clear() diff --git a/app/src/main/java/com/unogame/game/SimpleAI.kt b/app/src/main/java/com/unogame/game/SimpleAI.kt index 9b8a7c3..9c71740 100644 --- a/app/src/main/java/com/unogame/game/SimpleAI.kt +++ b/app/src/main/java/com/unogame/game/SimpleAI.kt @@ -30,17 +30,26 @@ object SimpleAI { data class AIMove( val type: MoveType, val cardIndex: Int = -1, - val chosenColor: CardColor? = null + val chosenColor: CardColor? = null, + val swapTargetId: String? = null ) enum class MoveType { PLAY, DRAW } + fun pickSwapTarget(players: List, selfId: String): String? { + val others = players.filter { it.id != selfId } + if (others.isEmpty()) return null + return others.random(Random).id + } + fun decideMove( player: Player, topCard: Card?, wildColor: CardColor?, flipped: Boolean = false, - difficulty: AIDifficulty = AIDifficulty.NORMAL + difficulty: AIDifficulty = AIDifficulty.NORMAL, + otherPlayers: List = emptyList(), + isSevenZero: Boolean = false ): AIMove { val hand = player.cards if (hand.isEmpty()) return AIMove(MoveType.DRAW) @@ -62,11 +71,15 @@ object SimpleAI { // Easy: play random card if (difficulty == AIDifficulty.EASY) { val chosen = playable.random() - if (chosen.second.type.isWild || chosen.second.activeCard(flipped).type.isWild) { + val idx = hand.indexOf(chosen.second) + return if (chosen.second.type.isWild || chosen.second.activeCard(flipped).type.isWild) { val color = pickBestColor(hand, flipped) - return AIMove(MoveType.PLAY, hand.indexOf(chosen.second), color) + AIMove(MoveType.PLAY, idx, color) + } else if (isSevenZero && chosen.second.type == CardType.NUMBER && chosen.second.number == 7) { + AIMove(MoveType.PLAY, idx, swapTargetId = pickSwapTarget(otherPlayers, player.id)) + } else { + AIMove(MoveType.PLAY, idx) } - return AIMove(MoveType.PLAY, hand.indexOf(chosen.second)) } // Hard: prefer cards that hurt the opponent most @@ -106,6 +119,10 @@ object SimpleAI { return AIMove(MoveType.PLAY, hand.indexOf(chosen.second), color) } + if (isSevenZero && chosen.second.type == CardType.NUMBER && chosen.second.number == 7) { + return AIMove(MoveType.PLAY, hand.indexOf(chosen.second), swapTargetId = pickSwapTarget(otherPlayers, player.id)) + } + return AIMove(MoveType.PLAY, hand.indexOf(chosen.second)) } diff --git a/app/src/main/java/com/unogame/ui/screens/GameScreen.kt b/app/src/main/java/com/unogame/ui/screens/GameScreen.kt index cebefb2..e28b74e 100644 --- a/app/src/main/java/com/unogame/ui/screens/GameScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/GameScreen.kt @@ -34,6 +34,8 @@ fun GameScreen( onCallUno: () -> Unit = {}, onChallengeUno: (String) -> Unit = {}, onShowLog: () -> Unit = {}, + onPlaySeven: (Int) -> Unit = {}, + isSevenZeroMode: Boolean = false, errorMessage: String ) { val scrollState = rememberScrollState() @@ -289,6 +291,8 @@ fun GameScreen( if (realCard.type.isWild) { selectedAutoCard = sortedCards.indexOf(realCard) showColorPicker = true + } else if (isSevenZeroMode && realCard.type == CardType.NUMBER && realCard.number == 7 && gameState.players.size > 2) { + onPlaySeven(myCards.indexOf(realCard)) } else { selectedCardIndex = index onPlayCard(myCards.indexOf(realCard)) diff --git a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt index aa803c4..ee7a686 100644 --- a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt @@ -25,6 +25,7 @@ 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 @@ -122,8 +123,14 @@ fun LocalGameScreen( 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 @@ -134,7 +141,7 @@ fun LocalGameScreen( myCards = state.players.find { it.id == myPlayerId }?.cards ?: myCards errorMessage = "" if (state.message.isNotEmpty()) { - gameLog = gameLog + state.message + gameLog = listOf(state.message) + gameLog } if (state.isGameOver) { isGameOver = true @@ -165,10 +172,17 @@ fun LocalGameScreen( } } - fun executePlay(cardIndex: Int, chosenColor: CardColor? = null) { - val result = engine.playCard(gameState, myPlayerId, cardIndex, chosenColor) + 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 -> updateState(result.state) + is GameEngine.PlayResult.Success -> { + if (result.needsSwapTarget) { + pendingSwapState = result.state + showSwapTargetPicker = true + } else { + updateState(result.state) + } + } is GameEngine.PlayResult.Error -> errorMessage = result.message } } @@ -180,6 +194,8 @@ fun LocalGameScreen( 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) @@ -195,10 +211,14 @@ fun LocalGameScreen( if (current.id != myPlayerId) { delay(1200L) - val move = SimpleAI.decideMove(current, gameState.topCard, gameState.currentWildColor, gameState.flipped, aiDiff) + 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) + val result = engine.playCard(gameState, current.id, move.cardIndex, move.chosenColor, move.swapTargetId) if (result is GameEngine.PlayResult.Success) { updateState(result.state) } else { @@ -246,13 +266,22 @@ fun LocalGameScreen( TextButton(onClick = { val state = pendingDrawState!! val cardIdx = pendingDrawnCardIndex - 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 + 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) } @@ -308,6 +337,85 @@ fun LocalGameScreen( ) } + // 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, @@ -326,7 +434,9 @@ fun LocalGameScreen( 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 },