From 3dbc4b5557bb7c565db55a9db75efb6842953413 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 15:30:15 +0000 Subject: [PATCH] Count the ace low in Klondike Rank.order puts the ace at fourteen, which is right for the games this deck was built for -- in Euchre, Kaiser and the rest it is the highest card there is. Patience counts it the other way: foundations run ace upwards and columns run down to an ace. Using the raw order meant a two could never follow an ace home and an ace could never be laid on a two, so no foundation could be built past its first card. Nothing about the deal or the moves would have looked wrong; the game would simply never have gone out. patienceOrder is local to this game rather than a change to Rank, because ace high is correct everywhere else in the app. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC --- .../games/game/klondike/KlondikeModel.kt | 14 +++++++++-- .../games/game/klondike/KlondikeRulesTest.kt | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) 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 cf8ed09..6b02b9d 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 @@ -120,16 +120,26 @@ data class MoveCards(val from: Spot, val to: Spot, val count: Int = 1) : Klondik /** Foundations are indexed by suit, so a card always knows which one it wants. */ fun foundationFor(card: Card): Int = card.suit.ordinal +/** + * Where a rank sits when the ace is low. + * + * [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. + */ +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.order == top.rank.order + 1 + 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.order == onto.rank.order - 1 && lower.suit.isRed != onto.suit.isRed + lower.rank.patienceOrder == onto.rank.patienceOrder - 1 && lower.suit.isRed != onto.suit.isRed /** * Whether [cards] is a run that can travel as one: each card a step below the 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 e9ada33..9f62f0e 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 @@ -112,6 +112,30 @@ class KlondikeRulesTest { // ---- building ---------------------------------------------------------- + @Test + fun `the ace is low here, whatever the rest of the deck thinks`() { + // Rank.order puts the ace at fourteen, because the deck was built for + // trick-taking games where it is the highest card there is. Patience + // counts it as one, and every comparison in this game has to agree — + // otherwise a two can never follow an ace home and the game is + // unwinnable in a way nothing else would show. + assertEquals(14, Rank.ACE.order) + assertEquals(1, Rank.ACE.patienceOrder) + assertEquals(13, Rank.KING.patienceOrder) + + var foundation = emptyList() + Rank.standard.sortedBy { it.patienceOrder }.forEach { rank -> + val card = c(rank, Suit.SPADES) + assertTrue("$rank should go home next", acceptsOnFoundation(foundation, card)) + foundation = foundation + card + } + assertEquals(13, foundation.size) + + // And a column runs the other way, down to an ace. + assertTrue(buildsDown(c(Rank.ACE, Suit.HEARTS), c(Rank.TWO, Suit.SPADES))) + assertTrue(buildsDown(c(Rank.QUEEN, Suit.HEARTS), c(Rank.KING, Suit.SPADES))) + } + @Test fun `foundations run ace upwards in one suit`() { assertTrue(acceptsOnFoundation(emptyList(), c(Rank.ACE, Suit.SPADES)))