SoliCards-iOS-iPadOS-MacOS/SoliCardsTests/GameEngine/KlondikeRulesTests.swift
idev2025 de0da01f25 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.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 08:26:14 -04:00

117 lines
3.9 KiB
Swift

import Testing
@testable import SoliCards
@Suite("Klondike Rules Tests")
struct KlondikeRulesTests {
let rules = KlondikeRules()
@Test("Deal creates correct tableau layout")
func dealLayout() {
let deck = Deck.standard()
let snapshot = rules.deal(deck: deck)
// 7 tableaus with 1,2,3,4,5,6,7 cards
#expect(snapshot.tableaus.count == 7)
for i in 0..<7 {
#expect(snapshot.tableaus[i].count == i + 1)
// Top card face-up
#expect(snapshot.tableaus[i].last!.isFaceUp)
// All others face-down
for j in 0..<i {
#expect(!snapshot.tableaus[i][j].isFaceUp)
}
}
// 4 empty foundations
#expect(snapshot.foundations.count == 4)
#expect(snapshot.foundations.allSatisfy { $0.isEmpty })
// Remaining 24 cards in stock
#expect(snapshot.stock.count == 24)
#expect(snapshot.waste.isEmpty)
}
@Test("Can move ace to empty foundation")
func aceToFoundation() {
let ace = Card(suit: .hearts, rank: .ace, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [ace]
#expect(rules.canMove(cards: [ace], from: .tableau(0), to: .foundation(0), state: snapshot))
}
@Test("Cannot move non-ace to empty foundation")
func nonAceToEmptyFoundation() {
let two = Card(suit: .hearts, rank: .two, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [two]
#expect(!rules.canMove(cards: [two], from: .tableau(0), to: .foundation(0), state: snapshot))
}
@Test("Can stack alternating colors descending on tableau")
func tableauStacking() {
let blackSix = Card(suit: .spades, rank: .six, isFaceUp: true)
let redFive = Card(suit: .hearts, rank: .five, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [blackSix]
snapshot.tableaus[1] = [redFive]
#expect(rules.canMove(cards: [redFive], from: .tableau(1), to: .tableau(0), state: snapshot))
}
@Test("Cannot stack same color on tableau")
func sameColorBlocked() {
let blackSix = Card(suit: .spades, rank: .six, isFaceUp: true)
let blackFive = Card(suit: .clubs, rank: .five, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [blackSix]
snapshot.tableaus[1] = [blackFive]
#expect(!rules.canMove(cards: [blackFive], from: .tableau(1), to: .tableau(0), state: snapshot))
}
@Test("Only king on empty tableau")
func kingOnEmptyTableau() {
let king = Card(suit: .hearts, rank: .king, isFaceUp: true)
let queen = Card(suit: .hearts, rank: .queen, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[1] = [king]
snapshot.tableaus[2] = [queen]
#expect(rules.canMove(cards: [king], from: .tableau(1), to: .tableau(0), state: snapshot))
#expect(!rules.canMove(cards: [queen], from: .tableau(2), to: .tableau(0), state: snapshot))
}
@Test("Win detection")
func winDetection() {
var snapshot = emptySnapshot()
// Fill all foundations with 13 cards each
for i in 0..<4 {
let suit = Suit.allCases[i]
snapshot.foundations[i] = Rank.allCases.map { Card(suit: suit, rank: $0, isFaceUp: true) }
}
#expect(rules.isWon(state: snapshot))
}
@Test("Not won with incomplete foundations")
func notWon() {
let snapshot = emptySnapshot()
#expect(!rules.isWon(state: snapshot))
}
// MARK: - Helpers
private func emptySnapshot() -> GameSnapshot {
GameSnapshot(
tableaus: Array(repeating: [], count: 7),
foundations: [[], [], [], []],
stock: [],
waste: [],
freeCells: [],
moves: 0,
score: 0
)
}
}