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 <[email protected]> Claude-Session: https://claude.ai/code/session_01Urn6eEBA6XzrnfoibL7faC
This commit is contained in:
@@ -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: 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
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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: 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<Card>, builds: (Card, Card) -> Boolean): Boolean =
|
||||
cards.isNotEmpty() && cards.zipWithNext().all { (upper, lower) -> builds(lower, upper) }
|
||||
@@ -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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user