Add app shell with Euchre and Sequence, LAN/Bluetooth play
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
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user