import Foundation struct KlondikeRules: GameRules { let variant: GameVariant = .klondike func deal(deck: [Card]) -> GameSnapshot { var remaining = deck var tableaus: [[Card]] = [] // Deal 7 tableaus: column i gets i+1 cards, top card face-up for i in 0..<7 { var column: [Card] = [] for j in 0...i { var card = remaining.removeFirst() card.isFaceUp = (j == i) column.append(card) } tableaus.append(column) } return GameSnapshot( tableaus: tableaus, foundations: [[], [], [], []], stock: remaining, waste: [], freeCells: [], moves: 0, score: 0 ) } func canMove(cards: [Card], from: CardLocation, to: CardLocation, state: GameSnapshot) -> Bool { guard let firstCard = cards.first else { return false } switch to { case .foundation(let index): guard cards.count == 1 else { return false } guard index >= 0, index < state.foundations.count else { return false } let foundation = state.foundations[index] return MoveValidator.canPlaceOnFoundation(firstCard, topCard: foundation.last) case .tableau(let index): guard index >= 0, index < state.tableaus.count else { return false } let tableau = state.tableaus[index] if tableau.isEmpty { return MoveValidator.isKing(firstCard) } guard let topCard = tableau.last, topCard.isFaceUp else { return false } return MoveValidator.isAlternatingColor(firstCard, with: topCard) && MoveValidator.isDescending(firstCard, onto: topCard) case .waste, .stock, .freeCell: return false } } func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction? { if state.stock.isEmpty { guard !state.waste.isEmpty else { return nil } // Reset: move waste back to stock, reversed state.stock = state.waste.reversed().map { card in var c = card c.isFaceUp = false return c } state.waste = [] return MoveAction(cards: [], from: .waste, to: .stock, didFlipCard: false, scoreChange: 0) } var drawn: [Card] = [] for _ in 0.. Int { switch (from, to) { case (.waste, .foundation): return 10 case (.tableau, .foundation): return 10 case (.waste, .tableau): return 5 case (.foundation, .tableau): return -15 default: return 0 } } func isWon(state: GameSnapshot) -> Bool { state.foundations.allSatisfy { $0.count == 13 } } func canAutoComplete(state: GameSnapshot) -> Bool { guard state.stock.isEmpty, state.waste.isEmpty else { return false } // All tableau cards must be face-up return state.tableaus.allSatisfy { column in column.allSatisfy { $0.isFaceUp } } } func findHints(state: GameSnapshot, settings: DifficultySettings) -> [HintResult] { guard settings.hintsEnabled else { return [] } var hints: [HintResult] = [] // Priority 1: Aces to foundation for (tabIndex, tableau) in state.tableaus.enumerated() { guard let topCard = tableau.last, topCard.isFaceUp, MoveValidator.isAce(topCard) else { continue } for (fIndex, foundation) in state.foundations.enumerated() { if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) { hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 1)) } } } // Priority 1: Ace from waste if let wasteTop = state.waste.last, MoveValidator.isAce(wasteTop) { for (fIndex, foundation) in state.foundations.enumerated() { if MoveValidator.canPlaceOnFoundation(wasteTop, topCard: foundation.last) { hints.append(HintResult(cards: [wasteTop], from: .waste, to: .foundation(fIndex), priority: 1)) } } } // Priority 2: Other foundation moves for (tabIndex, tableau) in state.tableaus.enumerated() { guard let topCard = tableau.last, topCard.isFaceUp, !MoveValidator.isAce(topCard) else { continue } for (fIndex, foundation) in state.foundations.enumerated() { if MoveValidator.canPlaceOnFoundation(topCard, topCard: foundation.last) { hints.append(HintResult(cards: [topCard], from: .tableau(tabIndex), to: .foundation(fIndex), priority: 2)) } } } // Priority 3: Tableau moves that reveal face-down cards for (tabIndex, tableau) in state.tableaus.enumerated() { let faceUpCards = tableau.trailingSuffix(while: { $0.isFaceUp }) guard !faceUpCards.isEmpty else { continue } let hasFaceDown = tableau.count > faceUpCards.count guard hasFaceDown else { continue } for (destIndex, _) in state.tableaus.enumerated() { guard destIndex != tabIndex else { continue } let cardsToMove = Array(faceUpCards) if canMove(cards: cardsToMove, from: .tableau(tabIndex), to: .tableau(destIndex), state: state) { hints.append(HintResult(cards: cardsToMove, from: .tableau(tabIndex), to: .tableau(destIndex), priority: 3)) } } } // Priority 4: Waste to tableau if let wasteTop = state.waste.last { for (tabIndex, _) in state.tableaus.enumerated() { if canMove(cards: [wasteTop], from: .waste, to: .tableau(tabIndex), state: state) { hints.append(HintResult(cards: [wasteTop], from: .waste, to: .tableau(tabIndex), priority: 4)) } } } // Priority 5: Draw from stock if !state.stock.isEmpty { hints.append(HintResult(cards: [], from: .stock, to: .waste, priority: 5)) } return hints.sorted { $0.priority < $1.priority } } func canStackOnTableau(card: Card, onto target: Card) -> Bool { MoveValidator.isAlternatingColor(card, with: target) && MoveValidator.isDescending(card, onto: target) } func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool { guard !cards.isEmpty else { return false } switch from { case .waste: return cards.count == 1 && cards[0] == state.waste.last case .tableau(let index): guard index >= 0, index < state.tableaus.count else { return false } let tableau = state.tableaus[index] guard cards.allSatisfy({ $0.isFaceUp }) else { return false } // Cards must be a valid sequence from the bottom of the face-up portion let faceUpCards = tableau.trailingSuffix(while: { $0.isFaceUp }) guard cards.count <= faceUpCards.count else { return false } let startIdx = faceUpCards.count - cards.count let expectedCards = Array(faceUpCards.dropFirst(startIdx)) return cards == expectedCards case .foundation(let index): guard cards.count == 1, index >= 0, index < state.foundations.count else { return false } return cards[0] == state.foundations[index].last default: return false } } }