From 7569f0b89197793a18cfe4dc833cad13ccca9922 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 15:44:53 +0000 Subject: [PATCH] Share the ace-low convention rather than copy it Klondike had its own patienceOrder, acceptsOnFoundation and buildsDown. FreeCell builds the same way, Spider builds in suit, and Pyramid needs an ace worth one to pair to thirteen -- four separate definitions of "the ace is low" would be four chances to disagree, and the one that already went wrong cost a CI round trip to find. game.solitaire.Patience now holds the ordering and the build rules, and Klondike reads them from there. buildsDown stays as a name in the Klondike package because that is what this game builds with; it just no longer defines what it means. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC --- .../games/game/klondike/KlondikeModel.kt | 23 +++-------- .../games/game/klondike/KlondikeRules.kt | 1 + .../games/game/solitaire/Patience.kt | 41 +++++++++++++++++++ .../games/game/klondike/KlondikeRulesTest.kt | 2 + 4 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/prolibertate/games/game/solitaire/Patience.kt diff --git a/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeModel.kt b/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeModel.kt index 6b02b9d..24f29b5 100644 --- a/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeModel.kt +++ b/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeModel.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable import org.prolibertate.games.game.cards.Card import org.prolibertate.games.game.cards.Rank import org.prolibertate.games.game.cards.Suit +import org.prolibertate.games.game.solitaire.buildsDownAlternating const val TABLEAU_PILES: Int = 7 const val FOUNDATIONS: Int = 4 @@ -121,25 +122,13 @@ data class MoveCards(val from: Spot, val to: Spot, val count: Int = 1) : Klondik fun foundationFor(card: Card): Int = card.suit.ordinal /** - * Where a rank sits when the ace is low. + * Down by one and in the other colour. * - * [Rank.order] puts the ace at fourteen, because the games this deck was built - * for are trick-taking ones where it is the highest card there is. Patience - * counts it the other way: foundations run ace upwards and columns run down to - * an ace, so every comparison here has to use this and not the raw order. + * Named here because it is what this game builds with, but the rule itself is + * shared: FreeCell builds the same way, Spider builds in suit, and all of them + * need the same ace-low ordering underneath. */ -val Rank.patienceOrder: Int get() = if (this == Rank.ACE) 1 else order - -/** Ace first, then strictly upwards in the same suit. */ -fun acceptsOnFoundation(foundation: List, card: Card): Boolean { - val top = foundation.lastOrNull() - ?: return card.rank == Rank.ACE - return card.suit == top.suit && card.rank.patienceOrder == top.rank.patienceOrder + 1 -} - -/** Down by one, and in the other colour. */ -fun buildsDown(lower: Card, onto: Card): Boolean = - lower.rank.patienceOrder == onto.rank.patienceOrder - 1 && lower.suit.isRed != onto.suit.isRed +fun buildsDown(lower: Card, onto: Card): Boolean = buildsDownAlternating(lower, onto) /** * Whether [cards] is a run that can travel as one: each card a step below the diff --git a/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeRules.kt b/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeRules.kt index ce1c175..5419e0c 100644 --- a/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeRules.kt +++ b/app/src/main/java/org/prolibertate/games/game/klondike/KlondikeRules.kt @@ -8,6 +8,7 @@ import org.prolibertate.games.game.cards.Decks import org.prolibertate.games.game.cards.shuffledWith import org.prolibertate.games.game.engine.GameRules import org.prolibertate.games.game.engine.TableConfig +import org.prolibertate.games.game.solitaire.acceptsOnFoundation import kotlin.random.Random /** diff --git a/app/src/main/java/org/prolibertate/games/game/solitaire/Patience.kt b/app/src/main/java/org/prolibertate/games/game/solitaire/Patience.kt new file mode 100644 index 0000000..653561f --- /dev/null +++ b/app/src/main/java/org/prolibertate/games/game/solitaire/Patience.kt @@ -0,0 +1,41 @@ +package org.prolibertate.games.game.solitaire + +import org.prolibertate.games.game.cards.Card +import org.prolibertate.games.game.cards.Rank + +/** + * Where a rank sits when the ace is low. + * + * [Rank.order] puts the ace at fourteen, which is right for the games this deck + * was built for: in Euchre, Kaiser, Wizard and the rest it is the highest card + * there is. Every patience in this app counts it the other way — foundations run + * ace upwards, columns run down to an ace, and Pyramid needs it to be worth one + * when a pair has to make thirteen. + * + * It lives here rather than on [Rank] because ace high is correct everywhere + * outside these games, and it is shared rather than copied because four + * separate definitions of "the ace is low" would be four chances to disagree. + */ +val Rank.patienceOrder: Int get() = if (this == Rank.ACE) 1 else order + +/** Down by one, and in the other colour: the ordinary tableau build. */ +fun buildsDownAlternating(lower: Card, onto: Card): Boolean = + lower.rank.patienceOrder == onto.rank.patienceOrder - 1 && + lower.suit.isRed != onto.suit.isRed + +/** Down by one in the same suit, which is what Spider pays for. */ +fun buildsDownInSuit(lower: Card, onto: Card): Boolean = + lower.rank.patienceOrder == onto.rank.patienceOrder - 1 && lower.suit == onto.suit + +/** Ace first, then strictly upwards in the same suit. */ +fun acceptsOnFoundation(foundation: List, card: Card): Boolean { + val top = foundation.lastOrNull() ?: return card.rank == Rank.ACE + return card.suit == top.suit && card.rank.patienceOrder == top.rank.patienceOrder + 1 +} + +/** + * Whether [cards] is a run that travels as one, by whatever rule the game + * builds with. + */ +fun isRun(cards: List, builds: (Card, Card) -> Boolean): Boolean = + cards.isNotEmpty() && cards.zipWithNext().all { (upper, lower) -> builds(lower, upper) } diff --git a/app/src/test/java/org/prolibertate/games/game/klondike/KlondikeRulesTest.kt b/app/src/test/java/org/prolibertate/games/game/klondike/KlondikeRulesTest.kt index 9f62f0e..e86778f 100644 --- a/app/src/test/java/org/prolibertate/games/game/klondike/KlondikeRulesTest.kt +++ b/app/src/test/java/org/prolibertate/games/game/klondike/KlondikeRulesTest.kt @@ -13,6 +13,8 @@ import org.prolibertate.games.game.cards.Suit import org.prolibertate.games.game.engine.PlayerKind import org.prolibertate.games.game.engine.PlayerSlot import org.prolibertate.games.game.engine.TableConfig +import org.prolibertate.games.game.solitaire.acceptsOnFoundation +import org.prolibertate.games.game.solitaire.patienceOrder class KlondikeRulesTest {