XCode-Claude-Workflow/SoliCardsTests/GameEngine/FreeCellRulesTests.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

88 lines
2.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Testing
@testable import SoliCards
@Suite("FreeCell Rules Tests")
struct FreeCellRulesTests {
let rules = FreeCellRules()
@Test("Deal distributes all 52 cards face-up")
func dealLayout() {
let deck = Deck.standard()
let snapshot = rules.deal(deck: deck)
#expect(snapshot.tableaus.count == 8)
// First 4 tableaus: 7 cards, last 4: 6 cards
for i in 0..<4 {
#expect(snapshot.tableaus[i].count == 7)
}
for i in 4..<8 {
#expect(snapshot.tableaus[i].count == 6)
}
// All face-up
let allCards = snapshot.tableaus.flatMap { $0 }
#expect(allCards.count == 52)
#expect(allCards.allSatisfy { $0.isFaceUp })
// 4 empty free cells
#expect(snapshot.freeCells.count == 4)
#expect(snapshot.freeCells.allSatisfy { $0 == nil })
// No stock
#expect(snapshot.stock.isEmpty)
}
@Test("Power move calculation")
func powerMoves() {
// (1 + empty_freecells) × 2^empty_tableaus
var snapshot = emptySnapshot()
// All 4 free cells empty, target is empty (so 7 empty tableaus count)
let max1 = rules.calculateMaxMovableCards(state: snapshot, targetEmpty: true)
// (1 + 4) × 2^7 = 5 × 128 = 640
#expect(max1 == 640)
// 2 free cells occupied, no empty tableaus
snapshot.freeCells = [Card(suit: .hearts, rank: .ace, isFaceUp: true),
Card(suit: .spades, rank: .two, isFaceUp: true),
nil, nil]
for i in 0..<8 {
snapshot.tableaus[i] = [Card(suit: .clubs, rank: .king, isFaceUp: true)]
}
let max2 = rules.calculateMaxMovableCards(state: snapshot, targetEmpty: false)
// (1 + 2) × 2^0 = 3
#expect(max2 == 3)
}
@Test("Can move single card to empty free cell")
func moveToFreeCell() {
let card = Card(suit: .hearts, rank: .five, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [card]
#expect(rules.canMove(cards: [card], from: .tableau(0), to: .freeCell(0), state: snapshot))
}
@Test("Cannot move multiple cards to free cell")
func multipleToFreeCell() {
let card1 = Card(suit: .spades, rank: .six, isFaceUp: true)
let card2 = Card(suit: .hearts, rank: .five, isFaceUp: true)
var snapshot = emptySnapshot()
snapshot.tableaus[0] = [card1, card2]
#expect(!rules.canMove(cards: [card1, card2], from: .tableau(0), to: .freeCell(0), state: snapshot))
}
// MARK: - Helpers
private func emptySnapshot() -> GameSnapshot {
GameSnapshot(
tableaus: Array(repeating: [], count: 8),
foundations: [[], [], [], []],
stock: [],
waste: [],
freeCells: [nil, nil, nil, nil],
moves: 0,
score: 0
)
}
}