490 lines
20 KiB
Kotlin
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
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|