fix: 修复7-0交换手牌bug、Flip卡面匹配bug,新增加摸牌后可出牌弹窗、出牌记录查看、手牌上限设置、昵称实时保存
This commit is contained in:
parent
d201ae48eb
commit
8ac938cb2b
@ -377,6 +377,7 @@ fun UnoApp() {
|
|||||||
LocalSetupScreen(
|
LocalSetupScreen(
|
||||||
playerName = savedName,
|
playerName = savedName,
|
||||||
modeDisplayName = mode.displayName,
|
modeDisplayName = mode.displayName,
|
||||||
|
mode = mode,
|
||||||
onStartGame = { totalPlayers, name ->
|
onStartGame = { totalPlayers, name ->
|
||||||
if (name.isNotEmpty()) {
|
if (name.isNotEmpty()) {
|
||||||
savedName = name
|
savedName = name
|
||||||
|
|||||||
@ -122,7 +122,10 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class PlayResult {
|
sealed class PlayResult {
|
||||||
data class Success(val state: GameState) : PlayResult()
|
data class Success(
|
||||||
|
val state: GameState,
|
||||||
|
val drawnCardPlayableIndex: Int = -1
|
||||||
|
) : PlayResult()
|
||||||
data class Error(val message: String) : PlayResult()
|
data class Error(val message: String) : PlayResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,15 +304,13 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
sevenZeroDone = true
|
sevenZeroDone = true
|
||||||
} else if (pendingDraw == -2) {
|
} else if (pendingDraw == -2) {
|
||||||
// 7: swap hand with the next player
|
// 7: swap hand with the next player
|
||||||
val nextIdx = advanceIndex(state, state.nextPlayerIndex())
|
val nextIdx = state.nextPlayerIndex()
|
||||||
if (nextIdx != playerIndex) {
|
val nextCards = state.players[nextIdx].cards
|
||||||
val nextCards = state.players[nextIdx].cards
|
modifiedPlayers = state.players.mapIndexed { i, p ->
|
||||||
modifiedPlayers = state.players.mapIndexed { i, p ->
|
when (i) {
|
||||||
when (i) {
|
playerIndex -> p.copy(cards = nextCards, cardCount = nextCards.size)
|
||||||
playerIndex -> p.copy(cards = nextCards, cardCount = nextCards.size)
|
nextIdx -> p.copy(cards = newCards, cardCount = newCards.size)
|
||||||
nextIdx -> p.copy(cards = newCards, cardCount = newCards.size)
|
else -> p
|
||||||
else -> p
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingDraw = 0
|
pendingDraw = 0
|
||||||
@ -355,7 +356,7 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
}
|
}
|
||||||
|
|
||||||
val calledUno = newCards.size == 1
|
val calledUno = newCards.size == 1
|
||||||
val basePlayers = if (activeCard.type == CardType.SKIP_ALL) modifiedPlayers else state.players
|
val basePlayers = if (activeCard.type == CardType.SKIP_ALL || sevenZeroDone) modifiedPlayers else state.players
|
||||||
val updatedPlayers = basePlayers.mapIndexed { i, p ->
|
val updatedPlayers = basePlayers.mapIndexed { i, p ->
|
||||||
when (i) {
|
when (i) {
|
||||||
playerIndex -> p.copy(cards = if (sevenZeroDone) p.cards else newCards, isCurrentTurn = false, calledUno = calledUno, cardCount = if (sevenZeroDone) p.cards.size else newCards.size)
|
playerIndex -> p.copy(cards = if (sevenZeroDone) p.cards else newCards, isCurrentTurn = false, calledUno = calledUno, cardCount = if (sevenZeroDone) p.cards.size else newCards.size)
|
||||||
@ -387,11 +388,12 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
val player = state.players[playerIndex]
|
val player = state.players[playerIndex]
|
||||||
var drawAmount = if (state.pendingDrawCount > 0) state.pendingDrawCount else 1
|
var drawAmount = if (state.pendingDrawCount > 0) state.pendingDrawCount else 1
|
||||||
|
|
||||||
// No Mercy 10-card limit: cap draw to not exceed 10
|
// Hand size limit: cap draw to not exceed maxHandSize
|
||||||
if (rules.mode == GameMode.NO_MERCY) {
|
val handLimit = rules.maxHandSize
|
||||||
val remaining = (10 - player.cards.size).coerceAtLeast(0)
|
if (handLimit > 0) {
|
||||||
|
val remaining = (handLimit - player.cards.size).coerceAtLeast(0)
|
||||||
if (remaining == 0) {
|
if (remaining == 0) {
|
||||||
val message = "${player.name} 手牌已满10张,跳过摸牌"
|
val message = "${player.name} 手牌已满${handLimit}张,跳过摸牌"
|
||||||
val nextIndex = state.nextPlayerIndex()
|
val nextIndex = state.nextPlayerIndex()
|
||||||
val updatedPlayers = state.players.mapIndexed { i, p ->
|
val updatedPlayers = state.players.mapIndexed { i, p ->
|
||||||
when (i) {
|
when (i) {
|
||||||
@ -432,7 +434,7 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
|
|
||||||
val message = if (state.pendingDrawCount > 0) {
|
val message = if (state.pendingDrawCount > 0) {
|
||||||
if (actualDraw < drawAmount && actualDraw < state.pendingDrawCount)
|
if (actualDraw < drawAmount && actualDraw < state.pendingDrawCount)
|
||||||
"${player.name} 摸了 ${drawnCards.size} 张惩罚牌(已达10张上限)"
|
"${player.name} 摸了 ${drawnCards.size} 张惩罚牌(已达${handLimit}张上限)"
|
||||||
else if (actualDraw < drawAmount)
|
else if (actualDraw < drawAmount)
|
||||||
"${player.name} 摸了 ${drawnCards.size} 张惩罚牌(牌不够,需${drawAmount}张)"
|
"${player.name} 摸了 ${drawnCards.size} 张惩罚牌(牌不够,需${drawAmount}张)"
|
||||||
else
|
else
|
||||||
@ -441,13 +443,60 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
"${player.name} 摸了一张牌"
|
"${player.name} 摸了一张牌"
|
||||||
}
|
}
|
||||||
|
|
||||||
val updatedPlayers = state.players.mapIndexed { i, p ->
|
// For voluntary draws, check if the drawn card can be played
|
||||||
when (i) {
|
var drawnCardPlayableIndex = -1
|
||||||
playerIndex -> p.copy(cards = newCards, isCurrentTurn = false, cardCount = newCards.size)
|
val wasVoluntary = state.pendingDrawCount == 0
|
||||||
else -> if (i == nextIndex) p.copy(isCurrentTurn = true) else p.copy(isCurrentTurn = false)
|
val topCard = discardPile.lastOrNull()
|
||||||
|
if (wasVoluntary && drawnCards.isNotEmpty() && topCard != null) {
|
||||||
|
val wildColor = state.currentWildColor
|
||||||
|
val flipped = state.flipped
|
||||||
|
val lastDrawn = drawnCards.last()
|
||||||
|
if (lastDrawn.matches(topCard, wildColor, flipped)) {
|
||||||
|
drawnCardPlayableIndex = newCards.size - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val updatedPlayers = state.players.mapIndexed { i, p ->
|
||||||
|
when (i) {
|
||||||
|
playerIndex -> p.copy(
|
||||||
|
cards = newCards,
|
||||||
|
isCurrentTurn = drawnCardPlayableIndex >= 0,
|
||||||
|
cardCount = newCards.size
|
||||||
|
)
|
||||||
|
else -> if (!wasVoluntary || drawnCardPlayableIndex < 0) {
|
||||||
|
if (i == nextIndex) p.copy(isCurrentTurn = true) else p.copy(isCurrentTurn = false)
|
||||||
|
} else {
|
||||||
|
p.copy(isCurrentTurn = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalNextIndex = if (drawnCardPlayableIndex >= 0) playerIndex else nextIndex
|
||||||
|
|
||||||
|
return PlayResult.Success(
|
||||||
|
state = state.copy(
|
||||||
|
players = updatedPlayers,
|
||||||
|
currentPlayerIndex = finalNextIndex,
|
||||||
|
discardPile = discardPile.toList(),
|
||||||
|
drawPileCount = drawPile.size,
|
||||||
|
currentWildColor = state.currentWildColor,
|
||||||
|
pendingDrawCount = if (drawnCardPlayableIndex >= 0) state.pendingDrawCount else 0,
|
||||||
|
flipped = state.flipped,
|
||||||
|
turnNumber = state.turnNumber + 1,
|
||||||
|
message = message
|
||||||
|
),
|
||||||
|
drawnCardPlayableIndex = drawnCardPlayableIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun skipAfterDraw(state: GameState): PlayResult {
|
||||||
|
val nextIndex = state.nextPlayerIndex()
|
||||||
|
val updatedPlayers = state.players.mapIndexed { i, p ->
|
||||||
|
when (i) {
|
||||||
|
state.currentPlayerIndex -> p.copy(isCurrentTurn = false)
|
||||||
|
else -> if (i == nextIndex) p.copy(isCurrentTurn = true) else p.copy(isCurrentTurn = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
return PlayResult.Success(
|
return PlayResult.Success(
|
||||||
state.copy(
|
state.copy(
|
||||||
players = updatedPlayers,
|
players = updatedPlayers,
|
||||||
@ -458,7 +507,7 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
pendingDrawCount = 0,
|
pendingDrawCount = 0,
|
||||||
flipped = state.flipped,
|
flipped = state.flipped,
|
||||||
turnNumber = state.turnNumber + 1,
|
turnNumber = state.turnNumber + 1,
|
||||||
message = message
|
message = "${state.currentPlayer.name} 选择不出牌"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -491,9 +540,9 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
if (target.cardCount != 1) return PlayResult.Error("该玩家不止1张牌")
|
if (target.cardCount != 1) return PlayResult.Error("该玩家不止1张牌")
|
||||||
if (target.calledUno) return PlayResult.Error("该玩家已经喊过UNO了")
|
if (target.calledUno) return PlayResult.Error("该玩家已经喊过UNO了")
|
||||||
var drawPenalty = 2
|
var drawPenalty = 2
|
||||||
// No Mercy 10-card limit: skip penalty if at limit, cap if near limit
|
val handLimit = rules.maxHandSize
|
||||||
if (rules.mode == GameMode.NO_MERCY) {
|
if (handLimit > 0) {
|
||||||
val remaining = (10 - target.cards.size).coerceAtLeast(0)
|
val remaining = (handLimit - target.cards.size).coerceAtLeast(0)
|
||||||
if (remaining == 0) {
|
if (remaining == 0) {
|
||||||
val updatedPlayers = state.players.mapIndexed { i, p ->
|
val updatedPlayers = state.players.mapIndexed { i, p ->
|
||||||
if (i == targetIdx) p.copy(calledUno = true) else p
|
if (i == targetIdx) p.copy(calledUno = true) else p
|
||||||
@ -502,7 +551,7 @@ class GameEngine(private val rules: GameRules = GameRules.forMode(GameMode.NORMA
|
|||||||
players = updatedPlayers,
|
players = updatedPlayers,
|
||||||
discardPile = discardPile.toList(),
|
discardPile = discardPile.toList(),
|
||||||
drawPileCount = drawPile.size,
|
drawPileCount = drawPile.size,
|
||||||
message = "${state.players[challengerIdx].name} 抓住了 ${target.name} 没喊UNO!(手牌已满10张,免罚)",
|
message = "${state.players[challengerIdx].name} 抓住了 ${target.name} 没喊UNO!(手牌已满${handLimit}张,免罚)",
|
||||||
turnNumber = state.turnNumber + 1
|
turnNumber = state.turnNumber + 1
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.unogame.game
|
package com.unogame.game
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.unogame.model.*
|
import com.unogame.model.*
|
||||||
|
|
||||||
enum class GameMode(val displayName: String) {
|
enum class GameMode(val displayName: String) {
|
||||||
@ -15,9 +16,12 @@ data class GameRules(
|
|||||||
val allowStacking: Boolean,
|
val allowStacking: Boolean,
|
||||||
val colors: List<CardColor>,
|
val colors: List<CardColor>,
|
||||||
val usesFlip: Boolean,
|
val usesFlip: Boolean,
|
||||||
val usesNoMercyCards: Boolean
|
val usesNoMercyCards: Boolean,
|
||||||
|
val maxHandSize: Int = 0 // 0 = no limit
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val PREF_KEY_PREFIX = "handsize_"
|
||||||
|
|
||||||
fun forMode(mode: GameMode): GameRules = when (mode) {
|
fun forMode(mode: GameMode): GameRules = when (mode) {
|
||||||
GameMode.NORMAL -> GameRules(
|
GameMode.NORMAL -> GameRules(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
@ -52,5 +56,24 @@ data class GameRules(
|
|||||||
usesNoMercyCards = false
|
usesNoMercyCards = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun forMode(mode: GameMode, maxHandSize: Int): GameRules {
|
||||||
|
return forMode(mode).copy(maxHandSize = maxHandSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadMaxHandSize(context: Context, mode: GameMode): Int {
|
||||||
|
return context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getInt(PREF_KEY_PREFIX + mode.name, defaultMaxHandSize(mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveMaxHandSize(context: Context, mode: GameMode, size: Int) {
|
||||||
|
context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit().putInt(PREF_KEY_PREFIX + mode.name, size).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultMaxHandSize(mode: GameMode): Int = when (mode) {
|
||||||
|
GameMode.NO_MERCY -> 10
|
||||||
|
else -> 0 // no limit for other modes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ data class Card(
|
|||||||
if (c.color == CardColor.WILD) return true
|
if (c.color == CardColor.WILD) return true
|
||||||
if (o.color == CardColor.WILD)
|
if (o.color == CardColor.WILD)
|
||||||
return currentWildColor == null || c.color == currentWildColor
|
return currentWildColor == null || c.color == currentWildColor
|
||||||
|
if (c.type == CardType.FLIP || o.type == CardType.FLIP) return true
|
||||||
if (c.color == o.color) return true
|
if (c.color == o.color) return true
|
||||||
if (c.type == CardType.NUMBER && o.type == CardType.NUMBER && c.number == o.number)
|
if (c.type == CardType.NUMBER && o.type == CardType.NUMBER && c.number == o.number)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -33,6 +33,7 @@ fun GameScreen(
|
|||||||
onChooseColor: (CardColor) -> Unit,
|
onChooseColor: (CardColor) -> Unit,
|
||||||
onCallUno: () -> Unit = {},
|
onCallUno: () -> Unit = {},
|
||||||
onChallengeUno: (String) -> Unit = {},
|
onChallengeUno: (String) -> Unit = {},
|
||||||
|
onShowLog: () -> Unit = {},
|
||||||
errorMessage: String
|
errorMessage: String
|
||||||
) {
|
) {
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@ -62,17 +63,29 @@ fun GameScreen(
|
|||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp)
|
||||||
|
.clickable { onShowLog() },
|
||||||
colors = CardDefaults.cardColors(containerColor = DarkSurface),
|
colors = CardDefaults.cardColors(containerColor = DarkSurface),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Row(
|
||||||
text = gameState.message,
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
color = GoldAccent,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
fontSize = 14.sp,
|
) {
|
||||||
fontWeight = FontWeight.Medium,
|
Text(
|
||||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
text = gameState.message,
|
||||||
)
|
color = GoldAccent,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
Icons.Default.List,
|
||||||
|
null,
|
||||||
|
tint = Color.White.copy(alpha = 0.4f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,22 @@
|
|||||||
package com.unogame.ui.screens
|
package com.unogame.ui.screens
|
||||||
|
|
||||||
import android.content.Context
|
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.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.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.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.unogame.game.AIDifficulty
|
import com.unogame.game.AIDifficulty
|
||||||
@ -11,6 +25,7 @@ import com.unogame.game.GameMode
|
|||||||
import com.unogame.game.GameRules
|
import com.unogame.game.GameRules
|
||||||
import com.unogame.game.SimpleAI
|
import com.unogame.game.SimpleAI
|
||||||
import com.unogame.model.*
|
import com.unogame.model.*
|
||||||
|
import com.unogame.ui.theme.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -78,7 +93,7 @@ fun LocalGameScreen(
|
|||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val rules = remember { GameRules.forMode(mode) }
|
val rules = remember { GameRules.forMode(mode, GameRules.loadMaxHandSize(context, mode)) }
|
||||||
val engine = remember { GameEngine(rules) }
|
val engine = remember { GameEngine(rules) }
|
||||||
val aiDiff = remember { AIDifficulty.load(context) }
|
val aiDiff = remember { AIDifficulty.load(context) }
|
||||||
val gameStartTime = remember { System.currentTimeMillis() }
|
val gameStartTime = remember { System.currentTimeMillis() }
|
||||||
@ -104,6 +119,11 @@ fun LocalGameScreen(
|
|||||||
var gameDuration by remember { mutableIntStateOf(0) }
|
var gameDuration by remember { mutableIntStateOf(0) }
|
||||||
var gameDifficulty by remember { mutableStateOf("") }
|
var gameDifficulty by remember { mutableStateOf("") }
|
||||||
var gameTurnNumber by remember { mutableIntStateOf(0) }
|
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 gameLog by remember { mutableStateOf(listOf<String>()) }
|
||||||
|
var showLogDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val myPlayerId = "human"
|
val myPlayerId = "human"
|
||||||
val currentPlayer = gameState.currentPlayer
|
val currentPlayer = gameState.currentPlayer
|
||||||
@ -113,6 +133,9 @@ fun LocalGameScreen(
|
|||||||
gameState = state
|
gameState = state
|
||||||
myCards = state.players.find { it.id == myPlayerId }?.cards ?: myCards
|
myCards = state.players.find { it.id == myPlayerId }?.cards ?: myCards
|
||||||
errorMessage = ""
|
errorMessage = ""
|
||||||
|
if (state.message.isNotEmpty()) {
|
||||||
|
gameLog = gameLog + state.message
|
||||||
|
}
|
||||||
if (state.isGameOver) {
|
if (state.isGameOver) {
|
||||||
isGameOver = true
|
isGameOver = true
|
||||||
winnerName = state.winner?.name ?: ""
|
winnerName = state.winner?.name ?: ""
|
||||||
@ -153,7 +176,15 @@ fun LocalGameScreen(
|
|||||||
fun executeDraw() {
|
fun executeDraw() {
|
||||||
val result = engine.drawCard(gameState, myPlayerId)
|
val result = engine.drawCard(gameState, myPlayerId)
|
||||||
when (result) {
|
when (result) {
|
||||||
is GameEngine.PlayResult.Success -> updateState(result.state)
|
is GameEngine.PlayResult.Success -> {
|
||||||
|
if (result.drawnCardPlayableIndex >= 0) {
|
||||||
|
pendingDrawState = result.state
|
||||||
|
pendingDrawnCardIndex = result.drawnCardPlayableIndex
|
||||||
|
showDrawPlayPopup = true
|
||||||
|
} else {
|
||||||
|
updateState(result.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
is GameEngine.PlayResult.Error -> errorMessage = result.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -177,7 +208,19 @@ fun LocalGameScreen(
|
|||||||
}
|
}
|
||||||
SimpleAI.MoveType.DRAW -> {
|
SimpleAI.MoveType.DRAW -> {
|
||||||
val drawResult = engine.drawCard(gameState, current.id)
|
val drawResult = engine.drawCard(gameState, current.id)
|
||||||
if (drawResult is GameEngine.PlayResult.Success) updateState(drawResult.state)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,6 +231,83 @@ fun LocalGameScreen(
|
|||||||
myCards.map { it.activeCard(gameState.flipped) }
|
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
|
||||||
|
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(
|
||||||
|
"${index + 1}. ",
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isGameOver) {
|
if (isGameOver) {
|
||||||
GameOverScreen(
|
GameOverScreen(
|
||||||
winnerName = winnerName,
|
winnerName = winnerName,
|
||||||
@ -209,6 +329,7 @@ fun LocalGameScreen(
|
|||||||
onPlayCard = { index -> executePlay(index, selectedWildColor) },
|
onPlayCard = { index -> executePlay(index, selectedWildColor) },
|
||||||
onDrawCard = { executeDraw() },
|
onDrawCard = { executeDraw() },
|
||||||
onChooseColor = { selectedWildColor = it },
|
onChooseColor = { selectedWildColor = it },
|
||||||
|
onShowLog = { showLogDialog = true },
|
||||||
onCallUno = {
|
onCallUno = {
|
||||||
// Mark player as having called UNO
|
// Mark player as having called UNO
|
||||||
val updated = gameState.players.map {
|
val updated = gameState.players.map {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import com.unogame.ui.theme.*
|
|||||||
fun LocalSetupScreen(
|
fun LocalSetupScreen(
|
||||||
playerName: String,
|
playerName: String,
|
||||||
modeDisplayName: String,
|
modeDisplayName: String,
|
||||||
|
mode: com.unogame.game.GameMode,
|
||||||
onStartGame: (Int, String) -> Unit,
|
onStartGame: (Int, String) -> Unit,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
@ -33,6 +34,8 @@ fun LocalSetupScreen(
|
|||||||
var name by remember { mutableStateOf(playerName) }
|
var name by remember { mutableStateOf(playerName) }
|
||||||
var difficulty by remember { mutableStateOf(com.unogame.game.AIDifficulty.NORMAL) }
|
var difficulty by remember { mutableStateOf(com.unogame.game.AIDifficulty.NORMAL) }
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val initMaxHandSize = remember { com.unogame.game.GameRules.loadMaxHandSize(context, mode) }
|
||||||
|
var maxHandSize by remember { mutableIntStateOf(initMaxHandSize) }
|
||||||
|
|
||||||
val botNames = listOf("🤖 机器人1", "🤖 机器人2", "🤖 机器人3")
|
val botNames = listOf("🤖 机器人1", "🤖 机器人2", "🤖 机器人3")
|
||||||
|
|
||||||
@ -152,6 +155,41 @@ fun LocalSetupScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(28.dp))
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
|
// Hand size limit
|
||||||
|
Text("手牌上限", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Slider(
|
||||||
|
value = maxHandSize.toFloat(),
|
||||||
|
onValueChange = { maxHandSize = it.toInt() },
|
||||||
|
valueRange = 0f..20f,
|
||||||
|
steps = 19,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = GoldAccent,
|
||||||
|
activeTrackColor = GoldAccent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (maxHandSize == 0) "无限制" else "${maxHandSize}张",
|
||||||
|
color = GoldAccent,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.width(56.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"0 = 无限制,默认各模式不限制,无情模式默认10张上限",
|
||||||
|
color = Color.White.copy(alpha = 0.35f),
|
||||||
|
fontSize = 11.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
|
||||||
// Player list preview
|
// Player list preview
|
||||||
Text("参与者预览", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
|
Text("参与者预览", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
@ -178,6 +216,7 @@ fun LocalSetupScreen(
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
com.unogame.game.AIDifficulty.save(context, difficulty)
|
com.unogame.game.AIDifficulty.save(context, difficulty)
|
||||||
|
com.unogame.game.GameRules.saveMaxHandSize(context, mode, maxHandSize)
|
||||||
onStartGame(totalPlayers, name.ifBlank { "玩家" })
|
onStartGame(totalPlayers, name.ifBlank { "玩家" })
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
|||||||
@ -74,7 +74,10 @@ fun MainMenuScreen(
|
|||||||
// Name input
|
// Name input
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = playerName,
|
value = playerName,
|
||||||
onValueChange = { playerName = it.take(10) },
|
onValueChange = {
|
||||||
|
playerName = it.take(10)
|
||||||
|
onNameChanged(playerName.ifBlank { "玩家" })
|
||||||
|
},
|
||||||
label = { Text("你的昵称", color = Color.White.copy(alpha = 0.6f)) },
|
label = { Text("你的昵称", color = Color.White.copy(alpha = 0.6f)) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
colors = OutlinedTextFieldDefaults.colors(
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
|||||||
@ -147,7 +147,11 @@ fun RulesHelpScreen(onBack: () -> Unit) {
|
|||||||
Label("UNO叫牌与抓人")
|
Label("UNO叫牌与抓人")
|
||||||
Bullet("出牌后手牌剩1张时,红色\"喊UNO!\"按钮出现,必须点击")
|
Bullet("出牌后手牌剩1张时,红色\"喊UNO!\"按钮出现,必须点击")
|
||||||
Bullet("未喊且被其他玩家\"抓XX!\",罚抽2张")
|
Bullet("未喊且被其他玩家\"抓XX!\",罚抽2张")
|
||||||
Bullet("若已被抓过或手牌已满10张(无情模式),免罚")
|
Bullet("若已被抓过或手牌已满上限(无情模式),免罚")
|
||||||
|
SpacerH(8)
|
||||||
|
Label("操作提示")
|
||||||
|
Bullet("长按手牌可查看牌面说明与分数")
|
||||||
|
Bullet("点击出牌记录可以查看完整出牌历史")
|
||||||
SpacerH(8)
|
SpacerH(8)
|
||||||
Label("游戏结束与计分")
|
Label("游戏结束与计分")
|
||||||
Bullet("先出完手牌者获胜,单局结束")
|
Bullet("先出完手牌者获胜,单局结束")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user