Polish table and clarify showdown results

This commit is contained in:
Jay
2026-07-26 21:46:28 -04:00
parent b8087a9872
commit df4d90f40b
9 changed files with 1625 additions and 277 deletions
+8
View File
@@ -28,6 +28,7 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
| `engine/src/commonMain/.../game/` | `Table` — betting rounds, side pots, showdown |
| `sim/` | JVM-only headless simulator used to **tune** bot profiles |
| `app/` | Android app: Compose table, `PokerViewModel` |
| `designs/` | Visual direction and interaction references; current table follows 1A |
| `assets/cards/` | 52 CC0 card faces + generated backs (**source of truth**) |
| `tools/generate_card_assets.sh` | Rasterises those SVGs into `app/.../drawable-*` |
@@ -139,9 +140,16 @@ export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
Persona output is accepted only beside the exact hand, street, and action
number that requested it; a late response is dropped rather than shown against
a newer decision.
- A completed hand cannot be reduced to one flat winner list. `HandResult`
retains each `PotAward` in main/side-pot order and only the hands publicly
revealed at showdown. The result UI reads those authoritative awards, so two
players winning different pots are never presented as though they tied.
- Android is playable in Compose. Engine snapshots are paced through 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.
The portrait table follows the 1A design direction: oval felt, orbiting seats,
spatial bets, large 2:3 card art, always-visible legal sizing controls, and a
winner-focused pot/hand breakdown at completion.
- 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.
@@ -22,6 +22,11 @@ data class ActionButtons(
val fixedRaiseTo: Int,
)
data class RaisePreset(
val label: String,
val amount: Int,
)
fun buttonsFor(offer: DecisionOffer): ActionButtons {
// When a stack cannot cover a full min-raise it may still shove; the engine
// clamps such a raise to maxRaiseTo. Requiring maxRaiseTo > minRaiseTo to show
@@ -55,3 +60,35 @@ fun buttonsFor(offer: DecisionOffer): ActionButtons {
/** Label for the raise button at [amount]. */
fun raiseLabel(buttons: ActionButtons, amount: Int): String =
if (amount >= buttons.sliderMax) "All in" else "Raise $amount"
/**
* Honest, legal raise-to shortcuts for the always-visible sizing rail.
*
* Facing a bet, a fraction describes the extra raise after calling: the pot
* after a call is [DecisionOffer.pot] plus the affordable call, and the final
* round commitment is the current bet plus that fraction. Every result is
* clamped to the exact engine-provided range; duplicates disappear rather than
* showing several labels that submit the same action.
*/
fun raisePresets(
offer: DecisionOffer,
buttons: ActionButtons = buttonsFor(offer),
): List<RaisePreset> {
if (!buttons.showRaise) return emptyList()
val low = buttons.sliderMin
val high = buttons.sliderMax
if (low >= high) return listOf(RaisePreset("All in", high))
val potAfterCall = offer.pot + offer.callAmount
val halfPot = (offer.currentBet + potAfterCall / 2).coerceIn(low, high)
val fullPot = (offer.currentBet + potAfterCall).coerceIn(low, high)
return buildList {
add(RaisePreset("Min", low))
if (halfPot in (low + 1) until high) add(RaisePreset("½ pot", halfPot))
if (fullPot in (low + 1) until high && fullPot != halfPot) {
add(RaisePreset("Pot", fullPot))
}
add(RaisePreset("All in", high))
}
}
@@ -1,7 +1,22 @@
package com.jsjdesigns.poker
import com.jsjdesigns.poker.core.HandEvaluator
import com.jsjdesigns.poker.game.HandResult
data class AwardWinner(
val seat: Int,
val label: String,
val amountWon: Int,
val hole: List<Int>,
val handDescription: String?,
)
data class PotAwardSummary(
val label: String,
val amount: Int,
val winners: List<AwardWinner>,
)
/**
* Immutable, presentation-ready outcome for one completed hand.
*
@@ -18,6 +33,8 @@ data class HandSummary(
val potSize: Int,
val heroNet: Int,
val wentToShowdown: Boolean,
/** Main/side-pot breakdown with public winning cards and evaluated hand names. */
val awards: List<PotAwardSummary>,
) {
val title: String
get() = when {
@@ -47,6 +64,40 @@ fun handSummaryFor(
require(heroSeat in result.net.indices) { "hero seat is outside the result" }
require(result.winners.isNotEmpty()) { "a completed hand must have a winner" }
require(result.winners.all { it in seatNames.indices }) { "winner seat is outside the roster" }
require(result.potAwards.isNotEmpty()) { "a completed hand must award at least one pot" }
require(result.potAwards.sumOf { it.amount } == result.potSize) {
"pot awards must account for the contested pot"
}
val showdownBySeat = result.showdownHands.associateBy { it.seat }
if (result.wentToShowdown) {
require(result.potAwards.flatMap { it.payouts }.all { it.seat in showdownBySeat }) {
"every showdown winner must include its public hand"
}
}
val multiplePots = result.potAwards.size > 1
val awards = result.potAwards.mapIndexed { index, award ->
require(award.payouts.all { it.seat in seatNames.indices }) {
"pot winner seat is outside the roster"
}
PotAwardSummary(
label = when {
!multiplePots -> "Pot"
index == 0 -> "Main pot"
else -> "Side pot $index"
},
amount = award.amount,
winners = award.payouts.map { payout ->
val showdown = showdownBySeat[payout.seat]
AwardWinner(
seat = payout.seat,
label = if (payout.seat == heroSeat) "You" else seatNames[payout.seat],
amountWon = payout.amount,
hole = showdown?.hole.orEmpty(),
handDescription = showdown?.let { HandEvaluator.describe(it.score) },
)
},
)
}
return HandSummary(
handNumber = handNumber,
@@ -57,5 +108,6 @@ fun handSummaryFor(
potSize = result.potSize,
heroNet = result.net[heroSeat],
wentToShowdown = result.wentToShowdown,
awards = awards,
)
}
File diff suppressed because it is too large Load Diff
@@ -78,6 +78,47 @@ class ActionButtonsTest {
assertFalse(buttonsFor(offer(canRaise = false)).showRaise)
}
@Test
fun `raise presets use the pot after calling and legal raise-to amounts`() {
val decision = offer(
toCall = 10,
minRaiseTo = 20,
maxRaiseTo = 500,
)
assertEquals(
listOf(
RaisePreset("Min", 20),
RaisePreset("½ pot", 35),
RaisePreset("Pot", 60),
RaisePreset("All in", 500),
),
raisePresets(decision),
)
}
@Test
fun `raise presets drop clamped duplicates instead of mislabelling them`() {
val decision = offer(
toCall = 10,
minRaiseTo = 45,
maxRaiseTo = 50,
stack = 50,
)
assertEquals(
listOf(RaisePreset("Min", 45), RaisePreset("All in", 50)),
raisePresets(decision),
)
}
@Test
fun `a single legal shove has one all-in preset`() {
val decision = offer(minRaiseTo = 200, maxRaiseTo = 120, stack = 120)
assertEquals(listOf(RaisePreset("All in", 120)), raisePresets(decision))
}
// ---------- fold and call ----------
@Test
@@ -1,6 +1,11 @@
package com.jsjdesigns.poker
import com.jsjdesigns.poker.core.HandEvaluator
import com.jsjdesigns.poker.core.cardsOf
import com.jsjdesigns.poker.game.HandResult
import com.jsjdesigns.poker.game.PotAward
import com.jsjdesigns.poker.game.PotPayout
import com.jsjdesigns.poker.game.ShowdownHand
import com.jsjdesigns.poker.game.Street
import com.jsjdesigns.poker.game.TableSnapshot
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -20,14 +25,41 @@ class HandSummaryTest {
winners: List<Int> = listOf(1),
showdown: Boolean = true,
pot: Int = 16,
) = HandResult(
board = intArrayOf(0, 1, 2, 3, 4),
awards: List<PotAward>? = null,
): HandResult {
val board = cardsOf("2c 7d 9s Jc 3h")
val holes = listOf(cardsOf("Ah Ad"), cardsOf("Kh Kd"), cardsOf("Qh Qd"))
return HandResult(
board = board,
net = net,
winners = winners,
wentToShowdown = showdown,
potSize = pot,
events = emptyList(),
showdownHands = if (showdown) {
winners.map { winner ->
val hole = holes[winner]
ShowdownHand(
seat = winner,
hole = hole.toList(),
score = HandEvaluator.evaluate(hole + board),
)
}
} else {
emptyList()
},
potAwards = awards ?: listOf(
PotAward(
amount = pot,
payouts = winners.mapIndexed { index, winner ->
PotPayout(
winner,
pot / winners.size + if (index < pot % winners.size) 1 else 0,
)
},
),
),
)
}
@Test
fun `summary names the winner and reports the hero's actual net`() {
@@ -76,6 +108,43 @@ class HandSummaryTest {
assertTrue(summary.heroWon)
assertTrue(summary.detail.endsWith("You broke even"))
}
@Test
fun `side pots name each award and the winning made hand`() {
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(100, 100, -200),
wentToShowdown = true,
potSize = 450,
events = emptyList(),
showdownHands = listOf(
ShowdownHand(0, aces.toList(), HandEvaluator.evaluate(aces + board)),
ShowdownHand(1, kings.toList(), HandEvaluator.evaluate(kings + board)),
),
potAwards = listOf(
PotAward(150, listOf(PotPayout(0, 150))),
PotAward(300, listOf(PotPayout(1, 300))),
),
)
val summary = handSummaryFor(
handNumber = 11,
result = result,
seatNames = listOf("Jay", "Ada", "Bruno"),
)
assertEquals("Main pot", summary.awards[0].label)
assertEquals(150, summary.awards[0].amount)
assertEquals("You", summary.awards[0].winners.single().label)
assertEquals(150, summary.awards[0].winners.single().amountWon)
assertEquals("Pair", summary.awards[0].winners.single().handDescription)
assertEquals("Side pot 1", summary.awards[1].label)
assertEquals("Ada", summary.awards[1].winners.single().label)
assertEquals(kings.toList(), summary.awards[1].winners.single().hole)
}
}
class VisibleHandSummaryTest {
@@ -87,6 +156,7 @@ class VisibleHandSummaryTest {
potSize = 20,
heroNet = -4,
wentToShowdown = true,
awards = emptyList(),
)
private fun snapshot(
+455
View File
@@ -0,0 +1,455 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<meta name="design_doc_mode" content="canvas">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Cormorant+Garamond:wght@600;700&display=swap" rel="stylesheet">
<style>
body { margin:0; background:#0B0F0D; font-family:Manrope, sans-serif; }
a { color:#E3C27E; text-decoration:none; }
a:hover { color:#F6DFA8; }
@keyframes seatPulse { 0%,100% { box-shadow:0 0 0 0 rgba(227,194,126,.55);} 50% { box-shadow:0 0 0 8px rgba(227,194,126,0);} }
@keyframes timerSpin { to { transform:rotate(360deg);} }
</style>
</helmet>
<section style="display:flex;flex-direction:column;gap:28px;padding:56px 64px 96px;background:#0B0F0D;color:#F3EFE6;min-height:100%;box-sizing:border-box">
<div style="display:flex;flex-direction:column;gap:10px;max-width:900px">
<div style="font:600 12px/1 Manrope;letter-spacing:.22em;text-transform:uppercase;color:#9E874F">Turn 1 · Hold'em table, portrait</div>
<h1 style="font:700 40px/1.1 'Cormorant Garamond', serif;margin:0;color:#F3EFE6">Polished table, two bet-sizing directions</h1>
<p style="font:400 15px/1.6 Manrope;margin:0;color:#9FB0A6;text-wrap:pretty">Same table treatment in both: oval felt with a rail, seats orbiting the pot instead of stacked in a row, chip stacks for live bets, a turn timer ring, bigger card faces, and the dead vertical space reclaimed. They differ only in how you size a raise — <a href="#1a">1a</a> keeps it always-visible, <a href="#1b">1b</a> hides it until you tap Raise and puts the slider under your thumb.</p>
</div>
<div style="display:flex;flex-wrap:wrap;gap:56px;align-items:flex-start">
<div id="1a" style="display:flex;flex-direction:column;gap:14px">
<div style="display:flex;align-items:center;gap:10px">
<span style="display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:24px;padding:0 8px;border-radius:6px;background:#E3C27E;color:#12281F;font:800 13px/1 Manrope">1a</span>
<span style="font:600 15px/1 Manrope;color:#F3EFE6">Always-on sizing rail</span>
</div>
<div style="width:390px;height:844px;border-radius:38px;overflow:hidden;position:relative;background:#08211A;box-shadow:0 30px 80px rgba(0,0,0,.6);display:flex;flex-direction:column">
<div style="height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 22px;font:600 13px/1 Manrope;color:rgba(243,239,230,.85);flex:none">
<span>20:50</span>
<span style="display:flex;gap:6px;align-items:center;opacity:.7">
<span style="width:14px;height:9px;border-radius:2px;background:currentColor;display:block"></span>
<span style="width:20px;height:10px;border-radius:3px;border:1.5px solid currentColor;display:block"></span>
</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:2px 18px 10px;flex:none">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:30px;height:30px;border-radius:9px;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center">
<div style="display:flex;flex-direction:column;gap:3px">
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:2px">
<span style="font:700 12px/1 Manrope;color:#F3EFE6">Hand 21</span>
<span style="font:500 10px/1 Manrope;letter-spacing:.14em;color:#7E9488">FLOP</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 11px;border-radius:999px;background:rgba(0,0,0,.28);border:1px solid rgba(227,194,126,.22)">
<span style="width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#F0D08A,#B98B2E);display:block"></span>
<span style="font:700 11px/1 Manrope;color:#E3C27E;letter-spacing:.04em">1 / 2</span>
</div>
</div>
<div style="position:relative;flex:1;min-height:0">
<div style="position:absolute;inset:-14px -30px 96px;border-radius:50%/34%;background:linear-gradient(180deg,#3B2A17,#221507);padding:10px;box-sizing:border-box;box-shadow:0 24px 60px rgba(0,0,0,.55)">
<div style="width:100%;height:100%;border-radius:50%/34%;border:1px solid rgba(227,194,126,.35);background:radial-gradient(ellipse at 50% 40%,#1E6E51 0%,#155440 48%,#0C3A2C 100%);box-shadow:inset 0 0 60px rgba(0,0,0,.45)"></div>
</div>
<div style="position:absolute;left:0;right:0;top:266px;display:flex;flex-direction:column;align-items:center;gap:14px">
<div style="display:flex;align-items:center;gap:9px;padding:7px 16px;border-radius:999px;background:rgba(4,20,14,.6);border:1px solid rgba(227,194,126,.3)">
<span style="font:600 10px/1 Manrope;letter-spacing:.2em;color:#9E874F">POT</span>
<span style="font:800 17px/1 Manrope;color:#F0D9A5">28</span>
</div>
<div style="display:flex;gap:7px">
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#17181A">Q</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#17181A"></span>
<span style="font:400 34px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#B4232C">7</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#B4232C"></span>
<span style="font:400 34px/1 Manrope;color:#B4232C"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#17181A">3</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#17181A"></span>
<span style="font:400 34px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;border:1px dashed rgba(227,194,126,.18);box-sizing:border-box"></div>
<div style="width:56px;height:78px;border-radius:8px;border:1px dashed rgba(227,194,126,.18);box-sizing:border-box"></div>
</div>
<div style="display:flex;align-items:flex-end;gap:6px;height:26px;margin-top:2px">
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#F2F2F2,#C6CBC9);border:3px dotted #7E1F1F;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45)"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#2A9E6B,#0E5B3D);border:3px dotted #D7F0E2;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45);margin-bottom:5px"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#2F72C4,#17457C);border:3px dotted #CFE0F5;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45)"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#F0D08A,#B98B2E);border:3px dotted #6B5320;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45);margin-bottom:9px"></span>
</div>
</div>
<div style="position:absolute;left:24px;top:176px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px;opacity:.45">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#2C4A3D,#163026);border:2px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#8FA79A">A</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.6);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#C8D3CC">Ada</span>
<span style="font:600 11px/1.2 Manrope;color:#7E9488">folded</span>
</div>
</div>
<div style="position:absolute;left:64px;top:52px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);border:2px solid rgba(227,194,126,.35);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">B</div>
<div style="position:absolute;right:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Bruno</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">304</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#F2F2F2,#C9CDCB);border:2px dotted #7E1F1F;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">6</span>
</div>
</div>
<div style="position:absolute;left:156px;top:14px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px;opacity:.45">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#2C4A3D,#163026);border:2px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#8FA79A">C</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.6);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#C8D3CC">Cleo</span>
<span style="font:600 11px/1.2 Manrope;color:#7E9488">folded</span>
</div>
</div>
<div style="position:absolute;left:248px;top:52px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);border:2px solid rgba(227,194,126,.35);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">D</div>
<div style="position:absolute;right:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
<span style="position:absolute;left:-8px;top:26px;width:19px;height:19px;border-radius:50%;background:linear-gradient(160deg,#FBF6EA,#D8CFB8);border:1px solid #A79463;display:flex;align-items:center;justify-content:center;font:800 9px/1 Manrope;color:#5A4A22">D</span>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Dex</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">613</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#2F72C4,#1B4B87);border:2px dotted #CFE0F5;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">6</span>
</div>
</div>
<div style="position:absolute;left:288px;top:176px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative;animation:seatPulse 1.6s ease-out infinite;border-radius:50%">
<div style="width:46px;height:46px;border-radius:50%;background:conic-gradient(#E3C27E 0turn 0.62turn, rgba(255,255,255,.12) 0.62turn 1turn);padding:2.5px;box-sizing:border-box">
<div style="width:100%;height:100%;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">E</div>
</div>
<div style="position:absolute;left:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Enzo</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">468</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#2A9E6B,#116343);border:2px dotted #D7F0E2;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">12</span>
</div>
</div>
<div style="position:absolute;left:0;right:0;bottom:110px;display:flex;justify-content:center">
<span style="padding:5px 13px;border-radius:999px;background:rgba(227,194,126,.14);border:1px solid rgba(227,194,126,.3);font:700 11px/1 Manrope;letter-spacing:.06em;color:#F0D9A5">Enzo raised to 12</span>
</div>
<div style="position:absolute;left:0;right:0;bottom:0;display:flex;align-items:flex-end;justify-content:center;gap:14px;padding:0 20px;box-sizing:border-box">
<div style="display:flex;align-items:flex-end">
<div style="width:76px;height:106px;border-radius:10px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 12px 24px rgba(0,0,0,.5);position:relative;display:flex;align-items:center;justify-content:center;transform:rotate(-5deg)">
<span style="position:absolute;top:7px;left:9px;font:800 23px/1 Manrope;color:#17181A">A</span>
<span style="position:absolute;top:31px;left:10px;font:400 15px/1 Manrope;color:#17181A"></span>
<span style="font:400 46px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:76px;height:106px;border-radius:10px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 12px 24px rgba(0,0,0,.5);position:relative;display:flex;align-items:center;justify-content:center;transform:rotate(4deg);margin-left:-14px">
<span style="position:absolute;top:7px;left:9px;font:800 23px/1 Manrope;color:#17181A">K</span>
<span style="position:absolute;top:31px;left:10px;font:400 15px/1 Manrope;color:#17181A"></span>
<span style="font:400 46px/1 Manrope;color:#17181A"></span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:5px;padding-bottom:8px">
<span style="font:800 15px/1 Manrope;color:#F3EFE6">Jay</span>
<span style="font:700 15px/1 Manrope;color:#E3C27E">220</span>
<span style="padding:4px 9px;border-radius:6px;background:rgba(42,158,107,.18);border:1px solid rgba(42,158,107,.4);font:700 10px/1 Manrope;letter-spacing:.04em;color:#8FE0B8">ACE HIGH · FLUSH DRAW</span>
</div>
</div>
</div>
<div style="flex:none;padding:12px 16px 22px;background:linear-gradient(180deg,rgba(4,16,12,0),rgba(4,16,12,.9) 30%,#04100C);display:flex;flex-direction:column;gap:11px">
<div style="display:flex;align-items:center;justify-content:space-between">
<span style="font:600 11px/1 Manrope;letter-spacing:.14em;color:#7E9488">RAISE TO</span>
<span style="font:800 20px/1 Manrope;color:#F0D9A5">36</span>
</div>
<div style="position:relative;height:22px;display:flex;align-items:center">
<div style="position:absolute;left:0;right:0;height:6px;border-radius:999px;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;left:0;width:34%;height:6px;border-radius:999px;background:linear-gradient(90deg,#B98B2E,#F0D08A)"></div>
<div style="position:absolute;left:calc(34% - 11px);width:22px;height:22px;border-radius:50%;background:radial-gradient(circle at 34% 30%,#FBF3DF,#D3B36B);border:2px solid #8C6E2E;box-shadow:0 3px 8px rgba(0,0,0,.5)"></div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:7px">
<button style="padding:8px 0;border-radius:9px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 11px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">Min 24</button>
<button style="padding:8px 0;border-radius:9px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 11px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">½ Pot</button>
<button style="padding:8px 0;border-radius:9px;border:1px solid rgba(227,194,126,.45);background:rgba(227,194,126,.16);color:#F0D9A5;font:700 11px/1 Manrope">Pot 40</button>
<button style="padding:8px 0;border-radius:9px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 11px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">All in</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1.15fr;gap:9px">
<button style="height:52px;border-radius:14px;border:1px solid rgba(196,74,74,.45);background:rgba(120,26,26,.35);color:#F0BDBD;font:800 15px/1 Manrope" style-hover="background:rgba(150,32,32,.55)">Fold</button>
<button style="height:52px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.08);color:#F3EFE6;font:800 15px/1 Manrope;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px" style-hover="background:rgba(255,255,255,.14)">
<span style="font:800 15px/1 Manrope">Call</span>
<span style="font:700 11px/1 Manrope;color:#9FB0A6">12</span>
</button>
<button style="height:52px;border-radius:14px;border:none;background:linear-gradient(180deg,#F0D08A,#C79A38);color:#22190A;font:800 15px/1 Manrope;box-shadow:0 8px 20px rgba(199,154,56,.32);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px" style-hover="filter:brightness(1.06)">
<span style="font:800 15px/1 Manrope">Raise</span>
<span style="font:700 11px/1 Manrope;color:rgba(34,25,10,.7)">to 36</span>
</button>
</div>
</div>
</div>
<p style="width:390px;font:400 13px/1.6 Manrope;color:#9FB0A6;margin:0;text-wrap:pretty">Sizing is always on screen, so raising is one tap. Costs ~90px of height — fine now that the table is an oval and the seats no longer eat a full band across the top.</p>
</div>
<div id="1b" style="display:flex;flex-direction:column;gap:14px">
<div style="display:flex;align-items:center;gap:10px">
<span style="display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:24px;padding:0 8px;border-radius:6px;background:#E3C27E;color:#12281F;font:800 13px/1 Manrope">1b</span>
<span style="font:600 15px/1 Manrope;color:#F3EFE6">Thumb-reach raise tray</span>
</div>
<div style="width:390px;height:844px;border-radius:38px;overflow:hidden;position:relative;background:#08211A;box-shadow:0 30px 80px rgba(0,0,0,.6);display:flex;flex-direction:column">
<div style="height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 22px;font:600 13px/1 Manrope;color:rgba(243,239,230,.85);flex:none">
<span>20:50</span>
<span style="display:flex;gap:6px;align-items:center;opacity:.7">
<span style="width:14px;height:9px;border-radius:2px;background:currentColor;display:block"></span>
<span style="width:20px;height:10px;border-radius:3px;border:1.5px solid currentColor;display:block"></span>
</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;padding:2px 18px 10px;flex:none">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:30px;height:30px;border-radius:9px;background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center">
<div style="display:flex;flex-direction:column;gap:3px">
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
<span style="width:12px;height:1.5px;background:#E3C27E;display:block"></span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:2px">
<span style="font:700 12px/1 Manrope;color:#F3EFE6">Hand 21</span>
<span style="font:500 10px/1 Manrope;letter-spacing:.14em;color:#7E9488">FLOP</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:6px;padding:6px 11px;border-radius:999px;background:rgba(0,0,0,.28);border:1px solid rgba(227,194,126,.22)">
<span style="width:11px;height:11px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#F0D08A,#B98B2E);display:block"></span>
<span style="font:700 11px/1 Manrope;color:#E3C27E;letter-spacing:.04em">1 / 2</span>
</div>
</div>
<div style="position:relative;flex:1;min-height:0">
<div style="position:absolute;inset:-14px -30px 96px;border-radius:50%/34%;background:linear-gradient(180deg,#3B2A17,#221507);padding:10px;box-sizing:border-box;box-shadow:0 24px 60px rgba(0,0,0,.55)">
<div style="width:100%;height:100%;border-radius:50%/34%;border:1px solid rgba(227,194,126,.35);background:radial-gradient(ellipse at 50% 40%,#1E6E51 0%,#155440 48%,#0C3A2C 100%);box-shadow:inset 0 0 60px rgba(0,0,0,.45)"></div>
</div>
<div style="position:absolute;left:0;right:0;top:266px;display:flex;flex-direction:column;align-items:center;gap:14px">
<div style="display:flex;align-items:center;gap:9px;padding:7px 16px;border-radius:999px;background:rgba(4,20,14,.6);border:1px solid rgba(227,194,126,.3)">
<span style="font:600 10px/1 Manrope;letter-spacing:.2em;color:#9E874F">POT</span>
<span style="font:800 17px/1 Manrope;color:#F0D9A5">28</span>
</div>
<div style="display:flex;gap:7px">
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#17181A">Q</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#17181A"></span>
<span style="font:400 34px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#B4232C">7</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#B4232C"></span>
<span style="font:400 34px/1 Manrope;color:#B4232C"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 8px 16px rgba(0,0,0,.45);position:relative;display:flex;align-items:center;justify-content:center">
<span style="position:absolute;top:5px;left:7px;font:800 17px/1 Manrope;color:#17181A">3</span>
<span style="position:absolute;top:23px;left:8px;font:400 12px/1 Manrope;color:#17181A"></span>
<span style="font:400 34px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:56px;height:78px;border-radius:8px;border:1px dashed rgba(227,194,126,.18);box-sizing:border-box"></div>
<div style="width:56px;height:78px;border-radius:8px;border:1px dashed rgba(227,194,126,.18);box-sizing:border-box"></div>
</div>
<div style="display:flex;align-items:flex-end;gap:6px;height:26px;margin-top:2px">
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#F2F2F2,#C6CBC9);border:3px dotted #7E1F1F;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45)"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#2A9E6B,#0E5B3D);border:3px dotted #D7F0E2;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45);margin-bottom:5px"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#2F72C4,#17457C);border:3px dotted #CFE0F5;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45)"></span>
<span style="width:20px;height:20px;border-radius:50%;background:radial-gradient(circle at 32% 28%,#F0D08A,#B98B2E);border:3px dotted #6B5320;box-sizing:border-box;display:block;box-shadow:0 3px 6px rgba(0,0,0,.45);margin-bottom:9px"></span>
</div>
</div>
<div style="position:absolute;left:24px;top:176px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px;opacity:.45">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#2C4A3D,#163026);border:2px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#8FA79A">A</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.6);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#C8D3CC">Ada</span>
<span style="font:600 11px/1.2 Manrope;color:#7E9488">folded</span>
</div>
</div>
<div style="position:absolute;left:64px;top:52px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);border:2px solid rgba(227,194,126,.35);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">B</div>
<div style="position:absolute;right:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Bruno</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">304</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#F2F2F2,#C9CDCB);border:2px dotted #7E1F1F;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">6</span>
</div>
</div>
<div style="position:absolute;left:156px;top:14px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px;opacity:.45">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#2C4A3D,#163026);border:2px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#8FA79A">C</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.6);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#C8D3CC">Cleo</span>
<span style="font:600 11px/1.2 Manrope;color:#7E9488">folded</span>
</div>
</div>
<div style="position:absolute;left:248px;top:52px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative">
<div style="width:46px;height:46px;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);border:2px solid rgba(227,194,126,.35);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">D</div>
<div style="position:absolute;right:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
<span style="position:absolute;left:-8px;top:26px;width:19px;height:19px;border-radius:50%;background:linear-gradient(160deg,#FBF6EA,#D8CFB8);border:1px solid #A79463;display:flex;align-items:center;justify-content:center;font:800 9px/1 Manrope;color:#5A4A22">D</span>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Dex</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">613</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#2F72C4,#1B4B87);border:2px dotted #CFE0F5;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">6</span>
</div>
</div>
<div style="position:absolute;left:288px;top:176px;display:flex;flex-direction:column;align-items:center;gap:5px;width:78px">
<div style="position:relative;animation:seatPulse 1.6s ease-out infinite;border-radius:50%">
<div style="width:46px;height:46px;border-radius:50%;background:conic-gradient(#E3C27E 0turn 0.62turn, rgba(255,255,255,.12) 0.62turn 1turn);padding:2.5px;box-sizing:border-box">
<div style="width:100%;height:100%;border-radius:50%;background:linear-gradient(160deg,#3A5C4B,#1B3A2D);display:flex;align-items:center;justify-content:center;font:700 17px/1 Manrope;color:#F0E4C8">E</div>
</div>
<div style="position:absolute;left:-4px;bottom:-2px;display:flex">
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block"></span>
<span style="width:15px;height:21px;border-radius:3px;background:linear-gradient(150deg,#9E2B2B,#6E1A1A);border:1px solid rgba(255,255,255,.5);display:block;margin-left:-7px"></span>
</div>
</div>
<div style="width:100%;border-radius:8px;background:rgba(4,18,13,.72);padding:4px 0;display:flex;flex-direction:column;align-items:center;gap:1px">
<span style="font:700 11px/1.2 Manrope;color:#F3EFE6">Enzo</span>
<span style="font:700 12px/1.2 Manrope;color:#E3C27E">468</span>
</div>
<div style="display:flex;align-items:center;gap:5px;margin-top:1px">
<span style="width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 32% 30%,#2A9E6B,#116343);border:2px dotted #D7F0E2;box-sizing:border-box;display:block"></span>
<span style="font:700 11px/1 Manrope;color:#CFE0D6">12</span>
</div>
</div>
<div style="position:absolute;right:12px;top:404px;height:246px;width:66px;border-radius:20px;background:rgba(4,16,12,.82);border:1px solid rgba(227,194,126,.24);box-shadow:0 12px 30px rgba(0,0,0,.5);display:flex;flex-direction:column;align-items:center;padding:12px 0;gap:10px">
<span style="font:800 16px/1 Manrope;color:#F0D9A5">36</span>
<div style="position:relative;flex:1;min-height:82px;width:12px;display:flex;justify-content:center">
<div style="width:6px;height:100%;border-radius:999px;background:rgba(255,255,255,.1)"></div>
<div style="position:absolute;bottom:0;width:6px;height:34%;border-radius:999px;background:linear-gradient(0deg,#B98B2E,#F0D08A)"></div>
<div style="position:absolute;bottom:calc(34% - 13px);width:26px;height:26px;border-radius:50%;background:radial-gradient(circle at 34% 30%,#FBF3DF,#D3B36B);border:2px solid #8C6E2E;box-shadow:0 3px 8px rgba(0,0,0,.5)"></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;width:52px">
<button style="padding:7px 0;border-radius:8px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 10px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">All in</button>
<button style="padding:7px 0;border-radius:8px;border:1px solid rgba(227,194,126,.45);background:rgba(227,194,126,.16);color:#F0D9A5;font:700 10px/1 Manrope">Pot</button>
<button style="padding:7px 0;border-radius:8px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 10px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">½</button>
<button style="padding:7px 0;border-radius:8px;border:1px solid rgba(227,194,126,.22);background:rgba(255,255,255,.05);color:#CFE0D6;font:700 10px/1 Manrope" style-hover="background:rgba(227,194,126,.16);color:#F0D9A5">Min</button>
</div>
</div>
<div style="position:absolute;left:0;right:0;bottom:110px;display:flex;justify-content:center">
<span style="padding:5px 13px;border-radius:999px;background:rgba(227,194,126,.14);border:1px solid rgba(227,194,126,.3);font:700 11px/1 Manrope;letter-spacing:.06em;color:#F0D9A5">Enzo raised to 12</span>
</div>
<div style="position:absolute;left:0;right:0;bottom:0;display:flex;align-items:flex-end;gap:14px;padding:0 20px;box-sizing:border-box">
<div style="display:flex;align-items:flex-end">
<div style="width:76px;height:106px;border-radius:10px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 12px 24px rgba(0,0,0,.5);position:relative;display:flex;align-items:center;justify-content:center;transform:rotate(-5deg)">
<span style="position:absolute;top:7px;left:9px;font:800 23px/1 Manrope;color:#17181A">A</span>
<span style="position:absolute;top:31px;left:10px;font:400 15px/1 Manrope;color:#17181A"></span>
<span style="font:400 46px/1 Manrope;color:#17181A"></span>
</div>
<div style="width:76px;height:106px;border-radius:10px;background:linear-gradient(180deg,#FFFDF8,#EFE9DC);box-shadow:0 12px 24px rgba(0,0,0,.5);position:relative;display:flex;align-items:center;justify-content:center;transform:rotate(4deg);margin-left:-14px">
<span style="position:absolute;top:7px;left:9px;font:800 23px/1 Manrope;color:#17181A">K</span>
<span style="position:absolute;top:31px;left:10px;font:400 15px/1 Manrope;color:#17181A"></span>
<span style="font:400 46px/1 Manrope;color:#17181A"></span>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:5px;padding-bottom:8px">
<span style="font:800 15px/1 Manrope;color:#F3EFE6">Jay</span>
<span style="font:700 15px/1 Manrope;color:#E3C27E">220</span>
<span style="padding:4px 9px;border-radius:6px;background:rgba(42,158,107,.18);border:1px solid rgba(42,158,107,.4);font:700 10px/1 Manrope;letter-spacing:.04em;color:#8FE0B8">ACE HIGH · FLUSH DRAW</span>
</div>
</div>
</div>
<div style="flex:none;padding:12px 16px 22px;background:linear-gradient(180deg,rgba(4,16,12,0),rgba(4,16,12,.9) 30%,#04100C);display:grid;grid-template-columns:1fr 1fr 1.15fr;gap:9px">
<button style="height:56px;border-radius:16px;border:1px solid rgba(196,74,74,.45);background:rgba(120,26,26,.35);color:#F0BDBD;font:800 15px/1 Manrope" style-hover="background:rgba(150,32,32,.55)">Fold</button>
<button style="height:56px;border-radius:16px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.08);color:#F3EFE6;font:800 15px/1 Manrope;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px" style-hover="background:rgba(255,255,255,.14)">
<span style="font:800 15px/1 Manrope">Call</span>
<span style="font:700 11px/1 Manrope;color:#9FB0A6">12</span>
</button>
<button style="height:56px;border-radius:16px;border:none;background:linear-gradient(180deg,#F0D08A,#C79A38);color:#22190A;font:800 15px/1 Manrope;box-shadow:0 8px 20px rgba(199,154,56,.32);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px" style-hover="filter:brightness(1.06)">
<span style="font:800 15px/1 Manrope">Raise</span>
<span style="font:700 11px/1 Manrope;color:rgba(34,25,10,.7)">to 36</span>
</button>
</div>
</div>
<p style="width:390px;font:400 13px/1.6 Manrope;color:#9FB0A6;margin:0;text-wrap:pretty">Tapping Raise opens a vertical tray at the right edge — the slider travels under your thumb instead of across the screen, and it costs zero permanent height. Second tap on Raise confirms.</p>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:18px;max-width:1000px;margin-top:8px">
<div style="flex:1 1 280px;display:flex;flex-direction:column;gap:6px;padding:18px 20px;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07)">
<span style="font:700 13px/1 Manrope;color:#E3C27E">What changed and why</span>
<span style="font:400 13px/1.65 Manrope;color:#9FB0A6;text-wrap:pretty">Oval felt with a rail gives the screen a subject; seats orbit the pot so position (dealer button, who acts next) is readable. Folded players dim out instead of staying at full weight. Bets become chip stacks sitting between seat and pot, so you read the action spatially, not as a text list.</span>
</div>
<div style="flex:1 1 280px;display:flex;flex-direction:column;gap:6px;padding:18px 20px;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07)">
<span style="font:700 13px/1 Manrope;color:#E3C27E">Legibility</span>
<span style="font:400 13px/1.65 Manrope;color:#9FB0A6;text-wrap:pretty">Hero cards are 76×106 with a single large pip and a clean corner index — no miniature pip grids. Board cards are 56×78 with dashed slots for undealt streets so the row never reflows. Stacks are gold, bets are white-on-chip, names are secondary.</span>
</div>
<div style="flex:1 1 280px;display:flex;flex-direction:column;gap:6px;padding:18px 20px;border-radius:14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07)">
<span style="font:700 13px/1 Manrope;color:#E3C27E">Still to add</span>
<span style="font:400 13px/1.65 Manrope;color:#9FB0A6;text-wrap:pretty">Deal/flip/chip-slide animations, pot-collect on win, all-in and showdown states, sound + haptics on your turn, and the landscape variant (same parts, seats spread wider, action bar bottom-right). Say the word and I'll storyboard those next.</span>
</div>
</div>
</section>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1100,&quot;height&quot;:1200}}"></script>
</body>
</html>
@@ -115,15 +115,57 @@ fun interface PlayerAgent {
data class Pot(val amount: Int, val eligible: List<Int>)
/** One public hand turned face-up at showdown, with its authoritative engine score. */
data class ShowdownHand(
val seat: Int,
val hole: List<Int>,
val score: Int,
)
data class PotPayout(
val seat: Int,
val amount: Int,
)
/**
* One settled pot in main-pot then side-pot order.
*
* A flat winner list cannot explain a side-pot hand: two players may both be
* winners without having tied or winning the same chips. Keeping each award
* and its exact payouts makes the completed-hand UI able to say exactly who won
* what, including the odd chip in a split.
*/
data class PotAward(
val amount: Int,
val payouts: List<PotPayout>,
) {
init {
require(amount > 0)
require(payouts.isNotEmpty())
require(payouts.all { it.amount > 0 })
require(payouts.map { it.seat }.distinct().size == payouts.size)
require(payouts.sumOf { it.amount } == amount)
}
val winnerSeats: List<Int> get() = payouts.map(PotPayout::seat)
}
data class HandResult(
val board: IntArray,
/** Net chip change per seat for this hand. */
val net: IntArray,
val winners: List<Int>,
val wentToShowdown: Boolean,
val potSize: Int,
val events: List<HandEvent>,
)
/** Empty for an uncontested hand; contains only cards publicly revealed at showdown. */
val showdownHands: List<ShowdownHand>,
/** Main pot first, then side pots from the shortest contribution upward. */
val potAwards: List<PotAward>,
) {
/** Unique winning seats in main/side-pot award order. */
val winners: List<Int>
get() = potAwards.flatMap(PotAward::winnerSeats).distinct()
}
/**
* A no-limit Texas Hold'em table.
@@ -565,7 +607,8 @@ class Table(
}
val contenders = seats.filter { it.contesting }
val winners = ArrayList<Int>()
val showdownHands = ArrayList<ShowdownHand>()
val potAwards = ArrayList<PotAward>()
var wentToShowdown = false
val potTotal = pot()
@@ -573,7 +616,7 @@ class Table(
val w = contenders.first()
w.stack += potTotal
for (s in seats) s.committedThisHand = 0
winners.add(w.index)
potAwards.add(PotAward(potTotal, listOf(PotPayout(w.index, potTotal))))
} else {
wentToShowdown = true
// Cards are face up from here, so snapshots may show them.
@@ -583,7 +626,15 @@ class Table(
val seven = IntArray(7)
seven[0] = c.hole[0]; seven[1] = c.hole[1]
for (k in board.indices) seven[2 + k] = board[k]
scores[c.index] = HandEvaluator.evaluate(seven, 2 + board.size)
val score = HandEvaluator.evaluate(seven, 2 + board.size)
scores[c.index] = score
showdownHands.add(
ShowdownHand(
seat = c.index,
hole = c.hole.toList(),
score = score,
),
)
}
val levels = contenders.map { it.committedThisHand }.distinct().sorted()
@@ -610,11 +661,16 @@ class Table(
.filter { scores.getValue(it) == best }
.sortedBy { (it - button - 1 + seats.size) % seats.size }
val share = p.amount / potWinners.size
var remainder = p.amount - share * potWinners.size
for (w in potWinners) {
seats[w].stack += share
if (remainder > 0) { seats[w].stack += 1; remainder-- }
if (w !in winners) winners.add(w)
val remainder = p.amount - share * potWinners.size
val payouts = potWinners.mapIndexed { index, winner ->
PotPayout(
seat = winner,
amount = share + if (index < remainder) 1 else 0,
)
}
potAwards.add(PotAward(p.amount, payouts))
for (payout in payouts) {
seats[payout.seat].stack += payout.amount
}
}
for (s in seats) s.committedThisHand = 0
@@ -624,10 +680,11 @@ class Table(
return HandResult(
board = board.toIntArray(),
net = net,
winners = winners,
wentToShowdown = wentToShowdown,
potSize = potTotal,
events = ArrayList(events),
showdownHands = showdownHands,
potAwards = potAwards,
)
}
@@ -182,6 +182,19 @@ class TableRulesTest {
// Kings beat queens for the side pot.
assertTrue(result.net[1] > 0, "kings should win the side pot")
assertTrue(result.net[2] < 0, "queens should lose")
assertEquals(
listOf(
PotAward(150, listOf(PotPayout(0, 150))),
PotAward(300, listOf(PotPayout(1, 300))),
),
result.potAwards,
"presentation must retain who won the main and side pots separately",
)
assertEquals(
listOf(0, 1, 2),
result.showdownHands.map { it.seat },
"only publicly revealed contenders belong in showdown results",
)
}
@Test
@@ -239,6 +252,16 @@ class TableRulesTest {
assertEquals(3, result.net[2], "seat left of the button gets the odd chip")
assertEquals(2, result.net[0], "the other winner gets the smaller share")
assertEquals(-5, result.net[1], "the folded small blind loses its post")
assertEquals(
listOf(PotAward(25, listOf(PotPayout(2, 13), PotPayout(0, 12)))),
result.potAwards,
"award must preserve the exact odd-chip payout, not only the tied seats",
)
assertEquals(
listOf(0, 2),
result.showdownHands.map { it.seat },
"a folded player's private cards must not enter the public showdown result",
)
}
// ---------- uncalled bets ----------