XCode-Claude-Workflow/SoliCardsTests/GameEngine/SpiderRulesTests.swift
idev2025 0f989f5c86 feat: SoliCards v1.2.0 — native SwiftUI solitaire for iOS, iPadOS, macOS
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>
2026-04-14 07:33:52 -04:00

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
)
}
}