Rework table spacing and betting controls
This commit is contained in:
@@ -147,12 +147,16 @@ 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.
|
||||||
The portrait table follows the 1A design direction: oval felt, orbiting seats,
|
The portrait table follows the 1A design direction: circular felt, perimeter seats,
|
||||||
spatial bets, large 2:3 card art, always-visible legal sizing controls, and a
|
spatial bets, large 2:3 card art, always-visible legal sizing controls, and a
|
||||||
winner-focused pot/hand breakdown at completion. Face-up cards have fixed
|
winner-focused pot/hand breakdown at completion. Face-up cards have fixed
|
||||||
legibility floors rather than shrinking on short displays; the opponent orbit
|
legibility floors rather than shrinking on short displays; the opponent orbit
|
||||||
remains above the community-card band, and the terminal modal repeats the
|
straddles the outside rail rather than occupying the community-card band.
|
||||||
board beside exact pot awards so dimming the table never hides the explanation.
|
The terminal modal repeats the board, every public hand turned up (including
|
||||||
|
losing hands), and exact pot awards so dimming the table never hides the
|
||||||
|
explanation. Raise sizing combines coarse slider/presets with one-chip minus
|
||||||
|
and plus controls; visible chip stacks grow with bets and the pot while numeric
|
||||||
|
labels remain authoritative.
|
||||||
- A cash-game session begins at an explicit take-a-seat screen. Opponents may
|
- 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
|
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.
|
explicitly choose "Reload to N & deal" from the completed-hand screen.
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ fun buttonsFor(offer: DecisionOffer): ActionButtons {
|
|||||||
fun raiseLabel(buttons: ActionButtons, amount: Int): String =
|
fun raiseLabel(buttons: ActionButtons, amount: Int): String =
|
||||||
if (amount >= buttons.sliderMax) "All in" else "Raise $amount"
|
if (amount >= buttons.sliderMax) "All in" else "Raise $amount"
|
||||||
|
|
||||||
|
/** One-chip fine tuning for the sizing rail, clamped to the engine's legal range. */
|
||||||
|
fun adjustRaiseAmount(
|
||||||
|
current: Int,
|
||||||
|
change: Int,
|
||||||
|
buttons: ActionButtons,
|
||||||
|
): Int = (current + change).coerceIn(buttons.sliderMin, buttons.sliderMax)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Honest, legal raise-to shortcuts for the always-visible sizing rail.
|
* Honest, legal raise-to shortcuts for the always-visible sizing rail.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.jsjdesigns.poker
|
|||||||
|
|
||||||
import com.jsjdesigns.poker.core.HandEvaluator
|
import com.jsjdesigns.poker.core.HandEvaluator
|
||||||
import com.jsjdesigns.poker.game.HandResult
|
import com.jsjdesigns.poker.game.HandResult
|
||||||
|
import com.jsjdesigns.poker.game.PotAward
|
||||||
|
import com.jsjdesigns.poker.game.PotPayout
|
||||||
|
|
||||||
data class AwardWinner(
|
data class AwardWinner(
|
||||||
val seat: Int,
|
val seat: Int,
|
||||||
@@ -17,6 +19,15 @@ data class PotAwardSummary(
|
|||||||
val winners: List<AwardWinner>,
|
val winners: List<AwardWinner>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ShowdownPlayerSummary(
|
||||||
|
val seat: Int,
|
||||||
|
val label: String,
|
||||||
|
val hole: List<Int>,
|
||||||
|
val handDescription: String,
|
||||||
|
/** Total won across every main/side pot; zero means the hand was shown but lost. */
|
||||||
|
val amountWon: Int,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutable, presentation-ready outcome for one completed hand.
|
* Immutable, presentation-ready outcome for one completed hand.
|
||||||
*
|
*
|
||||||
@@ -35,6 +46,8 @@ data class HandSummary(
|
|||||||
val potSize: Int,
|
val potSize: Int,
|
||||||
val heroNet: Int,
|
val heroNet: Int,
|
||||||
val wentToShowdown: Boolean,
|
val wentToShowdown: Boolean,
|
||||||
|
/** Every public hand turned face-up, including contenders that won no pot. */
|
||||||
|
val showdownPlayers: List<ShowdownPlayerSummary>,
|
||||||
/** Main/side-pot breakdown with public winning cards and evaluated hand names. */
|
/** Main/side-pot breakdown with public winning cards and evaluated hand names. */
|
||||||
val awards: List<PotAwardSummary>,
|
val awards: List<PotAwardSummary>,
|
||||||
) {
|
) {
|
||||||
@@ -77,6 +90,22 @@ fun handSummaryFor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val multiplePots = result.potAwards.size > 1
|
val multiplePots = result.potAwards.size > 1
|
||||||
|
val wonBySeat = result.potAwards
|
||||||
|
.flatMap(PotAward::payouts)
|
||||||
|
.groupBy(PotPayout::seat)
|
||||||
|
.mapValues { (_, payouts) -> payouts.sumOf(PotPayout::amount) }
|
||||||
|
val showdownPlayers = result.showdownHands.map { showdown ->
|
||||||
|
require(showdown.seat in seatNames.indices) {
|
||||||
|
"showdown seat is outside the roster"
|
||||||
|
}
|
||||||
|
ShowdownPlayerSummary(
|
||||||
|
seat = showdown.seat,
|
||||||
|
label = if (showdown.seat == heroSeat) "You" else seatNames[showdown.seat],
|
||||||
|
hole = showdown.hole.toList(),
|
||||||
|
handDescription = HandEvaluator.describe(showdown.score),
|
||||||
|
amountWon = wonBySeat[showdown.seat] ?: 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
val awards = result.potAwards.mapIndexed { index, award ->
|
val awards = result.potAwards.mapIndexed { index, award ->
|
||||||
require(award.payouts.all { it.seat in seatNames.indices }) {
|
require(award.payouts.all { it.seat in seatNames.indices }) {
|
||||||
"pot winner seat is outside the roster"
|
"pot winner seat is outside the roster"
|
||||||
@@ -111,6 +140,7 @@ fun handSummaryFor(
|
|||||||
potSize = result.potSize,
|
potSize = result.potSize,
|
||||||
heroNet = result.net[heroSeat],
|
heroNet = result.net[heroSeat],
|
||||||
wentToShowdown = result.wentToShowdown,
|
wentToShowdown = result.wentToShowdown,
|
||||||
|
showdownPlayers = showdownPlayers,
|
||||||
awards = awards,
|
awards = awards,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -41,7 +43,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
@@ -58,9 +59,7 @@ import com.jsjdesigns.poker.core.Card
|
|||||||
import com.jsjdesigns.poker.game.DecisionOffer
|
import com.jsjdesigns.poker.game.DecisionOffer
|
||||||
import com.jsjdesigns.poker.game.SeatSnapshot
|
import com.jsjdesigns.poker.game.SeatSnapshot
|
||||||
import com.jsjdesigns.poker.game.TableSnapshot
|
import com.jsjdesigns.poker.game.TableSnapshot
|
||||||
import kotlin.math.PI
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
private val Night = Color(0xFF04100C)
|
private val Night = Color(0xFF04100C)
|
||||||
private val TableBackground = Color(0xFF071D16)
|
private val TableBackground = Color(0xFF071D16)
|
||||||
@@ -76,25 +75,37 @@ private val Muted = Color(0xFF9FB0A6)
|
|||||||
private val Danger = Color(0xFF8C242A)
|
private val Danger = Color(0xFF8C242A)
|
||||||
private val Panel = Color(0xE604120D)
|
private val Panel = Color(0xE604120D)
|
||||||
|
|
||||||
|
internal fun visibleChipCount(amount: Int): Int = when {
|
||||||
|
amount <= 0 -> 0
|
||||||
|
amount <= 2 -> 1
|
||||||
|
amount <= 5 -> 2
|
||||||
|
amount <= 10 -> 3
|
||||||
|
amount <= 25 -> 4
|
||||||
|
amount <= 60 -> 5
|
||||||
|
amount <= 150 -> 6
|
||||||
|
else -> 7
|
||||||
|
}
|
||||||
|
|
||||||
internal data class NormalizedSeatPosition(
|
internal data class NormalizedSeatPosition(
|
||||||
val centerX: Float,
|
val centerX: Float,
|
||||||
val topY: Float,
|
val topY: Float,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Five positions along the upper half of an ellipse.
|
* Five seats around the outside of a circular felt.
|
||||||
*
|
*
|
||||||
* Keeping this geometry separate from Compose makes the table relationship
|
* Keeping this geometry separate from Compose makes the table relationship
|
||||||
* testable: opponents occupy the upper arc while the board owns the middle band.
|
* testable. The three top seats have independent horizontal lanes; the two side
|
||||||
|
* seats straddle the rail instead of consuming the board's middle.
|
||||||
*/
|
*/
|
||||||
internal fun opponentSeatOrbit(): List<NormalizedSeatPosition> =
|
internal fun opponentSeatOrbit(): List<NormalizedSeatPosition> =
|
||||||
listOf(145.0, 118.0, 90.0, 62.0, 35.0).map { degrees ->
|
listOf(
|
||||||
val radians = degrees * PI / 180.0
|
NormalizedSeatPosition(centerX = 0.13f, topY = 0.18f),
|
||||||
NormalizedSeatPosition(
|
NormalizedSeatPosition(centerX = 0.28f, topY = 0.01f),
|
||||||
centerX = (0.5 + 0.46 * cos(radians)).toFloat(),
|
NormalizedSeatPosition(centerX = 0.50f, topY = 0f),
|
||||||
topY = (0.35 - 0.27 * sin(radians)).toFloat(),
|
NormalizedSeatPosition(centerX = 0.72f, topY = 0.01f),
|
||||||
|
NormalizedSeatPosition(centerX = 0.87f, topY = 0.18f),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TableScreen(vm: PokerViewModel) {
|
fun TableScreen(vm: PokerViewModel) {
|
||||||
@@ -121,8 +132,9 @@ fun TableScreen(vm: PokerViewModel) {
|
|||||||
handSummary = handSummary,
|
handSummary = handSummary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f),
|
.aspectRatio(0.80f),
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
TableControls(
|
TableControls(
|
||||||
offer = offer,
|
offer = offer,
|
||||||
handSummary = handSummary,
|
handSummary = handSummary,
|
||||||
@@ -157,13 +169,13 @@ private fun TableHeader(
|
|||||||
"HAND ${snapshot?.handNumber ?: 1}",
|
"HAND ${snapshot?.handNumber ?: 1}",
|
||||||
color = Cream,
|
color = Cream,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 12.sp,
|
fontSize = 14.sp,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
snapshot?.street?.name.orEmpty(),
|
snapshot?.street?.name.orEmpty(),
|
||||||
color = Muted.copy(alpha = 0.8f),
|
color = Muted.copy(alpha = 0.8f),
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 10.sp,
|
fontSize = 11.sp,
|
||||||
letterSpacing = 1.4.sp,
|
letterSpacing = 1.4.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -183,7 +195,7 @@ private fun TableHeader(
|
|||||||
"${config?.bigBlind ?: DEFAULT_BIG_BLIND}",
|
"${config?.bigBlind ?: DEFAULT_BIG_BLIND}",
|
||||||
color = TableGold,
|
color = TableGold,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 11.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,8 +218,6 @@ private fun PokerTable(
|
|||||||
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(horizontal = 6.dp)
|
|
||||||
.clip(RoundedCornerShape(26.dp))
|
|
||||||
.background(TableBackground),
|
.background(TableBackground),
|
||||||
) {
|
) {
|
||||||
TableFelt(Modifier.fillMaxSize())
|
TableFelt(Modifier.fillMaxSize())
|
||||||
@@ -216,14 +226,14 @@ private fun PokerTable(
|
|||||||
seats.filter { it.index != HERO_SEAT }
|
seats.filter { it.index != HERO_SEAT }
|
||||||
.forEachIndexed { index, seat ->
|
.forEachIndexed { index, seat ->
|
||||||
val position = orbit.getOrElse(index) { orbit.last() }
|
val position = orbit.getOrElse(index) { orbit.last() }
|
||||||
val seatWidth = if (seat.revealed && seat.hole != null) 90.dp else 76.dp
|
val seatWidth = if (seat.revealed && seat.hole != null) 108.dp else 88.dp
|
||||||
OpponentSeat(
|
OpponentSeat(
|
||||||
seat = seat,
|
seat = seat,
|
||||||
isTurn = snapshot?.toAct == seat.index,
|
isTurn = snapshot?.toAct == seat.index,
|
||||||
isWinner = seat.index in winners,
|
isWinner = seat.index in winners,
|
||||||
modifier = Modifier.offset(
|
modifier = Modifier.offset(
|
||||||
x = maxWidth * position.centerX - seatWidth / 2,
|
x = maxWidth * position.centerX - seatWidth / 2,
|
||||||
y = maxHeight * position.topY,
|
y = maxWidth * position.topY,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -233,8 +243,8 @@ private fun PokerTable(
|
|||||||
status = status,
|
status = status,
|
||||||
tableTalk = tableTalk,
|
tableTalk = tableTalk,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.TopCenter)
|
||||||
.offset(y = 18.dp),
|
.offset(y = maxWidth * 0.40f),
|
||||||
)
|
)
|
||||||
|
|
||||||
HeroSeat(
|
HeroSeat(
|
||||||
@@ -251,7 +261,7 @@ private fun PokerTable(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Night.copy(alpha = 0.82f))
|
.background(Night.copy(alpha = 0.82f))
|
||||||
.padding(18.dp),
|
.padding(8.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
ShowdownModal(summary)
|
ShowdownModal(summary)
|
||||||
@@ -263,40 +273,33 @@ private fun PokerTable(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TableFelt(modifier: Modifier = Modifier) {
|
private fun TableFelt(modifier: Modifier = Modifier) {
|
||||||
Canvas(modifier) {
|
Canvas(modifier) {
|
||||||
val railTop = 2.dp.toPx()
|
val center = Offset(size.width * 0.5f, size.width * 0.5f)
|
||||||
val railBottom = 18.dp.toPx()
|
val railRadius = size.minDimension * 0.405f
|
||||||
val horizontalBleed = 22.dp.toPx()
|
drawCircle(
|
||||||
val railSize = Size(
|
|
||||||
width = size.width + horizontalBleed * 2,
|
|
||||||
height = size.height - railTop - railBottom,
|
|
||||||
)
|
|
||||||
val railOffset = Offset(-horizontalBleed, railTop)
|
|
||||||
drawOval(
|
|
||||||
brush = Brush.linearGradient(
|
brush = Brush.linearGradient(
|
||||||
colors = listOf(RailLight, RailDark),
|
colors = listOf(RailLight, RailDark),
|
||||||
start = railOffset,
|
start = Offset(center.x - railRadius, center.y - railRadius),
|
||||||
end = Offset(size.width, size.height),
|
end = Offset(size.width, size.height),
|
||||||
),
|
),
|
||||||
topLeft = railOffset,
|
radius = railRadius,
|
||||||
size = railSize,
|
center = center,
|
||||||
)
|
)
|
||||||
|
|
||||||
val inset = 9.dp.toPx()
|
val inset = 9.dp.toPx()
|
||||||
val feltOffset = Offset(railOffset.x + inset, railOffset.y + inset)
|
val feltRadius = railRadius - inset
|
||||||
val feltSize = Size(railSize.width - inset * 2, railSize.height - inset * 2)
|
drawCircle(
|
||||||
drawOval(
|
|
||||||
brush = Brush.radialGradient(
|
brush = Brush.radialGradient(
|
||||||
colors = listOf(FeltLight, FeltMid, FeltDark),
|
colors = listOf(FeltLight, FeltMid, FeltDark),
|
||||||
center = Offset(size.width * 0.5f, size.height * 0.38f),
|
center = Offset(center.x, center.y - feltRadius * 0.18f),
|
||||||
radius = size.maxDimension * 0.67f,
|
radius = feltRadius * 1.15f,
|
||||||
),
|
),
|
||||||
topLeft = feltOffset,
|
radius = feltRadius,
|
||||||
size = feltSize,
|
center = center,
|
||||||
)
|
)
|
||||||
drawOval(
|
drawCircle(
|
||||||
color = TableGold.copy(alpha = 0.34f),
|
color = TableGold.copy(alpha = 0.34f),
|
||||||
topLeft = feltOffset,
|
radius = feltRadius,
|
||||||
size = feltSize,
|
center = center,
|
||||||
style = Stroke(width = 1.dp.toPx()),
|
style = Stroke(width = 1.dp.toPx()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -311,14 +314,14 @@ private fun OpponentSeat(
|
|||||||
) {
|
) {
|
||||||
val faded = seat.folded && !isWinner
|
val faded = seat.folded && !isWinner
|
||||||
val revealed = seat.revealed && seat.hole != null
|
val revealed = seat.revealed && seat.hole != null
|
||||||
val faceWidth = 52.dp
|
val faceWidth = 50.dp
|
||||||
val faceHeight = 78.dp
|
val faceHeight = 75.dp
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(if (revealed) 90.dp else 76.dp)
|
.width(if (revealed) 108.dp else 88.dp)
|
||||||
.alpha(if (faded) 0.38f else 1f),
|
.alpha(if (faded) 0.38f else 1f),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
val highlight = when {
|
val highlight = when {
|
||||||
@@ -333,12 +336,12 @@ private fun OpponentSeat(
|
|||||||
color = highlight,
|
color = highlight,
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
)
|
)
|
||||||
.padding(3.dp),
|
.padding(2.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
val hole = seat.hole
|
val hole = seat.hole
|
||||||
if (revealed && hole != null) {
|
if (revealed && hole != null) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy((-18).dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
repeat(2) { index ->
|
repeat(2) { index ->
|
||||||
CardImage(
|
CardImage(
|
||||||
hole.getOrNull(index),
|
hole.getOrNull(index),
|
||||||
@@ -347,7 +350,19 @@ private fun OpponentSeat(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
) {
|
||||||
OpponentAvatar(seat)
|
OpponentAvatar(seat)
|
||||||
|
if (!seat.folded) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy((-5).dp)) {
|
||||||
|
repeat(2) {
|
||||||
|
CardImage(null, Modifier.size(18.dp, 27.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (seat.isButton) {
|
if (seat.isButton) {
|
||||||
@@ -373,7 +388,7 @@ private fun OpponentSeat(
|
|||||||
seat.name,
|
seat.name,
|
||||||
color = Cream,
|
color = Cream,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 10.sp,
|
fontSize = 12.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -389,7 +404,7 @@ private fun OpponentSeat(
|
|||||||
else -> TableGold.copy(alpha = 0.9f)
|
else -> TableGold.copy(alpha = 0.9f)
|
||||||
},
|
},
|
||||||
fontWeight = if (seat.allIn || isWinner) FontWeight.Bold else FontWeight.Medium,
|
fontWeight = if (seat.allIn || isWinner) FontWeight.Bold else FontWeight.Medium,
|
||||||
fontSize = 10.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,7 +421,7 @@ private fun OpponentSeat(
|
|||||||
private fun OpponentAvatar(seat: SeatSnapshot) {
|
private fun OpponentAvatar(seat: SeatSnapshot) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(44.dp)
|
.size(48.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(
|
.background(
|
||||||
Brush.linearGradient(
|
Brush.linearGradient(
|
||||||
@@ -419,20 +434,8 @@ private fun OpponentAvatar(seat: SeatSnapshot) {
|
|||||||
seat.name.take(1).uppercase(),
|
seat.name.take(1).uppercase(),
|
||||||
color = GoldLight,
|
color = GoldLight,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 16.sp,
|
fontSize = 18.sp,
|
||||||
)
|
)
|
||||||
if (!seat.folded) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.offset(x = 5.dp, y = 2.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy((-6).dp),
|
|
||||||
) {
|
|
||||||
repeat(2) {
|
|
||||||
CardImage(null, Modifier.size(14.dp, 21.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +483,7 @@ private fun HeroSeat(
|
|||||||
seat?.name ?: "You",
|
seat?.name ?: "You",
|
||||||
color = Cream,
|
color = Cream,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
fontSize = 14.sp,
|
fontSize = 15.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -488,7 +491,7 @@ private fun HeroSeat(
|
|||||||
"${seat?.stack ?: 0}",
|
"${seat?.stack ?: 0}",
|
||||||
color = TableGold,
|
color = TableGold,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 14.sp,
|
fontSize = 15.sp,
|
||||||
)
|
)
|
||||||
when {
|
when {
|
||||||
folded -> StatusTag("FOLDED", Danger)
|
folded -> StatusTag("FOLDED", Danger)
|
||||||
@@ -511,8 +514,8 @@ private fun BoardAndStatus(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val board = snapshot?.board.orEmpty()
|
val board = snapshot?.board.orEmpty()
|
||||||
val cardWidth = 52.dp
|
val cardWidth = 58.dp
|
||||||
val cardHeight = 78.dp
|
val cardHeight = 87.dp
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
@@ -548,12 +551,12 @@ private fun BoardAndStatus(
|
|||||||
Text(
|
Text(
|
||||||
"${it.speakerName}: “${it.text}”",
|
"${it.speakerName}: “${it.text}”",
|
||||||
color = Cream.copy(alpha = 0.9f),
|
color = Cream.copy(alpha = 0.9f),
|
||||||
fontSize = 11.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.width(260.dp),
|
modifier = Modifier.width(292.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -567,15 +570,16 @@ private fun PotPill(pot: Int) {
|
|||||||
border = androidx.compose.foundation.BorderStroke(1.dp, TableGold.copy(alpha = 0.32f)),
|
border = androidx.compose.foundation.BorderStroke(1.dp, TableGold.copy(alpha = 0.32f)),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 15.dp, vertical = 7.dp),
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 7.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
|
ChipStack(pot, chipSize = 18.dp)
|
||||||
Text(
|
Text(
|
||||||
"POT",
|
"POT",
|
||||||
color = TableGold.copy(alpha = 0.72f),
|
color = TableGold.copy(alpha = 0.85f),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 9.sp,
|
fontSize = 10.sp,
|
||||||
letterSpacing = 1.8.sp,
|
letterSpacing = 1.8.sp,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -602,13 +606,13 @@ private fun StatusPill(text: String, accent: Boolean) {
|
|||||||
text,
|
text,
|
||||||
color = if (accent) GoldLight else Cream.copy(alpha = 0.8f),
|
color = if (accent) GoldLight else Cream.copy(alpha = 0.8f),
|
||||||
fontWeight = if (accent) FontWeight.Bold else FontWeight.Medium,
|
fontWeight = if (accent) FontWeight.Bold else FontWeight.Medium,
|
||||||
fontSize = 10.sp,
|
fontSize = 12.sp,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(250.dp)
|
.width(286.dp)
|
||||||
.padding(horizontal = 11.dp, vertical = 5.dp),
|
.padding(horizontal = 11.dp, vertical = 7.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,12 +623,12 @@ private fun BetMarker(amount: Int) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
ChipDot(Color(0xFFE9E7DD), 14.dp)
|
ChipStack(amount, chipSize = 14.dp)
|
||||||
Text(
|
Text(
|
||||||
"$amount",
|
"$amount",
|
||||||
color = Cream.copy(alpha = 0.9f),
|
color = Cream.copy(alpha = 0.9f),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 10.sp,
|
fontSize = 12.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,22 +658,50 @@ private fun StatusTag(text: String, color: Color) {
|
|||||||
text,
|
text,
|
||||||
color = color,
|
color = color,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
fontSize = 9.sp,
|
fontSize = 10.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.5.sp,
|
||||||
modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.dp),
|
modifier = Modifier.padding(horizontal = 7.dp, vertical = 3.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ChipStack(amount: Int, chipSize: Dp) {
|
||||||
|
val colors = listOf(
|
||||||
|
Color(0xFFE7E4D9),
|
||||||
|
Color(0xFF2A9E6B),
|
||||||
|
Color(0xFF2F72C4),
|
||||||
|
Color(0xFFE2B85B),
|
||||||
|
Color(0xFFB94B52),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy((-4).dp),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
) {
|
||||||
|
repeat(visibleChipCount(amount)) { index ->
|
||||||
|
Box(Modifier.padding(bottom = ((index % 3) * 3).dp)) {
|
||||||
|
ChipDot(colors[index % colors.size], chipSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChipDot(color: Color, size: Dp) {
|
private fun ChipDot(color: Color, size: Dp) {
|
||||||
Canvas(Modifier.size(size)) {
|
Canvas(Modifier.size(size)) {
|
||||||
drawCircle(color = color)
|
val radius = this.size.minDimension * 0.48f
|
||||||
|
drawCircle(color = Color.Black.copy(alpha = 0.34f), radius = radius)
|
||||||
|
drawCircle(color = color, radius = radius * 0.91f)
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.White.copy(alpha = 0.48f),
|
color = Color.White.copy(alpha = 0.62f),
|
||||||
radius = this.size.minDimension * 0.34f,
|
radius = radius * 0.63f,
|
||||||
style = Stroke(width = 1.dp.toPx()),
|
style = Stroke(width = 1.2.dp.toPx()),
|
||||||
)
|
)
|
||||||
|
val mark = radius * 0.63f
|
||||||
|
drawLine(Color.White.copy(alpha = 0.78f), Offset(center.x, center.y - radius), Offset(center.x, center.y - mark), 1.5.dp.toPx())
|
||||||
|
drawLine(Color.White.copy(alpha = 0.78f), Offset(center.x, center.y + mark), Offset(center.x, center.y + radius), 1.5.dp.toPx())
|
||||||
|
drawLine(Color.White.copy(alpha = 0.78f), Offset(center.x - radius, center.y), Offset(center.x - mark, center.y), 1.5.dp.toPx())
|
||||||
|
drawLine(Color.White.copy(alpha = 0.78f), Offset(center.x + mark, center.y), Offset(center.x + radius, center.y), 1.5.dp.toPx())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,27 +781,41 @@ private fun DecisionControls(
|
|||||||
"RAISE TO",
|
"RAISE TO",
|
||||||
color = Muted.copy(alpha = 0.72f),
|
color = Muted.copy(alpha = 0.72f),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 10.sp,
|
fontSize = 12.sp,
|
||||||
letterSpacing = 1.4.sp,
|
letterSpacing = 1.4.sp,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"$raiseTo",
|
"$raiseTo",
|
||||||
color = GoldLight,
|
color = GoldLight,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
fontSize = 19.sp,
|
fontSize = 22.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
FineTuneButton("−") {
|
||||||
|
raiseTo = adjustRaiseAmount(raiseTo, -1, buttons)
|
||||||
|
}
|
||||||
Slider(
|
Slider(
|
||||||
value = raiseTo.coerceIn(buttons.sliderMin, buttons.sliderMax).toFloat(),
|
value = raiseTo.coerceIn(buttons.sliderMin, buttons.sliderMax).toFloat(),
|
||||||
onValueChange = { raiseTo = it.toInt() },
|
onValueChange = { raiseTo = it.roundToInt() },
|
||||||
valueRange = buttons.sliderMin.toFloat()..buttons.sliderMax.toFloat(),
|
valueRange = buttons.sliderMin.toFloat()..buttons.sliderMax.toFloat(),
|
||||||
colors = SliderDefaults.colors(
|
colors = SliderDefaults.colors(
|
||||||
thumbColor = GoldLight,
|
thumbColor = GoldLight,
|
||||||
activeTrackColor = TableGold,
|
activeTrackColor = TableGold,
|
||||||
inactiveTrackColor = Color.White.copy(alpha = 0.12f),
|
inactiveTrackColor = Color.White.copy(alpha = 0.12f),
|
||||||
),
|
),
|
||||||
modifier = Modifier.height(26.dp),
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(44.dp),
|
||||||
)
|
)
|
||||||
|
FineTuneButton("+") {
|
||||||
|
raiseTo = adjustRaiseAmount(raiseTo, 1, buttons)
|
||||||
|
}
|
||||||
|
}
|
||||||
val presets = raisePresets(offer, buttons)
|
val presets = raisePresets(offer, buttons)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -780,8 +826,8 @@ private fun DecisionControls(
|
|||||||
onClick = { raiseTo = preset.amount },
|
onClick = { raiseTo = preset.amount },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.height(32.dp),
|
.height(42.dp),
|
||||||
contentPadding = ButtonDefaults.ContentPadding,
|
contentPadding = PaddingValues(horizontal = 4.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = if (raiseTo == preset.amount) {
|
containerColor = if (raiseTo == preset.amount) {
|
||||||
TableGold.copy(alpha = 0.2f)
|
TableGold.copy(alpha = 0.2f)
|
||||||
@@ -795,7 +841,7 @@ private fun DecisionControls(
|
|||||||
Text(
|
Text(
|
||||||
"${preset.label} ${preset.amount}",
|
"${preset.label} ${preset.amount}",
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 9.sp,
|
fontSize = 11.sp,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -848,6 +894,25 @@ private fun DecisionControls(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FineTuneButton(
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.size(44.dp),
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Color.White.copy(alpha = 0.09f),
|
||||||
|
contentColor = GoldLight,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
) {
|
||||||
|
Text(label, fontWeight = FontWeight.ExtraBold, fontSize = 24.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActionButton(
|
private fun ActionButton(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -859,7 +924,7 @@ private fun ActionButton(
|
|||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.height(54.dp),
|
modifier = modifier.height(60.dp),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(containerColor = container, contentColor = content),
|
colors = ButtonDefaults.buttonColors(containerColor = container, contentColor = content),
|
||||||
) {
|
) {
|
||||||
@@ -879,7 +944,7 @@ private fun ActionButton(
|
|||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.height(54.dp)
|
.height(60.dp)
|
||||||
.background(container, RoundedCornerShape(14.dp)),
|
.background(container, RoundedCornerShape(14.dp)),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(14.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
@@ -894,9 +959,9 @@ private fun ActionButton(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ActionButtonText(title: String, detail: String?, color: Color) {
|
private fun ActionButtonText(title: String, detail: String?, color: Color) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Text(title, color = color, fontWeight = FontWeight.ExtraBold, fontSize = 14.sp)
|
Text(title, color = color, fontWeight = FontWeight.ExtraBold, fontSize = 16.sp)
|
||||||
detail?.let {
|
detail?.let {
|
||||||
Text(it, color = color.copy(alpha = 0.68f), fontWeight = FontWeight.Bold, fontSize = 10.sp)
|
Text(it, color = color.copy(alpha = 0.78f), fontWeight = FontWeight.Bold, fontSize = 12.sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -911,9 +976,9 @@ private fun ShowdownModal(summary: HandSummary) {
|
|||||||
shadowElevation = 14.dp,
|
shadowElevation = 14.dp,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 15.dp),
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(7.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (summary.wentToShowdown) "SHOWDOWN" else "HAND COMPLETE",
|
if (summary.wentToShowdown) "SHOWDOWN" else "HAND COMPLETE",
|
||||||
@@ -949,23 +1014,43 @@ private fun ShowdownModal(summary: HandSummary) {
|
|||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
summary.board.forEach { card ->
|
summary.board.forEach { card ->
|
||||||
CardImage(card, Modifier.size(46.dp, 69.dp))
|
CardImage(card, Modifier.size(50.dp, 75.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (summary.showdownPlayers.isNotEmpty()) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"ALL SHOWDOWN HANDS",
|
||||||
|
color = Muted.copy(alpha = 0.9f),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
letterSpacing = 1.2.sp,
|
||||||
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState()),
|
.horizontalScroll(rememberScrollState()),
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
summary.awards.forEach { award ->
|
summary.showdownPlayers.forEach { player ->
|
||||||
PotAwardCard(award)
|
ShowdownPlayerCard(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
summary.awards.forEach { award -> PotAwardCard(award) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1014,70 +1099,91 @@ private fun CompletionControls(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PotAwardCard(award: PotAwardSummary) {
|
private fun ShowdownPlayerCard(player: ShowdownPlayerSummary) {
|
||||||
// For a split pot, showing only the first winner's cards would imply those
|
|
||||||
// cards alone explain the award. The modal retains the board and names every
|
|
||||||
// tied winner with their exact payout instead.
|
|
||||||
val visibleCards = award.winners
|
|
||||||
.singleOrNull()
|
|
||||||
?.hole
|
|
||||||
.orEmpty()
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.width(310.dp),
|
modifier = Modifier.width(118.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (player.amountWon > 0) {
|
||||||
|
TableGold.copy(alpha = 0.13f)
|
||||||
|
} else {
|
||||||
|
Color.White.copy(alpha = 0.06f)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
border = androidx.compose.foundation.BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
if (player.amountWon > 0) TableGold.copy(alpha = 0.62f) else Color.White.copy(alpha = 0.1f),
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(7.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
player.hole.take(2).forEach { card ->
|
||||||
|
CardImage(card, Modifier.size(48.dp, 72.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
player.label,
|
||||||
|
color = Cream,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
player.handDescription,
|
||||||
|
color = if (player.amountWon > 0) GoldLight else Cream.copy(alpha = 0.84f),
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
if (player.amountWon > 0) "WON ${player.amountWon}" else "SHOWED",
|
||||||
|
color = if (player.amountWon > 0) TableGold else Muted,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PotAwardCard(award: PotAwardSummary) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.07f)),
|
colors = CardDefaults.cardColors(containerColor = Color.White.copy(alpha = 0.07f)),
|
||||||
border = androidx.compose.foundation.BorderStroke(1.dp, TableGold.copy(alpha = 0.38f)),
|
border = androidx.compose.foundation.BorderStroke(1.dp, TableGold.copy(alpha = 0.38f)),
|
||||||
shape = RoundedCornerShape(14.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 11.dp, vertical = 10.dp),
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
|
||||||
if (visibleCards.isNotEmpty()) {
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
visibleCards.take(2).forEach { card ->
|
|
||||||
CardImage(card, Modifier.size(76.dp, 114.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"${award.label.uppercase()} ${award.amount}",
|
"${award.label.uppercase()} ${award.amount}",
|
||||||
color = TableGold,
|
color = TableGold,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
fontWeight = FontWeight.ExtraBold,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
letterSpacing = 0.5.sp,
|
letterSpacing = 0.4.sp,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
if (award.winners.size == 1) {
|
award.winners.joinToString(" • ") { "${it.label} ${it.amountWon}" },
|
||||||
award.winners.single().label
|
modifier = Modifier.weight(1f),
|
||||||
} else {
|
|
||||||
award.winners.joinToString(" • ") { "${it.label} ${it.amountWon}" }
|
|
||||||
},
|
|
||||||
color = Cream,
|
color = Cream,
|
||||||
fontWeight = FontWeight.ExtraBold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
val descriptions = award.winners.mapNotNull(AwardWinner::handDescription).distinct()
|
|
||||||
if (descriptions.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
descriptions.joinToString(" / "),
|
|
||||||
color = GoldLight,
|
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 14.sp,
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CoachBar(presentation: CoachPresentation) {
|
private fun CoachBar(presentation: CoachPresentation) {
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ class ActionButtonsTest {
|
|||||||
assertEquals("Raise 60", raiseLabel(b, 60))
|
assertEquals("Raise 60", raiseLabel(b, 60))
|
||||||
assertEquals("All in", raiseLabel(b, 500))
|
assertEquals("All in", raiseLabel(b, 500))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `fine tuning moves one chip and cannot leave the legal range`() {
|
||||||
|
val b = buttonsFor(offer(minRaiseTo = 20, maxRaiseTo = 500))
|
||||||
|
|
||||||
|
assertEquals(59, adjustRaiseAmount(60, -1, b))
|
||||||
|
assertEquals(61, adjustRaiseAmount(60, 1, b))
|
||||||
|
assertEquals(20, adjustRaiseAmount(20, -1, b))
|
||||||
|
assertEquals(500, adjustRaiseAmount(500, 1, b))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CallCostTest {
|
class CallCostTest {
|
||||||
|
|||||||
@@ -146,6 +146,36 @@ class HandSummaryTest {
|
|||||||
assertEquals("Ada", summary.awards[1].winners.single().label)
|
assertEquals("Ada", summary.awards[1].winners.single().label)
|
||||||
assertEquals(kings.toList(), summary.awards[1].winners.single().hole)
|
assertEquals(kings.toList(), summary.awards[1].winners.single().hole)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `showdown lists every public contender including hands that lost`() {
|
||||||
|
val board = cardsOf("2c 7d 9s Jc 3h")
|
||||||
|
val aces = cardsOf("Ah Ad")
|
||||||
|
val kings = cardsOf("Kh Kd")
|
||||||
|
val result = HandResult(
|
||||||
|
board = board,
|
||||||
|
net = intArrayOf(10, -10),
|
||||||
|
wentToShowdown = true,
|
||||||
|
potSize = 20,
|
||||||
|
events = emptyList(),
|
||||||
|
showdownHands = listOf(
|
||||||
|
ShowdownHand(0, aces.toList(), HandEvaluator.evaluate(aces + board)),
|
||||||
|
ShowdownHand(1, kings.toList(), HandEvaluator.evaluate(kings + board)),
|
||||||
|
),
|
||||||
|
potAwards = listOf(PotAward(20, listOf(PotPayout(0, 20)))),
|
||||||
|
)
|
||||||
|
|
||||||
|
val summary = handSummaryFor(
|
||||||
|
handNumber = 12,
|
||||||
|
result = result,
|
||||||
|
seatNames = listOf("Jay", "Ada"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("You", "Ada"), summary.showdownPlayers.map { it.label })
|
||||||
|
assertEquals(listOf(20, 0), summary.showdownPlayers.map { it.amountWon })
|
||||||
|
assertEquals(listOf("Pair", "Pair"), summary.showdownPlayers.map { it.handDescription })
|
||||||
|
assertEquals(kings.toList(), summary.showdownPlayers[1].hole)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VisibleHandSummaryTest {
|
class VisibleHandSummaryTest {
|
||||||
@@ -158,6 +188,7 @@ class VisibleHandSummaryTest {
|
|||||||
potSize = 20,
|
potSize = 20,
|
||||||
heroNet = -4,
|
heroNet = -4,
|
||||||
wentToShowdown = true,
|
wentToShowdown = true,
|
||||||
|
showdownPlayers = emptyList(),
|
||||||
awards = emptyList(),
|
awards = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,29 @@ import org.junit.Test
|
|||||||
class TableOrbitTest {
|
class TableOrbitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `five opponents form a symmetric upper ellipse above the board band`() {
|
fun `five opponents form a symmetric perimeter around the circular felt`() {
|
||||||
val positions = opponentSeatOrbit()
|
val positions = opponentSeatOrbit()
|
||||||
|
|
||||||
assertEquals(5, positions.size)
|
assertEquals(5, positions.size)
|
||||||
assertTrue(positions.zipWithNext().all { (left, right) -> left.centerX < right.centerX })
|
assertTrue(positions.zipWithNext().all { (left, right) -> left.centerX < right.centerX })
|
||||||
assertTrue(positions.all { it.centerX in 0f..1f })
|
assertTrue(positions.all { it.centerX in 0f..1f })
|
||||||
assertTrue(
|
assertTrue(
|
||||||
"opponent tops must stay in the upper fifth, leaving the middle to the board",
|
"all opponents stay around the upper perimeter",
|
||||||
positions.all { it.topY < 0.21f },
|
positions.all { it.topY < 0.30f },
|
||||||
)
|
)
|
||||||
assertEquals(1f, positions.first().centerX + positions.last().centerX, 0.001f)
|
assertEquals(1f, positions.first().centerX + positions.last().centerX, 0.001f)
|
||||||
assertEquals(positions.first().topY, positions.last().topY, 0.001f)
|
assertEquals(positions.first().topY, positions.last().topY, 0.001f)
|
||||||
assertEquals(1f, positions[1].centerX + positions[3].centerX, 0.001f)
|
assertEquals(1f, positions[1].centerX + positions[3].centerX, 0.001f)
|
||||||
assertEquals(positions[1].topY, positions[3].topY, 0.001f)
|
assertEquals(positions[1].topY, positions[3].topY, 0.001f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `visible chips accumulate monotonically but remain bounded`() {
|
||||||
|
val amounts = listOf(0, 1, 3, 7, 20, 50, 100, 500)
|
||||||
|
val counts = amounts.map(::visibleChipCount)
|
||||||
|
|
||||||
|
assertEquals(0, counts.first())
|
||||||
|
assertEquals(7, counts.last())
|
||||||
|
assertTrue(counts.zipWithNext().all { (left, right) -> left <= right })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user