Golf: 8-card grids, chosen openers, final putt, end-of-hole score card
- An 8-card variant, laid out as two rows of four. The existing column scoring covers it without change. - Opening cards are now chosen rather than dealt face up. Nothing starts revealed; each player in turn turns over their allowance, and the hole begins once everyone has picked. This needed a new SETUP phase in the engine, since it is a real decision and has to be validated like any other move rather than being decided at deal time. - "Line up the final putt", off by default: throw a drawn card away without turning your last face-down card over, so you are not forced to close the hole on a card you did not choose. Only offered on the last face-down card. Because a whole table could otherwise decline forever, two full laps of nothing but thrown cards calls the hole and scores it where it stands. - The end of a hole now shows a score card — every player's strokes for the hole and their running total, lowest first — and the next hole is not dealt until it is dismissed. MatchController gained a general awaitsConfirmation hook for this, so any game wanting a scoreboard between rounds can stop the table the same way. Only the host waits; clients render what they are sent. Nine more Golf tests, covering the 8-card layout, the opening choice, when the putt option is and is not offered, and that a table refusing to putt out still finishes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
This commit is contained in:
@@ -16,21 +16,43 @@ class GolfAi(private val difficulty: Difficulty = Difficulty.NORMAL) :
|
||||
enum class Difficulty(
|
||||
/** Take a discard worth this or less rather than gambling on the stock. */
|
||||
val discardAppetite: Int,
|
||||
/** Only close the hole out when the visible score is at or under this. */
|
||||
val closeOutBelow: Int,
|
||||
) {
|
||||
EASY(discardAppetite = 2),
|
||||
NORMAL(discardAppetite = 5),
|
||||
CAUTIOUS(discardAppetite = 7),
|
||||
EASY(discardAppetite = 2, closeOutBelow = 30),
|
||||
NORMAL(discardAppetite = 5, closeOutBelow = 16),
|
||||
CAUTIOUS(discardAppetite = 7, closeOutBelow = 12),
|
||||
}
|
||||
|
||||
override fun chooseMove(state: GolfState, seat: Int, legal: List<GolfMove>): GolfMove {
|
||||
require(legal.isNotEmpty()) { "No legal move for seat $seat" }
|
||||
return when (state.phase) {
|
||||
GolfPhase.SETUP -> chooseReveal(state, seat, legal)
|
||||
GolfPhase.DRAW -> chooseDraw(state, seat, legal)
|
||||
GolfPhase.PLACE -> choosePlace(state, seat, legal)
|
||||
else -> legal.first()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opening reveals are spread across columns. Two cards in the same column
|
||||
* tell you whether that column can be cancelled but nothing about the rest
|
||||
* of the board; one in each column leaves more of the grid legible.
|
||||
*/
|
||||
private fun chooseReveal(state: GolfState, seat: Int, legal: List<GolfMove>): GolfMove {
|
||||
val reveals = legal.filterIsInstance<RevealCard>()
|
||||
if (reveals.isEmpty()) return legal.first()
|
||||
val options = state.options
|
||||
|
||||
val untouchedColumn = reveals.firstOrNull { move ->
|
||||
val column = move.index % options.cols
|
||||
(0 until options.rows).none { row ->
|
||||
state.revealed[seat][row * options.cols + column]
|
||||
}
|
||||
}
|
||||
return untouchedColumn ?: reveals.first()
|
||||
}
|
||||
|
||||
private fun chooseDraw(state: GolfState, seat: Int, legal: List<GolfMove>): GolfMove {
|
||||
val top = state.discard.lastOrNull()
|
||||
if (top != null && legal.contains(DrawFromDiscard)) {
|
||||
@@ -61,6 +83,16 @@ class GolfAi(private val difficulty: Difficulty = Difficulty.NORMAL) :
|
||||
if (golfValue(drawn) < currentValue) return knownWorst
|
||||
}
|
||||
|
||||
// On the last face-down card, turning it over ends the hole for
|
||||
// everyone. Only do that from a position worth defending; otherwise
|
||||
// line up the putt and wait for a better card.
|
||||
if (legal.contains(DiscardOnly)) {
|
||||
val showing = state.grids[seat].indices
|
||||
.filter { state.revealed[seat][it] }
|
||||
.sumOf { golfValue(state.grids[seat][it]) }
|
||||
if (showing > difficulty.closeOutBelow) return DiscardOnly
|
||||
}
|
||||
|
||||
// Nothing worth swapping: throw it and turn something over instead,
|
||||
// which at least buys information.
|
||||
if (flips.isNotEmpty()) return flips.first()
|
||||
|
||||
@@ -9,16 +9,22 @@ import org.prolibertate.games.game.cards.Suit
|
||||
@Serializable
|
||||
data class GolfOptions(
|
||||
val playerCount: Int = 2,
|
||||
/** Cards in front of each player: 4 (2x2), 6 (2x3) or 9 (3x3). */
|
||||
/** Cards in front of each player: 4 (2x2), 6 (2x3), 8 (2x4) or 9 (3x3). */
|
||||
val gridSize: Int = 6,
|
||||
val holes: Int = 9,
|
||||
/** How many of your own cards you get to see before play starts. */
|
||||
/** How many of your own cards you turn over before play starts. */
|
||||
val startingReveals: Int = 2,
|
||||
/**
|
||||
* Lining up the final putt: lets you throw a drawn card away without
|
||||
* turning your last face-down card over, so you are not forced to close
|
||||
* the hole on a card you have not chosen.
|
||||
*/
|
||||
val lineUpFinalPutt: Boolean = false,
|
||||
) {
|
||||
init {
|
||||
require(playerCount in 2..6) { "Golf seats two to six" }
|
||||
require(gridSize == 4 || gridSize == 6 || gridSize == 9) {
|
||||
"Golf grids are 4, 6 or 9 cards"
|
||||
require(gridSize in setOf(4, 6, 8, 9)) {
|
||||
"Golf grids are 4, 6, 8 or 9 cards"
|
||||
}
|
||||
require(holes >= 1) { "holes must be positive" }
|
||||
require(startingReveals in 0..gridSize) { "cannot reveal more than the grid holds" }
|
||||
@@ -82,7 +88,14 @@ fun scoreGrid(cards: List<Card>, options: GolfOptions): Int {
|
||||
val HIDDEN_CARD: Card = Card(Rank.TWO, Suit.SPADES)
|
||||
|
||||
@Serializable
|
||||
enum class GolfPhase { DRAW, PLACE, HOLE_OVER, GAME_OVER }
|
||||
enum class GolfPhase {
|
||||
/** Each player choosing which of their own cards to turn over first. */
|
||||
SETUP,
|
||||
DRAW,
|
||||
PLACE,
|
||||
HOLE_OVER,
|
||||
GAME_OVER,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GolfState(
|
||||
@@ -105,9 +118,17 @@ data class GolfState(
|
||||
val holeScores: List<Int>,
|
||||
val scores: List<Int>,
|
||||
val phase: GolfPhase,
|
||||
/**
|
||||
* Consecutive turns where nobody did anything but throw a card away.
|
||||
* Only reachable with [GolfOptions.lineUpFinalPutt] on, and only there to
|
||||
* stop a table of players all declining to close from playing forever.
|
||||
*/
|
||||
val idleDiscards: Int = 0,
|
||||
val log: List<String>,
|
||||
) {
|
||||
fun allRevealed(seat: Int): Boolean = revealed[seat].all { it }
|
||||
|
||||
fun faceDownCount(seat: Int): Int = revealed[seat].count { !it }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -130,3 +151,16 @@ data class ReplaceCard(val index: Int) : GolfMove
|
||||
@Serializable
|
||||
@SerialName("discard_flip")
|
||||
data class DiscardAndFlip(val index: Int) : GolfMove
|
||||
|
||||
/**
|
||||
* Throw the drawn card away and turn nothing over — lining up the final putt.
|
||||
* Only offered when turning a card over would be turning over your last one.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("discard_only")
|
||||
data object DiscardOnly : GolfMove
|
||||
|
||||
/** Choosing one of your own cards to see before play starts. */
|
||||
@Serializable
|
||||
@SerialName("reveal")
|
||||
data class RevealCard(val index: Int) : GolfMove
|
||||
|
||||
@@ -55,11 +55,9 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
deck = deck.drop(options.gridSize)
|
||||
}
|
||||
|
||||
// The opening reveals are the leftmost cards of the top row, so every
|
||||
// player starts knowing the same shape of their own board.
|
||||
val revealed = List(options.playerCount) {
|
||||
List(options.gridSize) { index -> index < options.startingReveals }
|
||||
}
|
||||
// Nothing starts face up: each player chooses which of their own cards
|
||||
// to turn over, which is a real decision rather than a dealt-out one.
|
||||
val revealed = List(options.playerCount) { List(options.gridSize) { false } }
|
||||
|
||||
val firstDiscard = deck.first()
|
||||
return GolfState(
|
||||
@@ -77,7 +75,8 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
finalTurnsLeft = 0,
|
||||
holeScores = List(options.playerCount) { 0 },
|
||||
scores = scores,
|
||||
phase = GolfPhase.DRAW,
|
||||
phase = if (options.startingReveals > 0) GolfPhase.SETUP else GolfPhase.DRAW,
|
||||
idleDiscards = 0,
|
||||
log = log,
|
||||
)
|
||||
}
|
||||
@@ -95,7 +94,7 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
}
|
||||
|
||||
override fun currentSeat(state: GolfState): Int? = when (state.phase) {
|
||||
GolfPhase.DRAW, GolfPhase.PLACE -> state.turn
|
||||
GolfPhase.SETUP, GolfPhase.DRAW, GolfPhase.PLACE -> state.turn
|
||||
GolfPhase.HOLE_OVER, GolfPhase.GAME_OVER -> null
|
||||
}
|
||||
|
||||
@@ -106,6 +105,10 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
override fun legalMoves(state: GolfState, seat: Int): List<GolfMove> {
|
||||
if (currentSeat(state) != seat) return emptyList()
|
||||
return when (state.phase) {
|
||||
GolfPhase.SETUP -> (0 until state.options.gridSize)
|
||||
.filter { !state.revealed[seat][it] }
|
||||
.map { RevealCard(it) }
|
||||
|
||||
GolfPhase.DRAW -> buildList {
|
||||
if (state.stock.isNotEmpty()) add(DrawFromStock)
|
||||
if (state.discard.isNotEmpty()) add(DrawFromDiscard)
|
||||
@@ -119,6 +122,11 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
for (index in 0 until state.options.gridSize) {
|
||||
if (!state.revealed[seat][index]) add(DiscardAndFlip(index))
|
||||
}
|
||||
// Lining up the final putt: decline to turn over the last
|
||||
// card, rather than being forced to close the hole with it.
|
||||
if (state.options.lineUpFinalPutt && state.faceDownCount(seat) == 1) {
|
||||
add(DiscardOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,9 +158,36 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
|
||||
is ReplaceCard -> applyReplace(state, seat, move.index)
|
||||
is DiscardAndFlip -> applyDiscardAndFlip(state, seat, move.index)
|
||||
is DiscardOnly -> applyDiscardOnly(state, seat)
|
||||
is RevealCard -> applyReveal(state, seat, move.index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turning over one of your opening cards. When a seat has seen its
|
||||
* allowance the choice passes on, and once everyone has chosen the hole
|
||||
* begins.
|
||||
*/
|
||||
private fun applyReveal(state: GolfState, seat: Int, index: Int): GolfState {
|
||||
val revealed = state.revealed.map { it.toMutableList() }
|
||||
revealed[seat][index] = true
|
||||
|
||||
val chosen = revealed[seat].count { it }
|
||||
if (chosen < state.options.startingReveals) {
|
||||
return state.copy(revealed = revealed)
|
||||
}
|
||||
|
||||
val next = (seat + 1) % state.options.playerCount
|
||||
val everyoneReady = revealed.all { row ->
|
||||
row.count { it } >= state.options.startingReveals
|
||||
}
|
||||
return state.copy(
|
||||
revealed = revealed,
|
||||
turn = if (everyoneReady) state.hole % state.options.playerCount else next,
|
||||
phase = if (everyoneReady) GolfPhase.DRAW else GolfPhase.SETUP,
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyReplace(state: GolfState, seat: Int, index: Int): GolfState {
|
||||
val drawn = requireNotNull(state.drawn) { "Nothing drawn" }
|
||||
val grids = state.grids.map { it.toMutableList() }
|
||||
@@ -191,12 +226,32 @@ object GolfRules : GameRules<GolfState, GolfMove> {
|
||||
)
|
||||
}
|
||||
|
||||
private fun applyDiscardOnly(state: GolfState, seat: Int): GolfState {
|
||||
val drawn = requireNotNull(state.drawn) { "Nothing drawn" }
|
||||
return endTurn(
|
||||
state.copy(
|
||||
discard = state.discard + drawn,
|
||||
drawn = null,
|
||||
idleDiscards = state.idleDiscards + 1,
|
||||
log = state.log + "Seat $seat throws ${drawn.label} and lines up the putt.",
|
||||
),
|
||||
seat,
|
||||
idle = true,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes out a turn: notices a finished grid, counts down the last lap and
|
||||
* either moves on or scores the hole.
|
||||
*/
|
||||
private fun endTurn(state: GolfState, seat: Int): GolfState {
|
||||
var next = state
|
||||
private fun endTurn(state: GolfState, seat: Int, idle: Boolean = false): GolfState {
|
||||
var next = if (idle) state else state.copy(idleDiscards = 0)
|
||||
|
||||
// Everyone declining to close, round after round, would never end. Two
|
||||
// full laps of nothing but thrown cards is taken as the hole being over.
|
||||
if (next.idleDiscards >= next.options.playerCount * 2) {
|
||||
return scoreHole(next.copy(log = next.log + "Nobody will putt out — hole called."))
|
||||
}
|
||||
|
||||
// The first player to turn everything over gives everyone else one
|
||||
// more turn, and no more than one.
|
||||
|
||||
@@ -47,6 +47,12 @@ class MatchController<S : Any, M : Any>(
|
||||
* next card lands on top of it.
|
||||
*/
|
||||
private val holdBeforeNextMove: (S) -> Long = { 0L },
|
||||
/**
|
||||
* States that should not advance on their own — an end-of-round scoreboard
|
||||
* the players need to read before the next deal. The controller stops and
|
||||
* waits for [confirmAdvance].
|
||||
*/
|
||||
private val awaitsConfirmation: (S) -> Boolean = { false },
|
||||
) {
|
||||
|
||||
enum class Role { HOST, CLIENT }
|
||||
@@ -65,6 +71,11 @@ class MatchController<S : Any, M : Any>(
|
||||
private val _notice = MutableStateFlow<String?>(null)
|
||||
val notice: StateFlow<String?> = _notice.asStateFlow()
|
||||
|
||||
private val _awaitingConfirmation = MutableStateFlow(false)
|
||||
|
||||
/** True while a scoreboard is up and the table is waiting to be released. */
|
||||
val awaitingConfirmation: StateFlow<Boolean> = _awaitingConfirmation.asStateFlow()
|
||||
|
||||
/** Host only: the true state. Clients never see this. */
|
||||
private var authoritative: S? = null
|
||||
private val lock = Mutex()
|
||||
@@ -198,6 +209,12 @@ class MatchController<S : Any, M : Any>(
|
||||
val seat = rules.currentSeat(current)
|
||||
if (seat == null) {
|
||||
// Nobody on the clock: a hand has been scored, or the game ended.
|
||||
if (role == Role.HOST && awaitsConfirmation(current)) {
|
||||
// Hold here until a player has read the scoreboard. Only the
|
||||
// host waits; clients simply render what they were sent.
|
||||
_awaitingConfirmation.value = true
|
||||
return
|
||||
}
|
||||
delay(aiThinkingMillis() * 2)
|
||||
val advanced = advanceIdle(current)
|
||||
if (advanced == null) {
|
||||
@@ -261,6 +278,18 @@ class MatchController<S : Any, M : Any>(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases a table stopped on a scoreboard. A no-op anywhere else, and on a
|
||||
* client — where the host owns the decision — it does nothing but clear the
|
||||
* local flag.
|
||||
*/
|
||||
fun confirmAdvance() {
|
||||
if (!_awaitingConfirmation.value) return
|
||||
_awaitingConfirmation.value = false
|
||||
if (role != Role.HOST) return
|
||||
scope.launch { driveIdleSeats() }
|
||||
}
|
||||
|
||||
fun close() {
|
||||
peers.values.forEach { runCatching { it.close() } }
|
||||
peers.clear()
|
||||
|
||||
@@ -263,7 +263,7 @@ private fun GolfOptionsEditor(options: GolfOptions, onChange: (GolfOptions) -> U
|
||||
)
|
||||
ChipRow(
|
||||
label = "Cards each",
|
||||
values = listOf(4, 6, 9),
|
||||
values = listOf(4, 6, 8, 9),
|
||||
selected = options.gridSize,
|
||||
display = { "$it" },
|
||||
onSelect = { size ->
|
||||
@@ -290,6 +290,13 @@ private fun GolfOptionsEditor(options: GolfOptions, onChange: (GolfOptions) -> U
|
||||
display = { "$it" },
|
||||
onSelect = { onChange(options.copy(startingReveals = it)) },
|
||||
)
|
||||
ToggleRow(
|
||||
title = "Line up the final putt",
|
||||
subtitle = "Throw a drawn card away without turning your last card over, " +
|
||||
"rather than being forced to close the hole",
|
||||
checked = options.lineUpFinalPutt,
|
||||
onChange = { onChange(options.copy(lineUpFinalPutt = it)) },
|
||||
)
|
||||
Text(
|
||||
text = "${options.rows} rows of ${options.cols}. Matching columns cancel out" +
|
||||
if (options.gridSize == 9) ", and so do matching rows." else ".",
|
||||
|
||||
@@ -145,6 +145,8 @@ fun PlayScreen(
|
||||
advanceIdle = { state ->
|
||||
if (state.phase == GolfPhase.HOLE_OVER) GolfRules.nextHole(state) else null
|
||||
},
|
||||
// The score card is read before the next hole is dealt.
|
||||
awaitsConfirmation = { state -> state.phase == GolfPhase.HOLE_OVER },
|
||||
aiThinkingMillis = { settings.scaled(700L) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -31,15 +34,16 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.prolibertate.games.game.golf.DiscardAndFlip
|
||||
import org.prolibertate.games.game.golf.DiscardOnly
|
||||
import org.prolibertate.games.game.golf.DrawFromDiscard
|
||||
import org.prolibertate.games.game.golf.DrawFromStock
|
||||
import org.prolibertate.games.game.golf.GolfMove
|
||||
import org.prolibertate.games.game.golf.GolfPhase
|
||||
import org.prolibertate.games.game.golf.GolfState
|
||||
import org.prolibertate.games.game.golf.ReplaceCard
|
||||
import org.prolibertate.games.game.golf.RevealCard
|
||||
import org.prolibertate.games.game.golf.golfValue
|
||||
import org.prolibertate.games.net.MatchController
|
||||
import org.prolibertate.games.ui.PrimaryAction
|
||||
import org.prolibertate.games.ui.ScreenScaffold
|
||||
import org.prolibertate.games.ui.common.CardBackView
|
||||
import org.prolibertate.games.ui.common.PlayingCardView
|
||||
@@ -109,21 +113,16 @@ fun GolfScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (current.phase) {
|
||||
GolfPhase.HOLE_OVER -> Text(
|
||||
text = "Hole ${current.hole + 1}: " + current.holeScores
|
||||
.mapIndexed { seat, s -> "seat $seat $s" }.joinToString(", "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
|
||||
GolfPhase.GAME_OVER -> {
|
||||
Text("Game over — lowest score wins.", fontWeight = FontWeight.Bold)
|
||||
PrimaryAction(text = "Back to the menu") { onExit() }
|
||||
// Only offered on your last face-down card, and only with
|
||||
// the option switched on in setup.
|
||||
if (legal.contains(DiscardOnly)) {
|
||||
OutlinedButton(
|
||||
onClick = { controller.submit(DiscardOnly) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Line up the putt — throw it, keep the last card down")
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
OwnGrid(
|
||||
@@ -135,10 +134,86 @@ fun GolfScreen(
|
||||
onMove = { controller.submit(it); mode = PlaceMode.SWAP },
|
||||
)
|
||||
}
|
||||
|
||||
// The card is read before the next one is dealt, so the scoreboard
|
||||
// is a dialog the players dismiss rather than a line that flashes by.
|
||||
if (current.phase == GolfPhase.HOLE_OVER || current.phase == GolfPhase.GAME_OVER) {
|
||||
ScoreCardDialog(
|
||||
state = current,
|
||||
localSeat = localSeat,
|
||||
onContinue = { controller.confirmAdvance() },
|
||||
onExit = onExit,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The card at the end of a hole: this hole's strokes and the running total for
|
||||
* everyone, lowest first.
|
||||
*/
|
||||
@Composable
|
||||
private fun ScoreCardDialog(
|
||||
state: GolfState,
|
||||
localSeat: Int,
|
||||
onContinue: () -> Unit,
|
||||
onExit: () -> Unit,
|
||||
) {
|
||||
var dismissed by remember(state.hole, state.phase) { mutableStateOf(false) }
|
||||
if (dismissed) return
|
||||
|
||||
val finalHole = state.phase == GolfPhase.GAME_OVER
|
||||
val standings = (0 until state.options.playerCount).sortedBy { state.scores[it] }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
title = {
|
||||
Text(
|
||||
if (finalHole) "Final card" else "Hole ${state.hole + 1} of ${state.options.holes}"
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Player", modifier = Modifier.weight(1f), fontWeight = FontWeight.Bold)
|
||||
Text("Hole", fontWeight = FontWeight.Bold)
|
||||
Text(" Total", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
standings.forEach { seat ->
|
||||
val you = seat == localSeat
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = if (you) "You" else "Seat $seat",
|
||||
modifier = Modifier.weight(1f),
|
||||
fontWeight = if (you) FontWeight.Bold else FontWeight.Normal,
|
||||
)
|
||||
Text("${state.holeScores[seat]}")
|
||||
Text(" ${state.scores[seat]}",
|
||||
fontWeight = if (you) FontWeight.Bold else FontWeight.Normal)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = if (finalHole) {
|
||||
"Lowest total wins — seat ${standings.first()} takes it."
|
||||
} else {
|
||||
"Lowest total wins."
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
dismissed = true
|
||||
if (finalHole) onExit() else onContinue()
|
||||
}) {
|
||||
Text(if (finalHole) "Back to the menu" else "Deal the next hole")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScoreLine(state: GolfState, localSeat: Int) {
|
||||
Row(
|
||||
@@ -226,10 +301,18 @@ private fun OwnGrid(
|
||||
val options = state.options
|
||||
val replacements = legal.filterIsInstance<ReplaceCard>().map { it.index }.toSet()
|
||||
val flips = legal.filterIsInstance<DiscardAndFlip>().map { it.index }.toSet()
|
||||
val reveals = legal.filterIsInstance<RevealCard>().map { it.index }.toSet()
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = when {
|
||||
state.phase == GolfPhase.SETUP && reveals.isNotEmpty() -> {
|
||||
val left = state.options.startingReveals - state.revealed[localSeat].count { it }
|
||||
"Choose $left more card(s) to look at"
|
||||
}
|
||||
|
||||
state.phase == GolfPhase.SETUP -> "Waiting for the others to choose…"
|
||||
|
||||
state.phase == GolfPhase.PLACE && mode == PlaceMode.SWAP ->
|
||||
"Tap a card to swap it out"
|
||||
|
||||
@@ -244,9 +327,13 @@ private fun OwnGrid(
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
for (col in 0 until options.cols) {
|
||||
val index = row * options.cols + col
|
||||
val tappable = when (mode) {
|
||||
PlaceMode.SWAP -> index in replacements
|
||||
PlaceMode.THROW -> index in flips
|
||||
val tappable = if (state.phase == GolfPhase.SETUP) {
|
||||
index in reveals
|
||||
} else {
|
||||
when (mode) {
|
||||
PlaceMode.SWAP -> index in replacements
|
||||
PlaceMode.THROW -> index in flips
|
||||
}
|
||||
}
|
||||
GridCard(
|
||||
card = state.grids[localSeat].getOrNull(index),
|
||||
@@ -254,9 +341,10 @@ private fun OwnGrid(
|
||||
width = cardWidth,
|
||||
tappable = tappable,
|
||||
onClick = {
|
||||
when (mode) {
|
||||
PlaceMode.SWAP -> onMove(ReplaceCard(index))
|
||||
PlaceMode.THROW -> onMove(DiscardAndFlip(index))
|
||||
when {
|
||||
state.phase == GolfPhase.SETUP -> onMove(RevealCard(index))
|
||||
mode == PlaceMode.SWAP -> onMove(ReplaceCard(index))
|
||||
else -> onMove(DiscardAndFlip(index))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user