package com.unogame import android.os.Bundle import android.content.pm.ActivityInfo import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavType import androidx.navigation.compose.* import androidx.navigation.navArgument import com.unogame.game.GameMode import com.unogame.model.* import com.unogame.network.* import com.unogame.ui.navigation.Screen import com.unogame.ui.screens.* import com.unogame.ui.theme.* import kotlinx.coroutines.launch import java.net.Inet4Address import java.net.NetworkInterface fun getLocalIpAddress(): String { try { NetworkInterface.getNetworkInterfaces()?.asSequence()?.forEach { iface -> if (iface.isLoopback || !iface.isUp) return@forEach iface.inetAddresses.asSequence() .filter { !it.isLoopbackAddress && it is Inet4Address } .forEach { return it.hostAddress ?: "" } } } catch (_: Exception) {} return "" } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { val darkScheme = darkColorScheme( primary = GoldAccent, secondary = GoldAccent, background = DarkBackground, surface = DarkSurface ) MaterialTheme(colorScheme = darkScheme) { UnoApp() } } } } @Composable fun UnoApp() { val navController = rememberNavController() val scope = rememberCoroutineScope() val context = LocalContext.current val prefs = context.getSharedPreferences("unogame_prefs", android.content.Context.MODE_PRIVATE) // Load saved player name var savedName by remember { mutableStateOf(prefs.getString("player_name", "玩家") ?: "玩家") } var cardTheme by remember { mutableStateOf(CardTheme.load(context)) } var tableBg by remember { mutableStateOf(TableBg.load(context)) } var isLandscape by remember { mutableStateOf(prefs.getBoolean("landscape", false)) } // Apply orientation on start LaunchedEffect(Unit) { (context as? android.app.Activity)?.requestedOrientation = if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } // Apply saved orientation LaunchedEffect(isLandscape) { (context as? android.app.Activity)?.requestedOrientation = if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } // Network state var gameServer by remember { mutableStateOf(null) } var gameClient by remember { mutableStateOf(null) } var discoveryService by remember { mutableStateOf(null) } var isHost by remember { mutableStateOf(false) } var myPlayerId by remember { mutableStateOf("") } var myName by remember { mutableStateOf("") } var errorMessage by remember { mutableStateOf("") } // Game state var gameState by remember { mutableStateOf(null) } var myCards by remember { mutableStateOf>(emptyList()) } var players by remember { mutableStateOf>(emptyList()) } var selectedWildColor by remember { mutableStateOf(null) } // Discovery state var discoveredHosts by remember { mutableStateOf>(emptyList()) } var isDiscovering by remember { mutableStateOf(false) } var isConnecting by remember { mutableStateOf(false) } var connectedHost by remember { mutableStateOf("") } var hostIp by remember { mutableStateOf("") } // Cleanup function fun cleanup() { gameServer?.shutdown() gameClient?.disconnect() discoveryService?.shutdown() gameServer = null gameClient = null discoveryService = null myPlayerId = "" isHost = false errorMessage = "" gameState = null myCards = emptyList() players = emptyList() discoveredHosts = emptyList() isDiscovering = false isConnecting = false connectedHost = "" hostIp = "" } // Listen to client events LaunchedEffect(gameClient) { gameClient?.events?.collect { event -> // Always sync critical state from client (StateFlow events can be lost) val myId = gameClient?.myPlayerId ?: "" if (myId.isNotEmpty() && myPlayerId.isEmpty()) { myPlayerId = myId players = gameClient?.currentPlayers ?: players isConnecting = false connectedHost = "${gameClient?.currentPlayers?.find { it.isHost }?.name ?: "主机"}" } when (event?.type) { "CONNECTED" -> { myPlayerId = event.playerId players = event.players isConnecting = false connectedHost = "${event.players.find { it.isHost }?.name ?: "主机"}" } "PLAYER_JOINED" -> { players = gameClient?.currentPlayers ?: event.players } "PLAYER_LEFT" -> { players = gameClient?.currentPlayers ?: event.players } "GAME_STARTED" -> { gameState = event.gameState myCards = event.playerCards players = event.players selectedWildColor = null navController.navigate(Screen.Game.route) } "GAME_STATE" -> { val prevState = gameState gameState = event.gameState myCards = event.playerCards players = event.players // Navigate to game over if needed if (event.gameState?.isGameOver == true && prevState?.isGameOver != true) { val winner = event.gameState.winner val isYou = winner?.id == myPlayerId navController.navigate( Screen.GameOver.createRoute( winnerName = winner?.name ?: "", isYouWinner = isYou ) ) } } "ERROR" -> { errorMessage = event.message } "DISCONNECTED" -> { errorMessage = event.message navController.navigate(Screen.MainMenu.route) { popUpTo(0) { inclusive = true } } cleanup() } } } } CompositionLocalProvider( LocalCardTheme provides cardTheme, LocalTableBg provides tableBg ) { NavHost( navController = navController, startDestination = Screen.MainMenu.route, modifier = Modifier.fillMaxSize() ) { composable(Screen.MainMenu.route) { MainMenuScreen( initialName = savedName, onNameChanged = { name -> savedName = name prefs.edit().putString("player_name", name).apply() }, onLocalGame = { navController.navigate(Screen.ModeSelect.route) }, onScoreboard = { navController.navigate(Screen.Scoreboard.route) }, onRules = { navController.navigate(Screen.Rules.route) }, currentTheme = cardTheme, currentBg = tableBg, onToggleTheme = { val next = CardTheme.values()[(cardTheme.ordinal + 1) % CardTheme.values().size] cardTheme = next CardTheme.save(context, next) }, onToggleBg = { val next = TableBg.values()[(tableBg.ordinal + 1) % TableBg.values().size] tableBg = next TableBg.save(context, next) }, onToggleOrientation = { isLandscape = !isLandscape prefs.edit().putBoolean("landscape", isLandscape).apply() (context as? android.app.Activity)?.requestedOrientation = if (isLandscape) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_PORTRAIT }, isLandscape = isLandscape, onHostGame = { name -> myName = name isHost = true errorMessage = "" hostIp = getLocalIpAddress() val server = GameServer() if (server.start()) { gameServer = server val discovery = DiscoveryService() discoveryService = discovery discovery.startAdvertising(name, 1) navController.navigate(Screen.Lobby.createRoute(true)) // Host connects as a client too (to localhost) - on IO thread scope.launch { val client = GameClient() if (client.connect("127.0.0.1", name)) { gameClient = client } } } else { errorMessage = "无法创建房间,请检查网络" } }, onJoinGame = { name -> myName = name isHost = false val discovery = DiscoveryService() discoveryService = discovery discovery.startDiscovery(name) isDiscovering = true scope.launch { discovery.hosts.collect { hosts -> discoveredHosts = hosts isDiscovering = hosts.isEmpty() } } navController.navigate(Screen.Lobby.createRoute(false)) } ) } composable( route = Screen.Lobby.route, arguments = listOf(navArgument("isHost") { type = NavType.BoolType }) ) { backStackEntry -> val hostFlag = backStackEntry.arguments?.getBoolean("isHost") ?: false LaunchedEffect(Unit) { if (hostFlag && players.size < 2) { errorMessage = "" } errorMessage = "" } LobbyScreen( isHost = hostFlag, hostIp = hostIp, players = players, discoveredHosts = discoveredHosts, isDiscovering = isDiscovering, isConnecting = isConnecting, connectedHost = connectedHost, errorMessage = errorMessage, onStartGame = { scope.launch { gameClient?.startGame() } }, onJoinHost = { host -> isConnecting = true errorMessage = "" scope.launch { try { val client = GameClient() if (client.connect(host.address, myName)) { gameClient = client discoveryService?.stopDiscovery() } else { errorMessage = "无法连接到 ${host.name} (${host.address}): ${client.lastError}" } } catch (e: Exception) { android.util.Log.e("UnoApp", "加入房间崩溃", e) errorMessage = "连接出错: ${e.message}" } isConnecting = false } }, onRefreshDiscovery = { discoveredHosts = emptyList() isDiscovering = true discoveryService?.stopDiscovery() discoveryService = DiscoveryService() discoveryService?.startDiscovery(myName) scope.launch { discoveryService?.hosts?.collect { hosts -> discoveredHosts = hosts isDiscovering = hosts.isEmpty() } } }, onManualConnect = { ip -> isConnecting = true errorMessage = "" scope.launch { try { val client = GameClient() if (client.connect(ip, myName)) { gameClient = client discoveryService?.stopDiscovery() } else { errorMessage = "无法连接到 $ip: ${client.lastError}" } } catch (e: Exception) { android.util.Log.e("UnoApp", "手动连接崩溃", e) errorMessage = "连接出错: ${e.message}" } isConnecting = false } }, onBack = { cleanup() navController.popBackStack() } ) } composable(Screen.ModeSelect.route) { ModeSelectScreen( playerName = savedName, onStartGame = { mode -> navController.navigate(Screen.LocalSetup.createRoute(mode.name)) }, onBack = { navController.popBackStack() } ) } composable( route = Screen.LocalSetup.route, arguments = listOf(navArgument("modeName") { type = NavType.StringType }) ) { backStackEntry -> val modeName = backStackEntry.arguments?.getString("modeName") ?: "NORMAL" val mode = try { GameMode.valueOf(modeName) } catch (_: Exception) { GameMode.NORMAL } LocalSetupScreen( playerName = savedName, modeDisplayName = mode.displayName, mode = mode, onStartGame = { totalPlayers, name -> if (name.isNotEmpty()) { savedName = name prefs.edit().putString("player_name", name).apply() } navController.navigate( Screen.LocalGame.createRoute(modeName, totalPlayers, name) ) }, onBack = { navController.popBackStack() } ) } composable( route = Screen.LocalGame.route, arguments = listOf( navArgument("modeName") { type = NavType.StringType }, navArgument("totalPlayers") { type = NavType.IntType }, navArgument("humanPlayerName") { type = NavType.StringType } ) ) { backStackEntry -> val modeName = backStackEntry.arguments?.getString("modeName") ?: "NORMAL" val mode = try { GameMode.valueOf(modeName) } catch (_: Exception) { GameMode.NORMAL } val totalPlayers = backStackEntry.arguments?.getInt("totalPlayers") ?: 2 val humanPlayerName = backStackEntry.arguments?.getString("humanPlayerName") ?: "玩家" LocalGameScreen( mode = mode, totalPlayers = totalPlayers, humanPlayerName = humanPlayerName, onBackToMenu = { navController.navigate(Screen.MainMenu.route) { popUpTo(0) { inclusive = true } } } ) } composable(Screen.Scoreboard.route) { ScoreboardScreen(onBack = { navController.popBackStack() }) } composable(Screen.Rules.route) { RulesHelpScreen(onBack = { navController.popBackStack() }) } composable(Screen.Game.route) { gameState?.let { state -> GameScreen( gameState = state, myCards = myCards, myPlayerId = myPlayerId, isMyTurn = state.currentPlayer.id == myPlayerId, errorMessage = errorMessage, onPlayCard = { index -> errorMessage = "" val colorToSend = if (index >= 0 && index < myCards.size && myCards[index].type.isWild) { selectedWildColor } else null scope.launch { gameClient?.playCard(index, colorToSend) } selectedWildColor = null }, onDrawCard = { errorMessage = "" scope.launch { gameClient?.drawCard() } }, onChooseColor = { color -> selectedWildColor = color }, onCallUno = { // Send call uno to server scope.launch { gameClient?.let { val msg = com.unogame.network.Protocol.Message( type = com.unogame.network.Protocol.CMD_CALL_UNO, playerId = myPlayerId ) // Use a simple approach - mark locally } } }, onChallengeUno = { targetId -> // Challenge handled via normal message sending } ) } } composable( route = Screen.GameOver.route, arguments = listOf( navArgument("winnerName") { type = NavType.StringType }, navArgument("isYouWinner") { type = NavType.BoolType } ) ) { backStackEntry -> val winnerName = backStackEntry.arguments?.getString("winnerName") ?: "" val isYouWinner = backStackEntry.arguments?.getBoolean("isYouWinner") ?: false GameOverScreen( winnerName = winnerName, isYouWinner = isYouWinner, onBackToMenu = { cleanup() navController.navigate(Screen.MainMenu.route) { popUpTo(0) { inclusive = true } } } ) } } } }