import Foundation struct SpiderRules: GameRules { let variant: GameVariant = .spider func deal(deck: [Card]) -> GameSnapshot { var remaining = deck var tableaus: [[Card]] = [] // Deal 10 tableaus: first 4 get 6 cards, last 6 get 5 cards, top face-up for i in 0..<10 { let cardCount = i < 4 ? 6 : 5 var column: [Card] = [] for j in 0.. Bool { guard let firstCard = cards.first else { return false } switch to { case .tableau(let index): guard index >= 0, index < state.tableaus.count else { return false } let tableau = state.tableaus[index] if tableau.isEmpty { return true } guard let topCard = tableau.last, topCard.isFaceUp else { return false } return MoveValidator.isDescending(firstCard, onto: topCard) case .foundation: // Foundations are auto-filled when a complete K→A same-suit sequence is formed return false default: return false } } func drawFromStock(state: inout GameSnapshot, drawCount: Int) -> MoveAction? { // Spider deals one card to each tableau (must have at least one card per tableau) guard !state.stock.isEmpty else { return nil } guard state.tableaus.allSatisfy({ !$0.isEmpty }) else { return nil } var drawn: [Card] = [] let dealCount = min(state.tableaus.count, state.stock.count) for i in 0.. Int { switch (from, to) { case (.tableau, .foundation): return 100 case (.tableau, .tableau): return 1 default: return 0 } } func isWon(state: GameSnapshot) -> Bool { state.foundations.allSatisfy { $0.count == 13 } } func canAutoComplete(state: GameSnapshot) -> Bool { guard state.stock.isEmpty else { return false } return state.tableaus.allSatisfy { column in column.allSatisfy { $0.isFaceUp } } } /// Check if the top cards of a tableau form a complete K→A same-suit sequence. func isCompleteSequence(in tableau: [Card]) -> Bool { guard tableau.count >= 13 else { return false } let sequence = tableau.suffix(13) let suit = sequence.first!.suit for (offset, card) in sequence.enumerated() { guard card.isFaceUp, card.suit == suit, card.rank.rawValue == 13 - offset else { return false } } return true } /// Check all tableaus for complete sequences and move them to foundations. func checkAndMoveCompleteSequences(state: inout GameSnapshot) -> Bool { var foundComplete = false for tabIndex in 0.. [HintResult] { guard settings.hintsEnabled else { return [] } var hints: [HintResult] = [] for (tabIndex, tableau) in state.tableaus.enumerated() { let faceUpRun = sameSuitDescendingRun(in: tableau) guard !faceUpRun.isEmpty else { continue } for (destIndex, _) in state.tableaus.enumerated() { guard destIndex != tabIndex else { continue } if canMove(cards: faceUpRun, from: .tableau(tabIndex), to: .tableau(destIndex), state: state) { let priority = (tableau.count - faceUpRun.count > 0 && !tableau[tableau.count - faceUpRun.count - 1].isFaceUp) ? 2 : 4 hints.append(HintResult(cards: faceUpRun, from: .tableau(tabIndex), to: .tableau(destIndex), priority: priority)) } } } 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.isDescending(card, onto: target) } func canPickUp(cards: [Card], from: CardLocation, state: GameSnapshot) -> Bool { guard !cards.isEmpty else { return false } switch from { case .tableau(let index): guard index >= 0, index < state.tableaus.count else { return false } guard cards.allSatisfy({ $0.isFaceUp }) else { return false } // Must be a same-suit descending sequence for i in 1.. [Card] { let faceUp = Array(tableau.trailingSuffix(while: { $0.isFaceUp })) guard !faceUp.isEmpty else { return [] } var run = [faceUp.last!] for i in stride(from: faceUp.count - 2, through: 0, by: -1) { let card = faceUp[i] guard MoveValidator.isSameSuit(card, as: run[0]), card.rank.rawValue == run[0].rank.rawValue + 1 else { break } run.insert(card, at: 0) } return run } }