Complete native rewrite of the web-based SoliCards game as a SwiftUI multiplatform app targeting iOS 17+, iPadOS 17+, and macOS 14+. Three solitaire variants (Klondike, Spider, FreeCell) with full game rules, drag & drop, smart zoom layout, 6 themes, 4 difficulty levels, SwiftData persistence, VoiceOver accessibility, and 57 unit tests. Key features: - MVVM + Protocol-Oriented Strategy architecture - DragGesture with coordinate-space hit-testing (long press + drag) - Smart zoom: cards auto-size to fit screen based on deepest column - Landscape: 30% bigger cards with scrollable overflow (iOS) - macOS: 120pt card cap, 92% height buffer for window resizing - Auto-save, game resume, statistics tracking via SwiftData - Privacy manifest, app icon, String Catalog, zero dependencies Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
87 lines
2.8 KiB
Swift
87 lines
2.8 KiB
Swift
import Testing
|
|
@testable import SoliCards
|
|
|
|
@Suite("Spider Rules Tests")
|
|
struct SpiderRulesTests {
|
|
let rules = SpiderRules()
|
|
|
|
@Test("Deal creates correct layout with 2 decks")
|
|
func dealLayout() {
|
|
let deck = Deck.double()
|
|
let snapshot = rules.deal(deck: deck)
|
|
|
|
#expect(snapshot.tableaus.count == 10)
|
|
// First 4 get 6 cards, last 6 get 5 cards
|
|
for i in 0..<4 {
|
|
#expect(snapshot.tableaus[i].count == 6)
|
|
}
|
|
for i in 4..<10 {
|
|
#expect(snapshot.tableaus[i].count == 5)
|
|
}
|
|
|
|
// Total dealt: 4*6 + 6*5 = 24 + 30 = 54
|
|
let totalDealt = snapshot.tableaus.reduce(0) { $0 + $1.count }
|
|
#expect(totalDealt == 54)
|
|
|
|
// Remaining in stock: 104 - 54 = 50
|
|
#expect(snapshot.stock.count == 50)
|
|
|
|
// 8 empty foundations
|
|
#expect(snapshot.foundations.count == 8)
|
|
}
|
|
|
|
@Test("Can place any card on descending rank in tableau")
|
|
func tableauStacking() {
|
|
let ten = Card(suit: .spades, rank: .ten, isFaceUp: true)
|
|
let nine = Card(suit: .hearts, rank: .nine, isFaceUp: true) // different suit OK
|
|
var snapshot = emptySpiderSnapshot()
|
|
snapshot.tableaus[0] = [ten]
|
|
snapshot.tableaus[1] = [nine]
|
|
|
|
#expect(rules.canMove(cards: [nine], from: .tableau(1), to: .tableau(0), state: snapshot))
|
|
}
|
|
|
|
@Test("Complete K-A same-suit sequence detected")
|
|
func completeSequence() {
|
|
var tableau: [Card] = []
|
|
for rank in Rank.allCases.reversed() {
|
|
tableau.append(Card(suit: .spades, rank: rank, isFaceUp: true))
|
|
}
|
|
#expect(rules.isCompleteSequence(in: tableau))
|
|
}
|
|
|
|
@Test("Mixed-suit sequence not complete")
|
|
func mixedSuitSequence() {
|
|
var tableau: [Card] = []
|
|
for (i, rank) in Rank.allCases.reversed().enumerated() {
|
|
let suit: Suit = i == 5 ? .hearts : .spades
|
|
tableau.append(Card(suit: suit, rank: rank, isFaceUp: true))
|
|
}
|
|
#expect(!rules.isCompleteSequence(in: tableau))
|
|
}
|
|
|
|
@Test("Cannot pick up mixed-suit sequence")
|
|
func cannotPickUpMixedSuit() {
|
|
let spade5 = Card(suit: .spades, rank: .five, isFaceUp: true)
|
|
let heart4 = Card(suit: .hearts, rank: .four, isFaceUp: true)
|
|
var snapshot = emptySpiderSnapshot()
|
|
snapshot.tableaus[0] = [spade5, heart4]
|
|
|
|
#expect(!rules.canPickUp(cards: [spade5, heart4], from: .tableau(0), state: snapshot))
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func emptySpiderSnapshot() -> GameSnapshot {
|
|
GameSnapshot(
|
|
tableaus: Array(repeating: [], count: 10),
|
|
foundations: Array(repeating: [], count: 8),
|
|
stock: [],
|
|
waste: [],
|
|
freeCells: [],
|
|
moves: 0,
|
|
score: 0
|
|
)
|
|
}
|
|
}
|