Add a switchable table shape alongside the seat style
A second header chip cycles the felt shape, so table and seat treatments can be compared together on a device rather than argued about from screenshots. - Oval: ellipse fitted to the container. Seats step by equal angle, because even horizontal spacing on a deep ellipse always bunches the middle three. - Arena: a circle wider than the screen. The left and right lobes fall outside and are clipped, trading side space nobody uses for a broad, shallow top arc. The arc drop from crown to outermost seat falls from roughly 143dp to 70dp at 384dp wide, so seats spread across the full width instead of stacking. - Stadium: racetrack. Flat across the middle, curving only at the ends. Shape and seat arc are one decision, not two: seats are placed on whichever curve the felt is drawn from, so they cannot drift off it the way hand-picked lanes did. On the shallow shapes seats space evenly by x, which is exactly where that works — the vertical differences are small enough to read as a gentle curve. Tests cover all three shapes at every supported width for overflow and collision, and assert arena flattens the arc relative to oval. 261 tests, 0 failures. Lint 0 errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,8 +65,11 @@ import com.jsjdesigns.poker.game.SeatSnapshot
|
|||||||
import com.jsjdesigns.poker.game.TableSnapshot
|
import com.jsjdesigns.poker.game.TableSnapshot
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.PI
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.max
|
||||||
import kotlin.math.sin
|
import kotlin.math.sin
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
private val Night = Color(0xFF04100C)
|
private val Night = Color(0xFF04100C)
|
||||||
private val TableBackground = Color(0xFF071D16)
|
private val TableBackground = Color(0xFF071D16)
|
||||||
@@ -142,6 +145,35 @@ private const val ORBIT_CROWN_Y = 0.06f
|
|||||||
/** Vertical radius of the seating arc, as a fraction of table height. */
|
/** Vertical radius of the seating arc, as a fraction of table height. */
|
||||||
private const val ORBIT_RY = 0.34f
|
private const val ORBIT_RY = 0.34f
|
||||||
|
|
||||||
|
/** Arena circle radius, as a multiple of table width. */
|
||||||
|
private const val ARENA_RADIUS_W = 0.78f
|
||||||
|
|
||||||
|
/** Corner radius of the stadium's rounded ends, as a fraction of table width. */
|
||||||
|
private const val STADIUM_CORNER_W = 0.30f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Competing felt shapes, switchable at runtime alongside [SeatStyle].
|
||||||
|
*
|
||||||
|
* The shape and the seat arc are one decision, not two: seats are placed on
|
||||||
|
* whichever curve the felt is drawn from, so they cannot drift off it.
|
||||||
|
*/
|
||||||
|
enum class TableStyle(val label: String) {
|
||||||
|
/** Ellipse fitted to the container. Deepest arc, narrowest crown. */
|
||||||
|
OVAL("Oval"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A circle wider than the screen, so the left and right extremes fall
|
||||||
|
* outside and only the broad top arc is used. Trades the unused side lobes
|
||||||
|
* for a wider, shallower run of seats.
|
||||||
|
*/
|
||||||
|
ARENA("Arena"),
|
||||||
|
|
||||||
|
/** Racetrack: flat across the middle, curving only at the ends. */
|
||||||
|
STADIUM("Stadium");
|
||||||
|
|
||||||
|
fun next(): TableStyle = entries[(ordinal + 1) % entries.size]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Height fraction of the pot, centred inside the ring the seats enclose.
|
* Height fraction of the pot, centred inside the ring the seats enclose.
|
||||||
*/
|
*/
|
||||||
@@ -204,14 +236,35 @@ internal fun opponentSeatWidthDp(
|
|||||||
*/
|
*/
|
||||||
internal fun opponentSeatPlacements(
|
internal fun opponentSeatPlacements(
|
||||||
availableWidthDp: Float,
|
availableWidthDp: Float,
|
||||||
|
availableHeightDp: Float,
|
||||||
seatWidthDp: Float = opponentSeatWidthDp(availableWidthDp),
|
seatWidthDp: Float = opponentSeatWidthDp(availableWidthDp),
|
||||||
): List<SeatPlacement> {
|
tableStyle: TableStyle = TableStyle.OVAL,
|
||||||
|
): List<SeatPlacement> = when (tableStyle) {
|
||||||
|
TableStyle.OVAL -> ovalPlacements(availableWidthDp, seatWidthDp)
|
||||||
|
TableStyle.ARENA -> arcPlacements(availableWidthDp, availableHeightDp, seatWidthDp) { dx, radius ->
|
||||||
|
radius - sqrt(max(0f, radius * radius - dx * dx))
|
||||||
|
}
|
||||||
|
TableStyle.STADIUM -> arcPlacements(availableWidthDp, availableHeightDp, seatWidthDp) { dx, _ ->
|
||||||
|
val corner = STADIUM_CORNER_W * availableWidthDp
|
||||||
|
val flatHalf = availableWidthDp / 2f - corner
|
||||||
|
val over = abs(dx) - flatHalf
|
||||||
|
if (over <= 0f) 0f else corner - sqrt(max(0f, corner * corner - over * over))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equal-angle steps around an ellipse.
|
||||||
|
*
|
||||||
|
* Even horizontal spacing cannot also be even around an oval, so this steps by
|
||||||
|
* angle instead; the side seats fall to board level and the rest space evenly
|
||||||
|
* from them.
|
||||||
|
*/
|
||||||
|
private fun ovalPlacements(availableWidthDp: Float, seatWidthDp: Float): List<SeatPlacement> {
|
||||||
val orbitRx = (availableWidthDp - seatWidthDp) / 2f - SEAT_EDGE_MARGIN_DP
|
val orbitRx = (availableWidthDp - seatWidthDp) / 2f - SEAT_EDGE_MARGIN_DP
|
||||||
val centerX = availableWidthDp / 2f
|
val centerX = availableWidthDp / 2f
|
||||||
val step = (2f * ORBIT_HALF_ANGLE_DEG) / 4f
|
val step = (2f * ORBIT_HALF_ANGLE_DEG) / 4f
|
||||||
return List(5) { index ->
|
return List(5) { index ->
|
||||||
val degrees = -ORBIT_HALF_ANGLE_DEG + step * index
|
val radians = (-ORBIT_HALF_ANGLE_DEG + step * index) * PI.toFloat() / 180f
|
||||||
val radians = degrees * PI.toFloat() / 180f
|
|
||||||
SeatPlacement(
|
SeatPlacement(
|
||||||
leftDp = centerX + orbitRx * sin(radians) - seatWidthDp / 2f,
|
leftDp = centerX + orbitRx * sin(radians) - seatWidthDp / 2f,
|
||||||
centerYFraction = ORBIT_CROWN_Y + ORBIT_RY * (1f - cos(radians)),
|
centerYFraction = ORBIT_CROWN_Y + ORBIT_RY * (1f - cos(radians)),
|
||||||
@@ -219,6 +272,34 @@ internal fun opponentSeatPlacements(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evenly spaced across the width, with height following the felt's own curve.
|
||||||
|
*
|
||||||
|
* A shallow curve is exactly where even horizontal spacing works: the vertical
|
||||||
|
* differences are small, so seats read as a gentle arc rather than bunching the
|
||||||
|
* way they do on a deep ellipse.
|
||||||
|
*/
|
||||||
|
private fun arcPlacements(
|
||||||
|
availableWidthDp: Float,
|
||||||
|
availableHeightDp: Float,
|
||||||
|
seatWidthDp: Float,
|
||||||
|
dropAt: (dx: Float, radius: Float) -> Float,
|
||||||
|
): List<SeatPlacement> {
|
||||||
|
val radius = ARENA_RADIUS_W * availableWidthDp
|
||||||
|
val centerX = availableWidthDp / 2f
|
||||||
|
val span = availableWidthDp - 2f * SEAT_EDGE_MARGIN_DP - seatWidthDp
|
||||||
|
val step = span / 4f
|
||||||
|
val crownDp = ORBIT_CROWN_Y * availableHeightDp
|
||||||
|
return List(5) { index ->
|
||||||
|
val left = SEAT_EDGE_MARGIN_DP + step * index
|
||||||
|
val dx = left + seatWidthDp / 2f - centerX
|
||||||
|
SeatPlacement(
|
||||||
|
leftDp = left,
|
||||||
|
centerYFraction = (crownDp + dropAt(dx, radius)) / availableHeightDp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TableScreen(vm: PokerViewModel) {
|
fun TableScreen(vm: PokerViewModel) {
|
||||||
val state by vm.state.collectAsStateWithLifecycle()
|
val state by vm.state.collectAsStateWithLifecycle()
|
||||||
@@ -235,6 +316,7 @@ fun TableScreen(vm: PokerViewModel) {
|
|||||||
// visibly reshaped the oval into a circle every time Raise was tapped.
|
// visibly reshaped the oval into a circle every time Raise was tapped.
|
||||||
var sizingRaise by remember(offer?.token) { mutableStateOf(false) }
|
var sizingRaise by remember(offer?.token) { mutableStateOf(false) }
|
||||||
var seatStyle by remember { mutableStateOf(SeatStyle.PLATE) }
|
var seatStyle by remember { mutableStateOf(SeatStyle.PLATE) }
|
||||||
|
var tableStyle by remember { mutableStateOf(TableStyle.OVAL) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -247,7 +329,9 @@ fun TableScreen(vm: PokerViewModel) {
|
|||||||
snapshot = snap,
|
snapshot = snap,
|
||||||
config = state.gameConfig,
|
config = state.gameConfig,
|
||||||
seatStyle = seatStyle,
|
seatStyle = seatStyle,
|
||||||
|
tableStyle = tableStyle,
|
||||||
onCycleSeatStyle = { seatStyle = seatStyle.next() },
|
onCycleSeatStyle = { seatStyle = seatStyle.next() },
|
||||||
|
onCycleTableStyle = { tableStyle = tableStyle.next() },
|
||||||
)
|
)
|
||||||
PokerTable(
|
PokerTable(
|
||||||
snapshot = snap,
|
snapshot = snap,
|
||||||
@@ -255,6 +339,7 @@ fun TableScreen(vm: PokerViewModel) {
|
|||||||
tableTalk = tableTalk,
|
tableTalk = tableTalk,
|
||||||
handSummary = handSummary,
|
handSummary = handSummary,
|
||||||
seatStyle = seatStyle,
|
seatStyle = seatStyle,
|
||||||
|
tableStyle = tableStyle,
|
||||||
// Weighted against a control area whose height no longer changes,
|
// Weighted against a control area whose height no longer changes,
|
||||||
// so the felt keeps one shape for the whole hand.
|
// so the felt keeps one shape for the whole hand.
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -295,7 +380,9 @@ private fun TableHeader(
|
|||||||
snapshot: TableSnapshot?,
|
snapshot: TableSnapshot?,
|
||||||
config: CashGameConfig?,
|
config: CashGameConfig?,
|
||||||
seatStyle: SeatStyle,
|
seatStyle: SeatStyle,
|
||||||
|
tableStyle: TableStyle,
|
||||||
onCycleSeatStyle: () -> Unit,
|
onCycleSeatStyle: () -> Unit,
|
||||||
|
onCycleTableStyle: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -335,12 +422,32 @@ private fun TableHeader(
|
|||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"SEATS · ${seatStyle.label.uppercase()}",
|
seatStyle.label.uppercase(),
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
modifier = Modifier.padding(horizontal = 9.dp, vertical = 5.dp),
|
||||||
color = TableGold,
|
color = TableGold,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 10.sp,
|
fontSize = 10.sp,
|
||||||
letterSpacing = 1.sp,
|
letterSpacing = 0.8.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.clickable(onClick = onCycleTableStyle),
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
color = Color.White.copy(alpha = 0.10f),
|
||||||
|
border = androidx.compose.foundation.BorderStroke(
|
||||||
|
1.dp,
|
||||||
|
FeltLight.copy(alpha = 0.75f),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
tableStyle.label.uppercase(),
|
||||||
|
modifier = Modifier.padding(horizontal = 9.dp, vertical = 5.dp),
|
||||||
|
color = FeltLight,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
letterSpacing = 0.8.sp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
@@ -373,6 +480,7 @@ private fun PokerTable(
|
|||||||
tableTalk: com.jsjdesigns.poker.bot.TableTalkLine?,
|
tableTalk: com.jsjdesigns.poker.bot.TableTalkLine?,
|
||||||
handSummary: HandSummary?,
|
handSummary: HandSummary?,
|
||||||
seatStyle: SeatStyle,
|
seatStyle: SeatStyle,
|
||||||
|
tableStyle: TableStyle,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val seats = snapshot?.seats.orEmpty()
|
val seats = snapshot?.seats.orEmpty()
|
||||||
@@ -385,12 +493,17 @@ private fun PokerTable(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(TableBackground),
|
.background(TableBackground),
|
||||||
) {
|
) {
|
||||||
TableFelt(Modifier.fillMaxSize())
|
TableFelt(tableStyle, Modifier.fillMaxSize())
|
||||||
|
|
||||||
// Seat width and position both come from the measured table, so five
|
// Seat width and position both come from the measured table, so five
|
||||||
// seats fit with real gaps on a 384dp phone as well as a 427dp emulator.
|
// seats fit with real gaps on a 384dp phone as well as a 427dp emulator.
|
||||||
val seatWidth = opponentSeatWidthDp(maxWidth.value, seatStyle)
|
val seatWidth = opponentSeatWidthDp(maxWidth.value, seatStyle)
|
||||||
val placements = opponentSeatPlacements(maxWidth.value, seatWidth)
|
val placements = opponentSeatPlacements(
|
||||||
|
availableWidthDp = maxWidth.value,
|
||||||
|
availableHeightDp = maxHeight.value,
|
||||||
|
seatWidthDp = seatWidth,
|
||||||
|
tableStyle = tableStyle,
|
||||||
|
)
|
||||||
seats.filter { it.index != HERO_SEAT }
|
seats.filter { it.index != HERO_SEAT }
|
||||||
.forEachIndexed { index, seat ->
|
.forEachIndexed { index, seat ->
|
||||||
val placement = placements.getOrElse(index) { placements.last() }
|
val placement = placements.getOrElse(index) { placements.last() }
|
||||||
@@ -452,50 +565,91 @@ private fun PokerTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TableFelt(modifier: Modifier = Modifier) {
|
private fun TableFelt(style: TableStyle, modifier: Modifier = Modifier) {
|
||||||
Canvas(modifier) {
|
Canvas(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 inset = 9.dp.toPx()
|
||||||
val railRect = androidx.compose.ui.geometry.Rect(
|
val railBrush = Brush.linearGradient(
|
||||||
left = size.width * FELT_LEFT,
|
colors = listOf(RailLight, RailDark),
|
||||||
top = size.height * FELT_TOP,
|
start = Offset(0f, 0f),
|
||||||
right = size.width * FELT_RIGHT,
|
end = Offset(size.width, size.height),
|
||||||
bottom = size.height * FELT_BOTTOM,
|
|
||||||
)
|
)
|
||||||
drawOval(
|
fun feltBrush(width: Float, height: Float, topLeft: Offset) = Brush.radialGradient(
|
||||||
brush = Brush.linearGradient(
|
colors = listOf(FeltLight, FeltMid, FeltDark),
|
||||||
colors = listOf(RailLight, RailDark),
|
center = Offset(topLeft.x + width * 0.5f, topLeft.y + height * 0.34f),
|
||||||
start = Offset(railRect.left, railRect.top),
|
radius = minOf(width, height) * 1.15f,
|
||||||
end = Offset(railRect.right, railRect.bottom),
|
|
||||||
),
|
|
||||||
topLeft = Offset(railRect.left, railRect.top),
|
|
||||||
size = androidx.compose.ui.geometry.Size(railRect.width, railRect.height),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val feltTopLeft = Offset(railRect.left + inset, railRect.top + inset)
|
when (style) {
|
||||||
val feltSize = androidx.compose.ui.geometry.Size(
|
TableStyle.OVAL -> {
|
||||||
railRect.width - inset * 2,
|
val rail = androidx.compose.ui.geometry.Rect(
|
||||||
railRect.height - inset * 2,
|
left = size.width * FELT_LEFT,
|
||||||
)
|
top = size.height * FELT_TOP,
|
||||||
drawOval(
|
right = size.width * FELT_RIGHT,
|
||||||
brush = Brush.radialGradient(
|
bottom = size.height * FELT_BOTTOM,
|
||||||
colors = listOf(FeltLight, FeltMid, FeltDark),
|
)
|
||||||
center = Offset(
|
drawOval(railBrush, Offset(rail.left, rail.top), rail.size)
|
||||||
feltTopLeft.x + feltSize.width * 0.5f,
|
val tl = Offset(rail.left + inset, rail.top + inset)
|
||||||
feltTopLeft.y + feltSize.height * 0.34f,
|
val sz = androidx.compose.ui.geometry.Size(
|
||||||
),
|
rail.width - inset * 2,
|
||||||
radius = feltSize.minDimension * 1.15f,
|
rail.height - inset * 2,
|
||||||
),
|
)
|
||||||
topLeft = feltTopLeft,
|
drawOval(feltBrush(sz.width, sz.height, tl), tl, sz)
|
||||||
size = feltSize,
|
drawOval(TableGold.copy(alpha = 0.30f), tl, sz, style = Stroke(1.dp.toPx()))
|
||||||
)
|
}
|
||||||
drawOval(
|
|
||||||
color = TableGold.copy(alpha = 0.30f),
|
TableStyle.ARENA -> {
|
||||||
topLeft = feltTopLeft,
|
// A circle wider than the canvas. The side lobes fall outside and
|
||||||
size = feltSize,
|
// are clipped away, leaving a broad, shallow top arc.
|
||||||
style = Stroke(width = 1.dp.toPx()),
|
val radius = size.width * ARENA_RADIUS_W
|
||||||
)
|
val center = Offset(size.width / 2f, size.height * ORBIT_CROWN_Y + radius)
|
||||||
|
drawCircle(railBrush, radius, center)
|
||||||
|
drawCircle(
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(FeltLight, FeltMid, FeltDark),
|
||||||
|
center = Offset(center.x, center.y - radius * 0.55f),
|
||||||
|
radius = radius * 1.25f,
|
||||||
|
),
|
||||||
|
radius - inset,
|
||||||
|
center,
|
||||||
|
)
|
||||||
|
drawCircle(
|
||||||
|
TableGold.copy(alpha = 0.30f),
|
||||||
|
radius - inset,
|
||||||
|
center,
|
||||||
|
style = Stroke(1.dp.toPx()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TableStyle.STADIUM -> {
|
||||||
|
val corner = size.width * STADIUM_CORNER_W
|
||||||
|
val rail = androidx.compose.ui.geometry.Rect(
|
||||||
|
left = size.width * 0.012f,
|
||||||
|
top = size.height * 0.03f,
|
||||||
|
right = size.width * 0.988f,
|
||||||
|
bottom = size.height * 1.02f,
|
||||||
|
)
|
||||||
|
drawRoundRect(
|
||||||
|
railBrush,
|
||||||
|
Offset(rail.left, rail.top),
|
||||||
|
rail.size,
|
||||||
|
androidx.compose.ui.geometry.CornerRadius(corner, corner),
|
||||||
|
)
|
||||||
|
val tl = Offset(rail.left + inset, rail.top + inset)
|
||||||
|
val sz = androidx.compose.ui.geometry.Size(
|
||||||
|
rail.width - inset * 2,
|
||||||
|
rail.height - inset * 2,
|
||||||
|
)
|
||||||
|
val cr = androidx.compose.ui.geometry.CornerRadius(corner - inset, corner - inset)
|
||||||
|
drawRoundRect(feltBrush(sz.width, sz.height, tl), tl, sz, cr)
|
||||||
|
drawRoundRect(
|
||||||
|
TableGold.copy(alpha = 0.30f),
|
||||||
|
tl,
|
||||||
|
sz,
|
||||||
|
cr,
|
||||||
|
style = Stroke(1.dp.toPx()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class TableOrbitTest {
|
|||||||
val seatHeightFraction = 0.14f
|
val seatHeightFraction = 0.14f
|
||||||
for (width in widths) {
|
for (width in widths) {
|
||||||
val seatWidth = opponentSeatWidthDp(width)
|
val seatWidth = opponentSeatWidthDp(width)
|
||||||
val placements = opponentSeatPlacements(width, seatWidth)
|
val placements = opponentSeatPlacements(width, 610f, seatWidth)
|
||||||
assertEquals(5, placements.size)
|
assertEquals(5, placements.size)
|
||||||
placements.zipWithNext { left, right ->
|
placements.zipWithNext { left, right ->
|
||||||
val horizontalGap = right.leftDp - (left.leftDp + seatWidth)
|
val horizontalGap = right.leftDp - (left.leftDp + seatWidth)
|
||||||
@@ -44,7 +44,7 @@ class TableOrbitTest {
|
|||||||
fun `seats stay inside the table at any supported width`() {
|
fun `seats stay inside the table at any supported width`() {
|
||||||
for (width in widths) {
|
for (width in widths) {
|
||||||
val seatWidth = opponentSeatWidthDp(width)
|
val seatWidth = opponentSeatWidthDp(width)
|
||||||
val placements = opponentSeatPlacements(width, seatWidth)
|
val placements = opponentSeatPlacements(width, 610f, seatWidth)
|
||||||
assertTrue("at ${width}dp the first seat is off-screen", placements.first().leftDp >= 0f)
|
assertTrue("at ${width}dp the first seat is off-screen", placements.first().leftDp >= 0f)
|
||||||
assertTrue(
|
assertTrue(
|
||||||
"at ${width}dp the last seat overflows by " +
|
"at ${width}dp the last seat overflows by " +
|
||||||
@@ -65,7 +65,7 @@ class TableOrbitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `seats are staggered into an arc rather than one straight row`() {
|
fun `seats are staggered into an arc rather than one straight row`() {
|
||||||
val lanes = opponentSeatPlacements(384f).map { it.centerYFraction }
|
val lanes = opponentSeatPlacements(384f, 610f).map { it.centerYFraction }
|
||||||
assertTrue("outer seats should sit lower than the crown", lanes[0] > lanes[2])
|
assertTrue("outer seats should sit lower than the crown", lanes[0] > lanes[2])
|
||||||
assertEquals("the arc should be symmetric", lanes[0], lanes[4], 0.0005f)
|
assertEquals("the arc should be symmetric", lanes[0], lanes[4], 0.0005f)
|
||||||
assertEquals("the arc should be symmetric", lanes[1], lanes[3], 0.0005f)
|
assertEquals("the arc should be symmetric", lanes[1], lanes[3], 0.0005f)
|
||||||
@@ -78,7 +78,7 @@ class TableOrbitTest {
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun `seats step evenly around the arc`() {
|
fun `seats step evenly around the arc`() {
|
||||||
val lanes = opponentSeatPlacements(384f).map { it.centerYFraction }
|
val lanes = opponentSeatPlacements(384f, 610f).map { it.centerYFraction }
|
||||||
val crownToShoulder = lanes[1] - lanes[2]
|
val crownToShoulder = lanes[1] - lanes[2]
|
||||||
val shoulderToSide = lanes[0] - lanes[1]
|
val shoulderToSide = lanes[0] - lanes[1]
|
||||||
assertTrue("the side seats must drop well below the crown", lanes[0] - lanes[2] > 0.20f)
|
assertTrue("the side seats must drop well below the crown", lanes[0] - lanes[2] > 0.20f)
|
||||||
@@ -88,7 +88,7 @@ class TableOrbitTest {
|
|||||||
shoulderToSide > crownToShoulder,
|
shoulderToSide > crownToShoulder,
|
||||||
)
|
)
|
||||||
|
|
||||||
val angles = opponentSeatPlacements(384f)
|
val angles = opponentSeatPlacements(384f, 610f)
|
||||||
val xs = angles.map { it.leftDp }
|
val xs = angles.map { it.leftDp }
|
||||||
assertTrue("seats must run left to right", xs.zipWithNext().all { (a, b) -> a < b })
|
assertTrue("seats must run left to right", xs.zipWithNext().all { (a, b) -> a < b })
|
||||||
}
|
}
|
||||||
@@ -102,4 +102,58 @@ class TableOrbitTest {
|
|||||||
assertEquals(7, counts.last())
|
assertEquals(7, counts.last())
|
||||||
assertTrue(counts.zipWithNext().all { (left, right) -> left <= right })
|
assertTrue(counts.zipWithNext().all { (left, right) -> left <= right })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every felt shape must keep five seats on screen and clear of each other.
|
||||||
|
* The shapes differ only in how deep the arc runs.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `every table shape seats five players without collision or overflow`() {
|
||||||
|
val seatHeightFraction = 0.14f
|
||||||
|
for (style in TableStyle.entries) {
|
||||||
|
for (width in widths) {
|
||||||
|
val seatWidth = opponentSeatWidthDp(width)
|
||||||
|
val placements = opponentSeatPlacements(width, 610f, seatWidth, style)
|
||||||
|
assertEquals(5, placements.size)
|
||||||
|
assertTrue(
|
||||||
|
"${style.label} at ${width}dp runs off the left",
|
||||||
|
placements.first().leftDp >= -0.01f,
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"${style.label} at ${width}dp runs off the right",
|
||||||
|
placements.last().leftDp + seatWidth <= width + 0.01f,
|
||||||
|
)
|
||||||
|
placements.zipWithNext { a, b ->
|
||||||
|
val horizontal = b.leftDp - (a.leftDp + seatWidth)
|
||||||
|
val vertical = kotlin.math.abs(b.centerYFraction - a.centerYFraction)
|
||||||
|
assertTrue(
|
||||||
|
"${style.label} at ${width}dp: seats collide " +
|
||||||
|
"(h $horizontal, v $vertical)",
|
||||||
|
horizontal >= -0.01f || vertical >= seatHeightFraction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The point of the arena shape: sacrificing the unused side lobes buys a
|
||||||
|
* wider, shallower run of seats than the oval can offer.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `arena runs a shallower arc than the oval`() {
|
||||||
|
fun drop(style: TableStyle): Float {
|
||||||
|
val lanes = opponentSeatPlacements(384f, 610f, opponentSeatWidthDp(384f), style)
|
||||||
|
.map { it.centerYFraction }
|
||||||
|
return lanes.max() - lanes.min()
|
||||||
|
}
|
||||||
|
assertTrue(
|
||||||
|
"arena should flatten the arc relative to the oval",
|
||||||
|
drop(TableStyle.ARENA) < drop(TableStyle.OVAL),
|
||||||
|
)
|
||||||
|
assertTrue(
|
||||||
|
"stadium should be flatter still",
|
||||||
|
drop(TableStyle.STADIUM) < drop(TableStyle.ARENA),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user