feat: UNO Kotlin Multiplatform app (Android, Desktop, Web)

- Full UNO game logic with AI opponents
- Compose Multiplatform UI with animations
- Shared module (commonMain) with game engine
- Desktop app (Compose Desktop)
- Android app (Compose Multiplatform)
- Web app (Kotlin/Wasm)
- Nix shell for reproducible dev environment
This commit is contained in:
2026-08-02 18:21:57 +02:00
commit 786b813f7b
25 changed files with 2093 additions and 0 deletions
@@ -0,0 +1,112 @@
package com.uno.web
import androidx.compose.runtime.*
import com.uno.game.GameEngine
import com.uno.game.model.*
import com.uno.shared.UnoGameScreen
import kotlinx.browser.document
import org.w3c.dom.HTMLElement
fun main() {
val root = document.getElementById("root") as HTMLElement
root.innerHTML = ""
val container = document.createElement("div")
container.id = "app"
root.appendChild(container)
composeWebRoot(container)
}
@Composable
fun composeWebRoot(container: HTMLElement) {
val engine = remember { GameEngine() }
var gameState by remember { mutableStateOf(engine.startGame()) }
var gameStarted by remember { mutableStateOf(false) }
if (!gameStarted) {
WebStartScreen(
onStartGame = { playerCount ->
gameState = engine.startGame(playerCount)
gameStarted = true
}
)
} else {
UnoGameScreen(
state = gameState,
onPlayCard = { card, color ->
gameState = engine.playCard(gameState, 0, card, color)
gameState = runAiTurns(engine, gameState)
},
onDrawCard = {
gameState = engine.drawCard(gameState, 0)
gameState = runAiTurns(engine, gameState)
},
onCallUno = {
gameState = engine.callUno(gameState, 0)
}
)
if (gameState.isFinished) {
WebGameOverScreen(
winner = gameState.winner,
onPlayAgain = {
gameState = engine.startGame()
}
)
}
}
}
private fun runAiTurns(engine: GameEngine, state: GameState): GameState {
var currentState = state
while (currentState.currentPlayerIndex != 0 && !currentState.isFinished) {
currentState = engine.aiPlay(currentState, currentState.currentPlayerIndex)
}
return currentState
}
@Composable
fun WebStartScreen(onStartGame: (Int) -> Unit) {
androidx.compose.foundation.layout.Column(
modifier = androidx.compose.foundation.layout.fillMaxSize().padding(32.dp),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center
) {
androidx.compose.material3.Text(
text = "UNO",
style = androidx.compose.material3.MaterialTheme.typography.displayLarge,
color = androidx.compose.material3.MaterialTheme.colorScheme.primary
)
androidx.compose.foundation.layout.Spacer(modifier = Modifier)
androidx.compose.material3.Text(
text = "Web Edition",
style = androidx.compose.material3.MaterialTheme.typography.titleMedium,
color = androidx.compose.material3.MaterialTheme.colorScheme.onSurfaceVariant
)
androidx.compose.foundation.layout.Spacer(modifier = Modifier)
androidx.compose.material3.Button(onClick = { onStartGame(4) }) {
androidx.compose.material3.Text("Spiel starten (4 Spieler)")
}
}
}
@Composable
fun WebGameOverScreen(winner: Player?, onPlayAgain: () -> Unit) {
androidx.compose.material3.AlertDialog(
onDismissRequest = onPlayAgain,
title = { androidx.compose.material3.Text("Spiel vorbei!") },
text = {
androidx.compose.material3.Text(
text = if (winner != null) "${winner.name} hat gewonnen!"
else "Unentschieden!"
)
},
confirmButton = {
androidx.compose.material3.Button(onClick = onPlayAgain) {
androidx.compose.material3.Text("Nochmal spielen")
}
}
)
}
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UNO - Kotlin Multiplatform</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
#root { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="root"></div>
<script src="webApp.js"></script>
</body>
</html>