Caption Sequence jacks as WILD or REMOVE

A jack's power is carried by its artwork on a real deck — drawn in
profile it shows one eye and removes a chip, drawn face-on it shows two
and is wild. These cards render as rank and suit only, so J♦ and J♠ were
indistinguishable in hand despite doing opposite things.

Jacks in hand now carry the word on their face, and the hint under the
hand says what tapping will do rather than the generic "tap a highlighted
square", which was wrong for a one-eyed jack anyway — those target an
opponent's chip, not a highlighted empty square.

The caption is a general PlayingCardView option, so any later game whose
cards carry meaning beyond rank and suit can use it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
This commit is contained in:
Claude
2026-08-06 02:39:51 +00:00
parent 39a312b157
commit 60ddcdf67f
3 changed files with 51 additions and 4 deletions
@@ -41,6 +41,12 @@ fun PlayingCardView(
width: Dp = 64.dp,
selected: Boolean = false,
enabled: Boolean = true,
/**
* Short note printed on the face, for when the rank and suit alone do not
* say what the card does — a Sequence jack being the obvious case, since
* one-eyed and two-eyed jacks are only distinguishable by their artwork.
*/
caption: String? = null,
onClick: (() -> Unit)? = null,
) {
val ink = if (card.suit.isRed) CardRed else CardBlack
@@ -61,6 +67,8 @@ fun PlayingCardView(
),
contentAlignment = Alignment.Center,
) {
// Shrink the pips slightly when a caption has to share the face.
val pipScale = if (caption == null) 0.34f else 0.28f
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
@@ -69,13 +77,22 @@ fun PlayingCardView(
text = card.rank.short,
color = ink,
fontWeight = FontWeight.Bold,
fontSize = (width.value * 0.34f).sp,
fontSize = (width.value * pipScale).sp,
)
Text(
text = card.suit.symbol,
color = ink,
fontSize = (width.value * 0.34f).sp,
fontSize = (width.value * pipScale).sp,
)
if (caption != null) {
Text(
text = caption,
color = ink,
fontWeight = FontWeight.Bold,
fontSize = (width.value * 0.145f).sp,
maxLines = 1,
)
}
}
}
}
@@ -35,6 +35,8 @@ import androidx.compose.ui.unit.sp
import org.prolibertate.games.game.cards.Card
import org.prolibertate.games.game.sequence.BOARD_SIZE
import org.prolibertate.games.game.sequence.ExchangeDeadCard
import org.prolibertate.games.game.sequence.isOneEyedJack
import org.prolibertate.games.game.sequence.isTwoEyedJack
import org.prolibertate.games.game.sequence.NO_TEAM
import org.prolibertate.games.game.sequence.PlaceChip
import org.prolibertate.games.game.sequence.RemoveChip
@@ -54,6 +56,17 @@ import org.prolibertate.games.ui.theme.TeamColours
*/
private val HighlightAmber = Color(0xFFFFC107)
/**
* What a jack does, printed on its face. On a real deck this is carried by the
* artwork — a jack shown in profile has one eye and removes a chip, one shown
* face-on has two and is wild — which a rank-and-suit card cannot convey.
*/
private fun jackCaption(card: Card): String? = when {
isTwoEyedJack(card) -> "WILD"
isOneEyedJack(card) -> "REMOVE"
else -> null
}
/**
* The Sequence board.
*
@@ -294,6 +307,9 @@ private fun HandStrip(
width = cardWidth,
selected = card == selected,
enabled = card in playable,
// A jack's power comes from its artwork, which these cards
// do not have, so spell it out on the face instead.
caption = jackCaption(card),
onClick = {
val dead = exchanges[card]
if (dead != null) onExchange(dead) else onSelect(card)
@@ -301,9 +317,18 @@ private fun HandStrip(
)
}
}
selected?.let {
selected?.let { card ->
Text(
text = "Tap a highlighted square to play ${it.label}.",
text = when {
isTwoEyedJack(card) ->
"${card.label} is wild — tap any empty square."
isOneEyedJack(card) ->
"${card.label} removes a chip — tap an opponent's, " +
"as long as it isn't part of a finished sequence."
else -> "Tap a highlighted square to play ${card.label}."
},
style = MaterialTheme.typography.bodySmall,
)
}