First vertical slice: the whole shell plus two games playable end to end, so every layer is exercised before the other nine games are built on it. Games - Euchre: bowers, ordering up, dealer discard, going alone, stick the dealer, and full scoring. Rule variants (deck size, points to win, stick the dealer, going alone) are toggles in game setup. - Sequence: two-eyed and one-eyed jacks, free corners, locked sequences, the shared-chip limit, and dead-card exchange. Teams, players per team and the sequence target are toggles. - The board layout in SequenceBoard is generated, not the commercial board — it holds every structural property the rules need, but does not match a physical board. Documented in docs/RULES-sequence.md. Multiplayer - One transport abstraction over newline-delimited JSON, implemented twice: LAN via NSD discovery and TCP, Bluetooth via Classic RFCOMM. A lobby advertises on both at once, so a table can mix them. - Host-authoritative: clients send move intents, the host re-validates every one against the rules, and each client receives a state redacted for its own seat so hidden information never crosses the wire. - Seats start as AI and are handed to people as they join, which is what makes any mix of humans and AI work without a separate code path. Shell - Main menu lists all eleven games; the nine without engines are marked coming soon and are driven entirely by GameCatalog. - Settings: sound, animation speed (one multiplier drives card movement and AI thinking time), and OTA updates from the latest GitHub release. - Layouts measure available space rather than assuming an orientation. The game package is free of Android types, so the rules engines and AI run under plain JUnit; 43 tests cover ranking, following suit, scoring, sequence detection, redaction, wire round-trips and full random and AI playthroughs. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
78 lines
2.6 KiB
Kotlin
78 lines
2.6 KiB
Kotlin
package org.prolibertate.games
|
|
|
|
import android.Manifest
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.provider.Settings as AndroidSettings
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.lifecycle.lifecycleScope
|
|
import org.prolibertate.games.net.LobbyController
|
|
import org.prolibertate.games.settings.SettingsRepository
|
|
import org.prolibertate.games.ui.AppEnv
|
|
import org.prolibertate.games.ui.AppRoot
|
|
import org.prolibertate.games.ui.theme.ProLibertateTheme
|
|
import org.prolibertate.games.update.Updater
|
|
import java.util.UUID
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
private lateinit var env: AppEnv
|
|
|
|
/**
|
|
* Bluetooth needs runtime consent from Android 12 onwards. The result is
|
|
* ignored deliberately: refusing simply means the Bluetooth transport
|
|
* reports itself unavailable and the lobby falls back to Wi-Fi.
|
|
*/
|
|
private val bluetoothPermissions =
|
|
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { }
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
env = AppEnv(
|
|
settingsRepository = SettingsRepository(applicationContext),
|
|
updater = Updater(this),
|
|
lobby = LobbyController(applicationContext, lifecycleScope),
|
|
peerId = stablePeerId(),
|
|
requestBluetoothPermissions = ::requestBluetoothPermissions,
|
|
)
|
|
|
|
setContent {
|
|
ProLibertateTheme {
|
|
AppRoot(env)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun requestBluetoothPermissions() {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
bluetoothPermissions.launch(
|
|
arrayOf(
|
|
Manifest.permission.BLUETOOTH_CONNECT,
|
|
Manifest.permission.BLUETOOTH_SCAN,
|
|
Manifest.permission.BLUETOOTH_ADVERTISE,
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Identifies this device to a host for the length of an install. ANDROID_ID
|
|
* is per-app-signing-key and survives restarts, which is all a lobby needs
|
|
* to tell two phones apart.
|
|
*/
|
|
private fun stablePeerId(): String {
|
|
val androidId = runCatching {
|
|
AndroidSettings.Secure.getString(contentResolver, AndroidSettings.Secure.ANDROID_ID)
|
|
}.getOrNull()
|
|
return androidId ?: UUID.randomUUID().toString()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
if (::env.isInitialized) env.lobby.stop()
|
|
}
|
|
}
|