Seat every player on the felt rail
Bruno and Dex floated on the felt interior while Ada, Cleo and Enzo straddled the rail, so the row looked inconsistent. The cause was that vertical lanes were hand-picked constants (0.30 / 0.15 / 0 / 0.15 / 0.30) with no relationship to the oval they were meant to sit on — any value would put some seats on the edge and others adrift, which is why tuning them kept trading one complaint for another. Seat placement now solves the felt ellipse for each seat's own horizontal position, and reads the same FELT_LEFT/RIGHT/TOP/BOTTOM constants the felt is drawn from. The rail and the seats can no longer disagree, and a test asserts every seat lands on the rail at every supported width rather than merely checking that lanes are ordered. At 384dp the solved fractions are 0.247 / 0.095 / 0.055 / 0.095 / 0.247 — a real curve rather than three seats abreast. Seat gap widened from 6dp to 10dp so the name plates have breathing room now that the shoulder seats sit nearer the crown. Verified on Galaxy S24+ geometry: nothing clipped at either edge. 261 tests, 0 failures. Lint 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ import com.jsjdesigns.poker.game.DecisionOffer
|
||||
import com.jsjdesigns.poker.game.SeatSnapshot
|
||||
import com.jsjdesigns.poker.game.TableSnapshot
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sqrt
|
||||
|
||||
private val Night = Color(0xFF04100C)
|
||||
private val TableBackground = Color(0xFF071D16)
|
||||
@@ -111,15 +112,40 @@ internal fun visibleChipCount(amount: Int): Int = when {
|
||||
internal data class SeatPlacement(
|
||||
/** Distance in dp from the table's left edge to this seat's left edge. */
|
||||
val leftDp: Float,
|
||||
/** Vertical lane as a fraction of table height. */
|
||||
val topFraction: Float,
|
||||
/** Where this seat's avatar centre belongs, as a fraction of table height. */
|
||||
val centerYFraction: Float,
|
||||
)
|
||||
|
||||
/** Space kept clear at the table's left and right edges. */
|
||||
private const val SEAT_EDGE_MARGIN_DP = 6f
|
||||
|
||||
/** Smallest horizontal gap permitted between two neighbouring seats. */
|
||||
private const val SEAT_MIN_GAP_DP = 6f
|
||||
private const val SEAT_MIN_GAP_DP = 10f
|
||||
|
||||
// The felt oval, as fractions of the table box. Seat placement reads the same
|
||||
// constants the felt is drawn from, so seats cannot drift off the rail when the
|
||||
// table shape changes — the two were previously unrelated numbers.
|
||||
private const val FELT_LEFT = 0.012f
|
||||
private const val FELT_RIGHT = 0.988f
|
||||
private const val FELT_TOP = 0.055f
|
||||
private const val FELT_BOTTOM = 0.965f
|
||||
|
||||
/**
|
||||
* Height fraction where the felt's upper rail sits at a given horizontal
|
||||
* position.
|
||||
*
|
||||
* Seats were previously assigned hand-picked vertical lanes, which left the
|
||||
* shoulder seats floating on the felt interior while the crown and side seats
|
||||
* straddled the rail. Solving the ellipse puts every seat on the same edge.
|
||||
*/
|
||||
internal fun railCenterYFraction(centerXFraction: Float): Float {
|
||||
val cx = (FELT_LEFT + FELT_RIGHT) / 2f
|
||||
val rx = (FELT_RIGHT - FELT_LEFT) / 2f
|
||||
val cy = (FELT_TOP + FELT_BOTTOM) / 2f
|
||||
val ry = (FELT_BOTTOM - FELT_TOP) / 2f
|
||||
val u = ((centerXFraction - cx) / rx).coerceIn(-1f, 1f)
|
||||
return cy - ry * sqrt(1f - u * u)
|
||||
}
|
||||
|
||||
/**
|
||||
* Width one opponent seat may occupy, derived from the table's real width.
|
||||
@@ -143,13 +169,14 @@ internal fun opponentSeatPlacements(
|
||||
availableWidthDp: Float,
|
||||
seatWidthDp: Float = opponentSeatWidthDp(availableWidthDp),
|
||||
): List<SeatPlacement> {
|
||||
val lanes = listOf(0.30f, 0.15f, 0f, 0.15f, 0.30f)
|
||||
val span = availableWidthDp - 2f * SEAT_EDGE_MARGIN_DP - seatWidthDp
|
||||
val step = span / 4f
|
||||
return List(5) { index ->
|
||||
val left = SEAT_EDGE_MARGIN_DP + step * index
|
||||
val centerXFraction = (left + seatWidthDp / 2f) / availableWidthDp
|
||||
SeatPlacement(
|
||||
leftDp = SEAT_EDGE_MARGIN_DP + step * index,
|
||||
topFraction = lanes[index],
|
||||
leftDp = left,
|
||||
centerYFraction = railCenterYFraction(centerXFraction),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -284,7 +311,10 @@ private fun PokerTable(
|
||||
width = seatWidth.dp,
|
||||
modifier = Modifier.offset(
|
||||
x = placement.leftDp.dp,
|
||||
y = maxHeight * placement.topFraction,
|
||||
// Placement gives where the avatar centre belongs on the
|
||||
// rail; the seat's top is half an avatar above it.
|
||||
y = maxHeight * placement.centerYFraction -
|
||||
(seatWidth.dp * 0.58f).coerceIn(34.dp, 48.dp) / 2,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -329,12 +359,11 @@ private fun TableFelt(modifier: Modifier = Modifier) {
|
||||
// An oval sized to the container, so the felt grows into whatever height
|
||||
// the table is given rather than staying a width-sized circle.
|
||||
val inset = 9.dp.toPx()
|
||||
val railTop = size.height * 0.055f
|
||||
val railRect = androidx.compose.ui.geometry.Rect(
|
||||
left = size.width * 0.012f,
|
||||
top = railTop,
|
||||
right = size.width * 0.988f,
|
||||
bottom = size.height * 0.965f,
|
||||
left = size.width * FELT_LEFT,
|
||||
top = size.height * FELT_TOP,
|
||||
right = size.width * FELT_RIGHT,
|
||||
bottom = size.height * FELT_BOTTOM,
|
||||
)
|
||||
drawOval(
|
||||
brush = Brush.linearGradient(
|
||||
|
||||
@@ -60,31 +60,42 @@ class TableOrbitTest {
|
||||
|
||||
@Test
|
||||
fun `seats are staggered into an arc rather than one straight row`() {
|
||||
val lanes = opponentSeatPlacements(384f).map { it.topFraction }
|
||||
val lanes = opponentSeatPlacements(384f).map { it.centerYFraction }
|
||||
assertTrue("outer seats should sit lower than the crown", lanes[0] > lanes[2])
|
||||
assertTrue("the arc should be symmetric", lanes[0] == lanes[4] && lanes[1] == lanes[3])
|
||||
assertEquals("the middle seat crowns the arc", 0f, lanes[2], 0.001f)
|
||||
assertEquals("the arc should be symmetric", lanes[0], lanes[4], 0.0005f)
|
||||
assertEquals("the arc should be symmetric", lanes[1], lanes[3], 0.0005f)
|
||||
assertTrue("the middle seat crowns the arc", lanes[2] < lanes[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* The shoulder seats sat at 0.06 against a crown of 0.0, so three seats read
|
||||
* as one crowded row instead of a curve. They belong between the crown and
|
||||
* the side seats.
|
||||
* Every seat must land on the felt's edge. Hand-picked lanes left the
|
||||
* shoulder seats floating on the felt interior while the crown and side
|
||||
* seats straddled the rail, so the row looked inconsistent.
|
||||
*/
|
||||
@Test
|
||||
fun `shoulder seats sit between the crown and the side seats`() {
|
||||
val lanes = opponentSeatPlacements(384f).map { it.topFraction }
|
||||
val crown = lanes[2]
|
||||
val side = lanes[0]
|
||||
val shoulder = lanes[1]
|
||||
assertTrue("shoulder must be below the crown", shoulder > crown)
|
||||
assertTrue("shoulder must be above the side seats", shoulder < side)
|
||||
val midpoint = (crown + side) / 2f
|
||||
assertTrue(
|
||||
"shoulder $shoulder should be near the midpoint $midpoint, not hugging the crown",
|
||||
kotlin.math.abs(shoulder - midpoint) <= 0.05f,
|
||||
fun `every seat sits on the felt rail at its own horizontal position`() {
|
||||
for (width in widths) {
|
||||
val seatWidth = opponentSeatWidthDp(width)
|
||||
opponentSeatPlacements(width, seatWidth).forEach { placement ->
|
||||
val centerX = (placement.leftDp + seatWidth / 2f) / width
|
||||
assertEquals(
|
||||
"at ${width}dp a seat drifted off the rail",
|
||||
railCenterYFraction(centerX),
|
||||
placement.centerYFraction,
|
||||
0.0001f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the rail solution is a real ellipse, not a flat line`() {
|
||||
val crown = railCenterYFraction(0.5f)
|
||||
val shoulder = railCenterYFraction(0.30f)
|
||||
val side = railCenterYFraction(0.06f)
|
||||
assertTrue("the crown is the highest point", crown < shoulder)
|
||||
assertTrue("the rail keeps dropping toward the sides", shoulder < side)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `spacing is even so the row reads as a deliberate arrangement`() {
|
||||
|
||||
Reference in New Issue
Block a user