SoliCards-iOS-iPadOS-MacOS/SoliCardsTests/GameEngine/FreeCellRulesTests.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

88 lines
2.9 KiB
Swift
Raw Permalink 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
)
}
}