Add cash game session setup and reload
This commit is contained in:
@@ -115,6 +115,9 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
|
|||||||
- Android is playable in Compose. Engine snapshots are paced through a
|
- Android is playable in Compose. Engine snapshots are paced through a
|
||||||
rendezvous channel, human decisions are matched by engine-owned tokens, and a
|
rendezvous channel, human decisions are matched by engine-owned tokens, and a
|
||||||
completed hand stays on screen until the player explicitly starts the next one.
|
completed hand stays on screen until the player explicitly starts the next one.
|
||||||
|
- A cash-game session begins at an explicit take-a-seat screen. Opponents may
|
||||||
|
auto-reload below the big blind; the human is never silently topped up and must
|
||||||
|
explicitly choose "Reload to N & deal" from the completed-hand screen.
|
||||||
- Gradle emits an `archives` deprecation from the Kotlin Multiplatform plugin's
|
- Gradle emits an `archives` deprecation from the Kotlin Multiplatform plugin's
|
||||||
own `jvm()` target registration — upstream in Kotlin 2.2.10, not our build.
|
own `jvm()` target registration — upstream in Kotlin 2.2.10, not our build.
|
||||||
- Not built yet: LLM persona layer and opt-in coach.
|
- Not built yet: LLM persona layer and opt-in coach.
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.jsjdesigns.poker
|
||||||
|
|
||||||
|
internal const val MAX_PLAYER_NAME_LENGTH = 16
|
||||||
|
internal const val DEFAULT_BUY_IN = 200
|
||||||
|
internal const val DEFAULT_SMALL_BLIND = 1
|
||||||
|
internal const val DEFAULT_BIG_BLIND = 2
|
||||||
|
|
||||||
|
data class CashGameConfig(
|
||||||
|
val playerName: String,
|
||||||
|
val buyIn: Int = DEFAULT_BUY_IN,
|
||||||
|
val smallBlind: Int = DEFAULT_SMALL_BLIND,
|
||||||
|
val bigBlind: Int = DEFAULT_BIG_BLIND,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalises player input once at the session boundary.
|
||||||
|
*
|
||||||
|
* "You" remains the safe default, but a supplied name is trimmed, internal
|
||||||
|
* whitespace is collapsed, and the table-sized label is bounded.
|
||||||
|
*/
|
||||||
|
fun cashGameConfig(playerName: String): CashGameConfig {
|
||||||
|
val normalised = playerName
|
||||||
|
.trim()
|
||||||
|
.replace(Regex("\\s+"), " ")
|
||||||
|
.take(MAX_PLAYER_NAME_LENGTH)
|
||||||
|
.ifBlank { "You" }
|
||||||
|
return CashGameConfig(playerName = normalised)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cash-game bots may top themselves up automatically; the human never does.
|
||||||
|
* Returning indices keeps the policy pure and independently testable.
|
||||||
|
*/
|
||||||
|
fun automaticTopUpSeatIndices(
|
||||||
|
stacks: List<Int>,
|
||||||
|
heroSeat: Int,
|
||||||
|
bigBlind: Int,
|
||||||
|
): List<Int> {
|
||||||
|
require(heroSeat in stacks.indices) { "hero seat is outside the table" }
|
||||||
|
require(bigBlind > 0) { "big blind must be positive" }
|
||||||
|
return stacks.indices.filter { it != heroSeat && stacks[it] < bigBlind }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun continuationButtonLabel(
|
||||||
|
nextHandRequested: Boolean,
|
||||||
|
rebuyRequired: Boolean,
|
||||||
|
buyIn: Int,
|
||||||
|
): String = when {
|
||||||
|
nextHandRequested -> "Dealing…"
|
||||||
|
rebuyRequired -> "Reload to $buyIn & deal"
|
||||||
|
else -> "Next hand"
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
val vm: PokerViewModel = viewModel()
|
val vm: PokerViewModel = viewModel()
|
||||||
TableScreen(vm)
|
PokerApp(vm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.jsjdesigns.poker
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
|
private val SetupFelt = Color(0xFF0B3D26)
|
||||||
|
private val TableGold = Color(0xFFE3C179)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PokerApp(vm: PokerViewModel) {
|
||||||
|
val state by vm.state.collectAsStateWithLifecycle()
|
||||||
|
if (state.gameConfig == null) {
|
||||||
|
TakeASeatScreen(onStart = vm::startGame)
|
||||||
|
} else {
|
||||||
|
TableScreen(vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TakeASeatScreen(onStart: (String) -> Unit) {
|
||||||
|
var playerName by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(SetupFelt)
|
||||||
|
.systemBarsPadding()
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"JSJ Hold’em",
|
||||||
|
color = TableGold,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 34.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Take a seat",
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = playerName,
|
||||||
|
onValueChange = { playerName = it.take(MAX_PLAYER_NAME_LENGTH) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = { Text("Your name (optional)") },
|
||||||
|
placeholder = { Text("You") },
|
||||||
|
singleLine = true,
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedTextColor = Color.White,
|
||||||
|
unfocusedTextColor = Color.White,
|
||||||
|
focusedBorderColor = TableGold,
|
||||||
|
unfocusedBorderColor = Color.White.copy(alpha = 0.4f),
|
||||||
|
focusedLabelColor = TableGold,
|
||||||
|
unfocusedLabelColor = Color.White.copy(alpha = 0.6f),
|
||||||
|
cursorColor = TableGold,
|
||||||
|
focusedPlaceholderColor = Color.White.copy(alpha = 0.35f),
|
||||||
|
unfocusedPlaceholderColor = Color.White.copy(alpha = 0.35f),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.08f)),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text("Six-max cash game", color = Color.White, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
"Buy-in $DEFAULT_BUY_IN Blinds $DEFAULT_SMALL_BLIND / $DEFAULT_BIG_BLIND",
|
||||||
|
color = TableGold,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
"Ada · Bruno · Cleo · Dex · Enzo",
|
||||||
|
color = Color.White.copy(alpha = 0.72f),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"A calibrated mix of skill levels and playing styles.",
|
||||||
|
color = Color.White.copy(alpha = 0.48f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { onStart(playerName) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color(0xFF2C6E49),
|
||||||
|
contentColor = Color.White,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text("Buy in for $DEFAULT_BUY_IN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,16 +24,15 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
const val HERO_SEAT = 0
|
const val HERO_SEAT = 0
|
||||||
private const val STARTING_STACK = 200
|
|
||||||
private const val SMALL_BLIND = 1
|
|
||||||
private const val BIG_BLIND = 2
|
|
||||||
|
|
||||||
data class UiState(
|
data class UiState(
|
||||||
|
/** Null until the player has explicitly started a cash-game session. */
|
||||||
|
val gameConfig: CashGameConfig? = null,
|
||||||
val snapshot: TableSnapshot? = null,
|
val snapshot: TableSnapshot? = null,
|
||||||
val handsPlayed: Int = 0,
|
val handsPlayed: Int = 0,
|
||||||
val handSummary: HandSummary? = null,
|
val handSummary: HandSummary? = null,
|
||||||
val nextHandRequested: Boolean = false,
|
val nextHandRequested: Boolean = false,
|
||||||
val message: String? = null,
|
val heroNeedsRebuy: Boolean = false,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* The decision to show, or null.
|
* The decision to show, or null.
|
||||||
@@ -93,34 +92,46 @@ class PokerViewModel : ViewModel() {
|
|||||||
/** What the player is being asked to decide, or null when it isn't their turn. */
|
/** What the player is being asked to decide, or null when it isn't their turn. */
|
||||||
val offer = human.offer
|
val offer = human.offer
|
||||||
|
|
||||||
private val roster = listOf(
|
private var gameStarted = false
|
||||||
BotProfile("You", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE),
|
private lateinit var seats: List<Seat>
|
||||||
|
private lateinit var table: Table
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch { consumeFrames() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the table exactly once, after the player chooses to take a seat.
|
||||||
|
* A rapid double tap cannot launch two engines against the same HumanAgent.
|
||||||
|
*/
|
||||||
|
fun startGame(playerName: String) {
|
||||||
|
if (gameStarted) return
|
||||||
|
gameStarted = true
|
||||||
|
|
||||||
|
val config = cashGameConfig(playerName)
|
||||||
|
val roster = listOf(
|
||||||
|
BotProfile(config.playerName, SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE),
|
||||||
BotProfile("Ada", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE, "Ice-cold. Punishes mistakes."),
|
BotProfile("Ada", SkillLevel.EXPERT, PlayStyle.TIGHT_AGGRESSIVE, "Ice-cold. Punishes mistakes."),
|
||||||
BotProfile("Bruno", SkillLevel.ADVANCED, PlayStyle.LOOSE_AGGRESSIVE, "Relentless pressure."),
|
BotProfile("Bruno", SkillLevel.ADVANCED, PlayStyle.LOOSE_AGGRESSIVE, "Relentless pressure."),
|
||||||
BotProfile("Cleo", SkillLevel.INTERMEDIATE, PlayStyle.TRAPPER, "Quiet until she has you."),
|
BotProfile("Cleo", SkillLevel.INTERMEDIATE, PlayStyle.TRAPPER, "Quiet until she has you."),
|
||||||
BotProfile("Dex", SkillLevel.INTERMEDIATE, PlayStyle.CALLING_STATION, "Pays to see it."),
|
BotProfile("Dex", SkillLevel.INTERMEDIATE, PlayStyle.CALLING_STATION, "Pays to see it."),
|
||||||
BotProfile("Enzo", SkillLevel.BEGINNER, PlayStyle.MANIAC, "Chaos, and certain he's winning."),
|
BotProfile("Enzo", SkillLevel.BEGINNER, PlayStyle.MANIAC, "Chaos, and certain he's winning."),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val seats: List<Seat>
|
|
||||||
private val table: Table
|
|
||||||
|
|
||||||
init {
|
|
||||||
val deckRandom = Random(System.nanoTime())
|
val deckRandom = Random(System.nanoTime())
|
||||||
seats = roster.mapIndexed { i, profile ->
|
seats = roster.mapIndexed { i, profile ->
|
||||||
val agent = if (i == HERO_SEAT) human else MathBot(profile, Random(deckRandom.nextLong()))
|
val agent = if (i == HERO_SEAT) human else MathBot(profile, Random(deckRandom.nextLong()))
|
||||||
Seat(i, profile.name, STARTING_STACK, agent)
|
Seat(i, profile.name, config.buyIn, agent)
|
||||||
}
|
}
|
||||||
table = Table(
|
table = Table(
|
||||||
seats = seats,
|
seats = seats,
|
||||||
smallBlind = SMALL_BLIND,
|
smallBlind = config.smallBlind,
|
||||||
bigBlind = BIG_BLIND,
|
bigBlind = config.bigBlind,
|
||||||
random = deckRandom,
|
random = deckRandom,
|
||||||
observer = { frames.send(it) },
|
observer = { frames.send(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModelScope.launch { consumeFrames() }
|
_state.update { UiState(gameConfig = config) }
|
||||||
viewModelScope.launch(Dispatchers.Default) { playContinuously() }
|
viewModelScope.launch(Dispatchers.Default) { playContinuously(config) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun consumeFrames() {
|
private suspend fun consumeFrames() {
|
||||||
@@ -134,6 +145,7 @@ class PokerViewModel : ViewModel() {
|
|||||||
// terminal board with a generic "Waiting…" message.
|
// terminal board with a generic "Waiting…" message.
|
||||||
handSummary = current.handSummary.takeIf { sameCompletedHand },
|
handSummary = current.handSummary.takeIf { sameCompletedHand },
|
||||||
nextHandRequested = current.nextHandRequested && sameCompletedHand,
|
nextHandRequested = current.nextHandRequested && sameCompletedHand,
|
||||||
|
heroNeedsRebuy = current.heroNeedsRebuy && sameCompletedHand,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
delay(pacingMillis(frame))
|
delay(pacingMillis(frame))
|
||||||
@@ -154,10 +166,16 @@ class PokerViewModel : ViewModel() {
|
|||||||
TableSnapshot.Phase.COMPLETE -> 1200
|
TableSnapshot.Phase.COMPLETE -> 1200
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun playContinuously() {
|
private suspend fun playContinuously(config: CashGameConfig) {
|
||||||
while (true) {
|
while (true) {
|
||||||
// Cash-game convention: top anyone back up who cannot cover a blind.
|
// Opponents rebuy automatically. The human can re-enter only through
|
||||||
for (s in seats) if (s.stack < BIG_BLIND) s.stack = STARTING_STACK
|
// the explicit result-screen action below.
|
||||||
|
val botTopUps = automaticTopUpSeatIndices(
|
||||||
|
stacks = seats.map(Seat::stack),
|
||||||
|
heroSeat = HERO_SEAT,
|
||||||
|
bigBlind = config.bigBlind,
|
||||||
|
)
|
||||||
|
for (seatIndex in botTopUps) seats[seatIndex].stack = config.buyIn
|
||||||
table.advanceButton()
|
table.advanceButton()
|
||||||
runCatching { table.playHand() }
|
runCatching { table.playHand() }
|
||||||
.onSuccess { result ->
|
.onSuccess { result ->
|
||||||
@@ -166,16 +184,23 @@ class PokerViewModel : ViewModel() {
|
|||||||
result = result,
|
result = result,
|
||||||
seatNames = seats.map(Seat::name),
|
seatNames = seats.map(Seat::name),
|
||||||
)
|
)
|
||||||
|
val heroNeedsRebuy = seats[HERO_SEAT].stack < config.bigBlind
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
handsPlayed = it.handsPlayed + 1,
|
handsPlayed = it.handsPlayed + 1,
|
||||||
handSummary = summary,
|
handSummary = summary,
|
||||||
nextHandRequested = false,
|
nextHandRequested = false,
|
||||||
|
heroNeedsRebuy = heroNeedsRebuy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Do not let the next deal erase the outcome before the
|
// Do not let the next deal erase the outcome before the
|
||||||
// player has read it and chosen to continue.
|
// player has read it and chosen to continue.
|
||||||
nextHandGate.awaitRequest(summary.handNumber)
|
nextHandGate.awaitRequest(summary.handNumber)
|
||||||
|
if (heroNeedsRebuy) {
|
||||||
|
// This runs on the engine coroutine after the explicit
|
||||||
|
// "Reload & deal" request, never as a silent table mutation.
|
||||||
|
seats[HERO_SEAT].stack = config.buyIn
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onFailure { return } // scope cancelled: the screen went away
|
.onFailure { return } // scope cancelled: the screen went away
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ fun TableScreen(vm: PokerViewModel) {
|
|||||||
offer = offer,
|
offer = offer,
|
||||||
handSummary = handSummary,
|
handSummary = handSummary,
|
||||||
nextHandRequested = state.nextHandRequested,
|
nextHandRequested = state.nextHandRequested,
|
||||||
|
rebuyRequired = state.heroNeedsRebuy,
|
||||||
|
buyIn = state.gameConfig?.buyIn ?: DEFAULT_BUY_IN,
|
||||||
heroFolded = hero?.folded == true,
|
heroFolded = hero?.folded == true,
|
||||||
onFold = vm::fold,
|
onFold = vm::fold,
|
||||||
onCheckCall = vm::checkOrCall,
|
onCheckCall = vm::checkOrCall,
|
||||||
@@ -243,6 +245,8 @@ private fun ActionBar(
|
|||||||
offer: com.jsjdesigns.poker.game.DecisionOffer?,
|
offer: com.jsjdesigns.poker.game.DecisionOffer?,
|
||||||
handSummary: HandSummary?,
|
handSummary: HandSummary?,
|
||||||
nextHandRequested: Boolean,
|
nextHandRequested: Boolean,
|
||||||
|
rebuyRequired: Boolean,
|
||||||
|
buyIn: Int,
|
||||||
heroFolded: Boolean,
|
heroFolded: Boolean,
|
||||||
onFold: (Long) -> Unit,
|
onFold: (Long) -> Unit,
|
||||||
onCheckCall: (Long) -> Unit,
|
onCheckCall: (Long) -> Unit,
|
||||||
@@ -254,6 +258,8 @@ private fun ActionBar(
|
|||||||
HandResultBar(
|
HandResultBar(
|
||||||
summary = handSummary,
|
summary = handSummary,
|
||||||
nextHandRequested = nextHandRequested,
|
nextHandRequested = nextHandRequested,
|
||||||
|
rebuyRequired = rebuyRequired,
|
||||||
|
buyIn = buyIn,
|
||||||
onNextHand = onNextHand,
|
onNextHand = onNextHand,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -333,6 +339,8 @@ private fun ActionBar(
|
|||||||
private fun HandResultBar(
|
private fun HandResultBar(
|
||||||
summary: HandSummary,
|
summary: HandSummary,
|
||||||
nextHandRequested: Boolean,
|
nextHandRequested: Boolean,
|
||||||
|
rebuyRequired: Boolean,
|
||||||
|
buyIn: Int,
|
||||||
onNextHand: (Int) -> Unit,
|
onNextHand: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
@@ -354,6 +362,14 @@ private fun HandResultBar(
|
|||||||
color = Color.White.copy(alpha = 0.7f),
|
color = Color.White.copy(alpha = 0.7f),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
|
if (rebuyRequired) {
|
||||||
|
Text(
|
||||||
|
"Your stack cannot cover the big blind.",
|
||||||
|
color = Color(0xFFC9545B),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { onNextHand(summary.handNumber) },
|
onClick = { onNextHand(summary.handNumber) },
|
||||||
@@ -366,7 +382,7 @@ private fun HandResultBar(
|
|||||||
disabledContentColor = Color.White.copy(alpha = 0.65f),
|
disabledContentColor = Color.White.copy(alpha = 0.65f),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(if (nextHandRequested) "Dealing…" else "Next hand")
|
Text(continuationButtonLabel(nextHandRequested, rebuyRequired, buyIn))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.jsjdesigns.poker
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CashGameSessionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank player name has an honest second-person fallback`() {
|
||||||
|
assertEquals("You", cashGameConfig(" ").playerName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `player name is normalised once at the session boundary`() {
|
||||||
|
assertEquals("Jay Smith", cashGameConfig(" Jay Smith ").playerName)
|
||||||
|
assertEquals(
|
||||||
|
MAX_PLAYER_NAME_LENGTH,
|
||||||
|
cashGameConfig("A name much too long for a phone table").playerName.length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `automatic top ups categorically exclude the human seat`() {
|
||||||
|
val topUps = automaticTopUpSeatIndices(
|
||||||
|
stacks = listOf(0, 0, 1, 2, 200),
|
||||||
|
heroSeat = 0,
|
||||||
|
bigBlind = 2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf(1, 2), topUps)
|
||||||
|
assertFalse(HERO_SEAT in topUps)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `continuation copy promises the exact stack the reload produces`() {
|
||||||
|
assertEquals(
|
||||||
|
"Reload to 200 & deal",
|
||||||
|
continuationButtonLabel(
|
||||||
|
nextHandRequested = false,
|
||||||
|
rebuyRequired = true,
|
||||||
|
buyIn = 200,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals("Next hand", continuationButtonLabel(false, false, 200))
|
||||||
|
assertEquals("Dealing…", continuationButtonLabel(true, true, 200))
|
||||||
|
assertTrue(continuationButtonLabel(false, true, 200).contains("200"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user