UnoGame/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt

490 lines
20 KiB
Kotlin

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<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? = ""
)
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 = ""
) {
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<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 = 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<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(", ")
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
}
}
)
}
}