Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ComputerSolitaire.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 0.8.0;
MARKETING_VERSION = 0.8.1;
PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire;
PRODUCT_NAME = "Computer Solitaire";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -376,7 +376,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 0.8.0;
MARKETING_VERSION = 0.8.1;
PRODUCT_BUNDLE_IDENTIFIER = com.crapshack.ComputerSolitaire;
PRODUCT_NAME = "Computer Solitaire";
REGISTER_APP_GROUPS = YES;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum CanfieldAutoMoveAdvisor {
nonisolated enum CanfieldAutoMoveAdvisor {
static func allowsTableauPickup(of cards: [Card], in state: GameState) -> Bool {
// Canfield's defining transfer rule: a pile moves between tableau
// piles only in its entirety, and its exposed top card plays to a
Expand Down
4 changes: 2 additions & 2 deletions ComputerSolitaire/Game/Canfield/CanfieldPlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import Foundation
/// hint wins 25.0% of games versus the 1.2% random control, with zero
/// stalemate loops, zero exact-position revisits, and every loss an honest
/// deadlock proven by an exhaustive search.
enum CanfieldPlanner {
nonisolated enum CanfieldPlanner {
struct Limits {
var maxNodes: Int
var maxDepth: Int
Expand Down Expand Up @@ -198,7 +198,7 @@ enum CanfieldPlanner {

// MARK: - Search internals

private extension CanfieldPlanner {
nonisolated private extension CanfieldPlanner {
static func search(in state: GameState, limits: Limits) -> SearchOutcome {
let rootScore = score(state)
var nodes: [Node] = [Node(state: state, parent: -1, action: nil, depth: 0, score: rootScore)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum CanfieldPersistenceRules {
nonisolated enum CanfieldPersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.tableau.count == CanfieldGameRules.tableauPileCount else { return false }
// Every tableau card deals (and stays) face up, and every pile stays a
Expand Down
4 changes: 2 additions & 2 deletions ComputerSolitaire/Game/Canfield/GameRulesCanfield.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum CanfieldGameRules {
nonisolated enum CanfieldGameRules {
static let tableauPileCount = 4
static let reserveCardCount = 13
/// The 52-card deal minus the reserve, the base card, and four tableau cards.
Expand Down Expand Up @@ -137,7 +137,7 @@ enum CanfieldGameRules {
}
}

private extension CanfieldGameRules {
nonisolated private extension CanfieldGameRules {
/// The raw rank one step above `rank`, turning the corner from King to Ace.
static func wrappedRankAbove(_ rank: Rank) -> Int {
rank.rawValue % Rank.allCases.count + 1
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Canfield/GameStateCanfield.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
/// The Canfield deal: thirteen cards face down into the reserve with its
/// top card turned face up, one face-up base card onto the first
/// foundation (its rank is where all four foundations start), one face-up
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum FortyThievesAutoMoveAdvisor {
nonisolated enum FortyThievesAutoMoveAdvisor {
static func allowsTableauPickup(of cards: [Card], in state: GameState) -> Bool {
// Forty Thieves' defining rule: only the exposed top card of a column
// moves — never a sequence, however well ordered.
Expand Down
4 changes: 2 additions & 2 deletions ComputerSolitaire/Game/FortyThieves/FortyThievesPlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Foundation
/// tap-crossing lines only win when the plays they enable pay for them.
/// Foundations are locked and `candidateSelections` offers no foundation
/// sources for rollback-free variants, so there is no rollback stage.
enum FortyThievesPlanner {
nonisolated enum FortyThievesPlanner {
struct Limits {
var maxNodes: Int
var maxDepth: Int
Expand Down Expand Up @@ -164,7 +164,7 @@ enum FortyThievesPlanner {

// MARK: - Search internals

private extension FortyThievesPlanner {
nonisolated private extension FortyThievesPlanner {
static func search(in state: GameState, limits: Limits) -> SearchOutcome {
let rootScore = score(state)
var nodes: [Node] = [Node(state: state, parent: -1, action: nil, depth: 0, score: rootScore)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum FortyThievesPersistenceRules {
nonisolated enum FortyThievesPersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.tableau.count == FortyThievesGameRules.columnCount else { return false }
// Every board card is dealt (and stays) face up. No depth cap: columns
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum FortyThievesGameRules {
nonisolated enum FortyThievesGameRules {
static let columnCount = 10
static let dealColumnDepth = 4
static let dealTableauCardCount = 40
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
/// The Forty Thieves deal: two full decks shuffled together, ten columns
/// of four face-up cards dealt column-major (column 0 bottom-to-top first,
/// then column 1, and so on), and the remaining 64 cards face down in the
Expand Down
29 changes: 29 additions & 0 deletions ComputerSolitaire/Game/FreeCell/AutoFinishPlannerFreeCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

nonisolated extension AutoFinishPlanner {
/// A necessary condition for a FreeCell foundation run, so the win
/// simulation only runs on positions that could plausibly pass it.
/// Auto-finish plays nothing but cascade tops (and free cells) onto the
/// foundations, so a buried card can only reach its foundation after every
/// same-suit card above it — and the foundation ascends, so each of those
/// must outrank it. A cascade holding a same-suit pair whose deeper card
/// is the lower rank can therefore never drain, and the position can never
/// auto-finish. The check is monotone under the run's own moves (removing
/// tops cannot create such a pair), so a run that starts available stays
/// available step to step exactly as before.
static func freeCellCascadesAllowFoundationRun(_ state: GameState) -> Bool {
for pile in state.tableau {
// Bottom to top, each suit's ranks must strictly descend; track
// the lowest rank seen so far per suit and reject any card that
// sits above a lower-ranked card of its own suit.
var lowestRankBySuit: [Suit: Rank] = [:]
for card in pile {
if let lowest = lowestRankBySuit[card.suit], card.rank > lowest {
return false
}
lowestRankBySuit[card.suit] = card.rank
}
}
return true
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum FreeCellAutoMoveAdvisor {
nonisolated enum FreeCellAutoMoveAdvisor {
static func allowsTableauPickup(of cards: [Card], in state: GameState) -> Bool {
AutoMoveAdvisor.isValidTableauSequence(cards)
}
Expand Down
6 changes: 3 additions & 3 deletions ComputerSolitaire/Game/FreeCell/FreeCellSolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
/// `maxFreeCellTransferCount` rule, so every move in a returned solution is directly
/// executable in the UI. Typical deals solve in a few thousand nodes; the search stops
/// at `Limits.maxNodes` or `Limits.deadline`, whichever comes first.
enum FreeCellSolver {
nonisolated enum FreeCellSolver {
/// A card is `suitIndex << 4 | rank` (rank 1...13); suit order follows `Suit.allCases`.
typealias Code = UInt8

Expand Down Expand Up @@ -170,7 +170,7 @@ enum FreeCellSolver {

// MARK: - Board model

extension FreeCellSolver {
nonisolated extension FreeCellSolver {
struct Board: Hashable {
var cascades: [[Code]]
var cells: [Code] // 0 = empty
Expand Down Expand Up @@ -225,7 +225,7 @@ extension FreeCellSolver {

// MARK: - Search internals

private extension FreeCellSolver {
nonisolated private extension FreeCellSolver {
/// Bias strongly toward foundation progress and untangling cascades; solution
/// length matters less than finding one quickly. Tuned empirically: this config
/// solves ~99% of random deals in a median of ~3ms (p95 ~35ms).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum FreeCellPersistenceRules {
nonisolated enum FreeCellPersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.tableau.count == 8 else { return false }
guard state.stock.isEmpty, state.waste.isEmpty else { return false }
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/FreeCell/GameRulesFreeCell.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
enum FreeCellGameRules {
nonisolated enum FreeCellGameRules {
static func canMoveToTableau(card: Card, destinationPile: [Card]) -> Bool {
if destinationPile.isEmpty {
return true
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/FreeCell/GameStateFreeCell.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
static func newFreeCellGame() -> GameState {
var deck = Card.fullDeck().shuffled()
var tableau = Array(repeating: [Card](), count: 8)
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Golf/AutoMoveAdvisorGolf.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation
/// variants — its only move is exposed column card onto the waste — so
/// `AutoMoveAdvisor` dispatches to it wholesale instead of threading its moves
/// through the pile-oriented hooks.
enum GolfAutoMoveAdvisor {
nonisolated enum GolfAutoMoveAdvisor {
/// The exposed (last) card of every non-empty column, each as a
/// single-card selection. The waste top is never a selection: in Golf it
/// is the match target, not a mover.
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Golf/GamePersistenceGolf.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum GolfPersistenceRules {
nonisolated enum GolfPersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.tableau.count == GolfGameRules.columnCount else { return false }
// Columns deal five cards and only ever shrink; every board card is
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Golf/GameRulesGolf.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum GolfGameRules {
nonisolated enum GolfGameRules {
static let columnCount = 7
static let columnDepth = 5
static let dealTableauCardCount = 35
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Golf/GameStateGolf.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
/// The Golf deal: seven columns of five face-up cards dealt column-major
/// (column 0 bottom-to-top first, then column 1, and so on), one face-up
/// card starting the waste, and the remaining 16 cards face down in the
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Golf/GolfMatchState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
/// match total is a plain sum. Lower is better throughout, and negative
/// scores are legal results (a cleared board banks one bonus stroke per
/// leftover stock card).
struct GolfMatchState: Codable, Equatable {
nonisolated struct GolfMatchState: Codable, Equatable {
static let holeCount = 9
/// Traditional par framing: a nine-hole total of 45 or under is par.
static let parTotal = 45
Expand Down
10 changes: 5 additions & 5 deletions ComputerSolitaire/Game/Golf/GolfPlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import Foundation
///
/// Hint-quality baselines live in the `tools/hint-probe` ledger; the measured
/// verdict split is recorded below `Limits`.
enum GolfPlanner {
nonisolated enum GolfPlanner {
struct Limits {
var maxNodes: Int
var deadline: Date?
Expand Down Expand Up @@ -175,7 +175,7 @@ enum GolfPlanner {

// MARK: - Session move mapping

private extension GolfPlanner {
nonisolated private extension GolfPlanner {
static func sessionMove(
for move: Move,
in state: GameState
Expand All @@ -197,7 +197,7 @@ private extension GolfPlanner {

// MARK: - Compact position

private extension GolfPlanner {
nonisolated private extension GolfPlanner {
/// The deal's immutable rank tables plus the packed dynamic board. Stock cards
/// are indexed in draw order relative to the root; cards already in the waste
/// below its top are simply absent — the search never needs them.
Expand Down Expand Up @@ -283,7 +283,7 @@ private extension GolfPlanner {

// MARK: - Move generation and transitions

private extension GolfPlanner {
nonisolated private extension GolfPlanner {
/// Legal moves in a fixed, deterministic order: playable columns ascending,
/// then draw — so equal-depth ties favor clearing over flipping and lines
/// read sensibly. Legality is `GolfGameRules.canPlayRank`, so the strict
Expand Down Expand Up @@ -327,7 +327,7 @@ private extension GolfPlanner {

// MARK: - Search

private extension GolfPlanner {
nonisolated private extension GolfPlanner {
/// One explored position, packed to 12 bytes so the million-node budget
/// costs ~12 MB of nodes instead of ~50: the 30-bit board code stands in
/// for the whole board (it round-trips through `Board(code:)`), the move
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum KlondikeAutoMoveAdvisor {
nonisolated enum KlondikeAutoMoveAdvisor {
static func allowsTableauPickup(of cards: [Card], in state: GameState) -> Bool {
AutoMoveAdvisor.isValidTableauSequence(cards)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum KlondikePersistenceRules {
nonisolated enum KlondikePersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.tableau.count == 7 else { return false }
// Klondike renders no free-cell slots, so a card stranded there would be
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Klondike/GameRulesKlondike.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
enum KlondikeGameRules {
nonisolated enum KlondikeGameRules {
static func canMoveToTableau(card: Card, destinationPile: [Card]) -> Bool {
SharedGameRules.canMoveToKingAnchoredTableau(card: card, destinationPile: destinationPile)
}
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Klondike/GameStateKlondike.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
static func newKlondikeGame() -> GameState {
var deck = Card.fullDeck().shuffled()
var tableau = Array(repeating: [Card](), count: 7)
Expand Down
4 changes: 2 additions & 2 deletions ComputerSolitaire/Game/Klondike/KlondikePlanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
///
/// The search reads the true state, including cards the player hasn't seen yet, but it
/// only ever recommends actions that are legal right now.
enum KlondikePlanner {
nonisolated enum KlondikePlanner {
struct Limits {
var maxNodes: Int
var maxDepth: Int
Expand Down Expand Up @@ -99,7 +99,7 @@ enum KlondikePlanner {

// MARK: - Search internals

private extension KlondikePlanner {
nonisolated private extension KlondikePlanner {
enum Action {
case move(Selection, Destination)
case stockTap
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
/// Pyramid shares no foundation/tableau/free-cell move algebra with the other
/// variants, so `AutoMoveAdvisor` dispatches to it wholesale instead of threading
/// its moves through the pile-oriented hooks.
enum PyramidAutoMoveAdvisor {
nonisolated enum PyramidAutoMoveAdvisor {
/// The top waste card (if any) plus every selectable pyramid card, each as a
/// single-card selection.
static func candidateSelections(in state: GameState) -> [Selection] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum PyramidPersistenceRules {
nonisolated enum PyramidPersistenceRules {
static func hasValidLayout(state: GameState) -> Bool {
guard state.pyramid.count == PyramidGeometry.cardCount else { return false }
guard state.tableau.isEmpty else { return false }
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Pyramid/GameRulesPyramid.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

enum PyramidGameRules {
nonisolated enum PyramidGameRules {
/// Two cards pair when their ranks sum to this; a King reaches it alone.
static let pairSum = 13
/// The waste may be recycled into the stock this many times (three total passes).
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Pyramid/GameStatePyramid.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

extension GameState {
nonisolated extension GameState {
static func newPyramidGame() -> GameState {
var deck = Card.fullDeck().shuffled()
var pyramid: [Card?] = []
Expand Down
2 changes: 1 addition & 1 deletion ComputerSolitaire/Game/Pyramid/PyramidGeometry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Foundation
/// Slots are row-major: row `r` (0-based, apex first) occupies indices
/// `r(r+1)/2 ..< (r+1)(r+2)/2`, so row 0 is slot 0 and row 6 is slots 21...27.
/// Each slot except the bottom row is covered by two slots in the row below.
enum PyramidGeometry {
nonisolated enum PyramidGeometry {
static let rowCount = 7
static let cardCount = 28

Expand Down
Loading