This refactor fixes the achievement system by using stable canonical string IDs (e.g., "stadium_mlb_fenway_park") instead of random UUIDs. This ensures stadium mappings for achievements are consistent across app launches and CloudKit sync operations. Changes: - Stadium, Team, Game: id property changed from UUID to String - Trip, TripStop, TripPreferences: updated to use String IDs for games/stadiums - CKModels: removed UUID parsing, use canonical IDs directly - AchievementEngine: now matches against canonical stadium IDs - All test files updated to use String IDs instead of UUID() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
306 lines
10 KiB
Swift
306 lines
10 KiB
Swift
//
|
|
// BruteForceRouteVerifier.swift
|
|
// SportsTimeTests
|
|
//
|
|
// Exhaustively enumerates all route permutations to verify optimality.
|
|
// Used for inputs with ≤8 stops where brute force is feasible.
|
|
//
|
|
|
|
import Foundation
|
|
import CoreLocation
|
|
@testable import SportsTime
|
|
|
|
// MARK: - Route Verifier
|
|
|
|
struct BruteForceRouteVerifier {
|
|
|
|
// MARK: - Route Comparison Result
|
|
|
|
struct VerificationResult {
|
|
let isOptimal: Bool
|
|
let proposedRouteDistance: Double
|
|
let optimalRouteDistance: Double
|
|
let optimalRoute: [String]?
|
|
let improvement: Double? // Percentage improvement if not optimal
|
|
let permutationsChecked: Int
|
|
|
|
var improvementPercentage: Double? {
|
|
guard let improvement = improvement else { return nil }
|
|
return improvement * 100
|
|
}
|
|
}
|
|
|
|
// MARK: - Verification
|
|
|
|
/// Verify that a proposed route is optimal (or near-optimal) by checking all permutations
|
|
/// - Parameters:
|
|
/// - proposedRoute: The route to verify (ordered list of stop IDs)
|
|
/// - stops: Dictionary mapping stop IDs to their coordinates
|
|
/// - tolerance: Percentage tolerance for "near-optimal" (default 0 = must be exactly optimal)
|
|
/// - Returns: Verification result
|
|
static func verify(
|
|
proposedRoute: [String],
|
|
stops: [String: CLLocationCoordinate2D],
|
|
tolerance: Double = 0
|
|
) -> VerificationResult {
|
|
guard proposedRoute.count <= TestConstants.bruteForceMaxStops else {
|
|
fatalError("BruteForceRouteVerifier should only be used for ≤\(TestConstants.bruteForceMaxStops) stops")
|
|
}
|
|
|
|
guard proposedRoute.count >= 2 else {
|
|
// Single stop or empty - trivially optimal
|
|
return VerificationResult(
|
|
isOptimal: true,
|
|
proposedRouteDistance: 0,
|
|
optimalRouteDistance: 0,
|
|
optimalRoute: proposedRoute,
|
|
improvement: nil,
|
|
permutationsChecked: 1
|
|
)
|
|
}
|
|
|
|
let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops)
|
|
|
|
// Find optimal route by checking all permutations
|
|
let allPermutations = permutations(of: proposedRoute)
|
|
var optimalDistance = Double.infinity
|
|
var optimalRoute: [String] = []
|
|
|
|
for permutation in allPermutations {
|
|
let distance = calculateRouteDistance(permutation, stops: stops)
|
|
if distance < optimalDistance {
|
|
optimalDistance = distance
|
|
optimalRoute = permutation
|
|
}
|
|
}
|
|
|
|
let isOptimal: Bool
|
|
var improvement: Double? = nil
|
|
|
|
if tolerance == 0 {
|
|
// Exact optimality check with floating point tolerance
|
|
isOptimal = abs(proposedDistance - optimalDistance) < 0.001
|
|
} else {
|
|
// Within tolerance
|
|
let maxAllowed = optimalDistance * (1 + tolerance)
|
|
isOptimal = proposedDistance <= maxAllowed
|
|
}
|
|
|
|
if !isOptimal && optimalDistance > 0 {
|
|
improvement = (proposedDistance - optimalDistance) / optimalDistance
|
|
}
|
|
|
|
return VerificationResult(
|
|
isOptimal: isOptimal,
|
|
proposedRouteDistance: proposedDistance,
|
|
optimalRouteDistance: optimalDistance,
|
|
optimalRoute: optimalRoute,
|
|
improvement: improvement,
|
|
permutationsChecked: allPermutations.count
|
|
)
|
|
}
|
|
|
|
/// Verify a route is optimal with a fixed start and end point
|
|
static func verifyWithFixedEndpoints(
|
|
proposedRoute: [String],
|
|
stops: [String: CLLocationCoordinate2D],
|
|
startId: String,
|
|
endId: String,
|
|
tolerance: Double = 0
|
|
) -> VerificationResult {
|
|
guard proposedRoute.first == startId && proposedRoute.last == endId else {
|
|
// Invalid route - doesn't match required endpoints
|
|
return VerificationResult(
|
|
isOptimal: false,
|
|
proposedRouteDistance: Double.infinity,
|
|
optimalRouteDistance: 0,
|
|
optimalRoute: nil,
|
|
improvement: nil,
|
|
permutationsChecked: 0
|
|
)
|
|
}
|
|
|
|
// Get intermediate stops (excluding start and end)
|
|
let intermediateStops = proposedRoute.dropFirst().dropLast()
|
|
|
|
guard intermediateStops.count <= TestConstants.bruteForceMaxStops - 2 else {
|
|
fatalError("BruteForceRouteVerifier: too many intermediate stops")
|
|
}
|
|
|
|
let proposedDistance = calculateRouteDistance(proposedRoute, stops: stops)
|
|
|
|
// Generate all permutations of intermediate stops
|
|
let allPermutations = permutations(of: Array(intermediateStops))
|
|
var optimalDistance = Double.infinity
|
|
var optimalRoute: [String] = []
|
|
|
|
for permutation in allPermutations {
|
|
var fullRoute = [startId]
|
|
fullRoute.append(contentsOf: permutation)
|
|
fullRoute.append(endId)
|
|
|
|
let distance = calculateRouteDistance(fullRoute, stops: stops)
|
|
if distance < optimalDistance {
|
|
optimalDistance = distance
|
|
optimalRoute = fullRoute
|
|
}
|
|
}
|
|
|
|
let isOptimal: Bool
|
|
var improvement: Double? = nil
|
|
|
|
if tolerance == 0 {
|
|
isOptimal = abs(proposedDistance - optimalDistance) < 0.001
|
|
} else {
|
|
let maxAllowed = optimalDistance * (1 + tolerance)
|
|
isOptimal = proposedDistance <= maxAllowed
|
|
}
|
|
|
|
if !isOptimal && optimalDistance > 0 {
|
|
improvement = (proposedDistance - optimalDistance) / optimalDistance
|
|
}
|
|
|
|
return VerificationResult(
|
|
isOptimal: isOptimal,
|
|
proposedRouteDistance: proposedDistance,
|
|
optimalRouteDistance: optimalDistance,
|
|
optimalRoute: optimalRoute,
|
|
improvement: improvement,
|
|
permutationsChecked: allPermutations.count
|
|
)
|
|
}
|
|
|
|
/// Check if there's an obviously better route (significantly shorter)
|
|
static func hasObviouslyBetterRoute(
|
|
proposedRoute: [String],
|
|
stops: [String: CLLocationCoordinate2D],
|
|
threshold: Double = 0.1 // 10% improvement threshold
|
|
) -> (hasBetter: Bool, improvement: Double?) {
|
|
let result = verify(proposedRoute: proposedRoute, stops: stops, tolerance: threshold)
|
|
return (!result.isOptimal, result.improvement)
|
|
}
|
|
|
|
// MARK: - Distance Calculation
|
|
|
|
/// Calculate total route distance using haversine formula
|
|
static func calculateRouteDistance(
|
|
_ route: [String],
|
|
stops: [String: CLLocationCoordinate2D]
|
|
) -> Double {
|
|
guard route.count >= 2 else { return 0 }
|
|
|
|
var totalDistance: Double = 0
|
|
|
|
for i in 0..<(route.count - 1) {
|
|
guard let from = stops[route[i]],
|
|
let to = stops[route[i + 1]] else {
|
|
continue
|
|
}
|
|
totalDistance += haversineDistanceMiles(from: from, to: to)
|
|
}
|
|
|
|
return totalDistance
|
|
}
|
|
|
|
/// Haversine distance between two coordinates in miles
|
|
static func haversineDistanceMiles(
|
|
from: CLLocationCoordinate2D,
|
|
to: CLLocationCoordinate2D
|
|
) -> Double {
|
|
let earthRadiusMiles = TestConstants.earthRadiusMiles
|
|
|
|
let lat1 = from.latitude * .pi / 180
|
|
let lat2 = to.latitude * .pi / 180
|
|
let deltaLat = (to.latitude - from.latitude) * .pi / 180
|
|
let deltaLon = (to.longitude - from.longitude) * .pi / 180
|
|
|
|
let a = sin(deltaLat / 2) * sin(deltaLat / 2) +
|
|
cos(lat1) * cos(lat2) *
|
|
sin(deltaLon / 2) * sin(deltaLon / 2)
|
|
let c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
|
|
|
return earthRadiusMiles * c
|
|
}
|
|
|
|
// MARK: - Permutation Generation
|
|
|
|
/// Generate all permutations of an array (Heap's algorithm)
|
|
static func permutations<T>(of array: [T]) -> [[T]] {
|
|
var result: [[T]] = []
|
|
var arr = array
|
|
|
|
func generate(_ n: Int) {
|
|
if n == 1 {
|
|
result.append(arr)
|
|
return
|
|
}
|
|
|
|
for i in 0..<n {
|
|
generate(n - 1)
|
|
if n % 2 == 0 {
|
|
arr.swapAt(i, n - 1)
|
|
} else {
|
|
arr.swapAt(0, n - 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
generate(array.count)
|
|
return result
|
|
}
|
|
|
|
// MARK: - Factorial
|
|
|
|
/// Calculate factorial (for estimating permutation count)
|
|
static func factorial(_ n: Int) -> Int {
|
|
guard n > 1 else { return 1 }
|
|
return (1...n).reduce(1, *)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience Extensions
|
|
|
|
extension BruteForceRouteVerifier {
|
|
/// Verify a trip's route is optimal
|
|
static func verifyTrip(_ trip: Trip) -> VerificationResult {
|
|
var stops: [String: CLLocationCoordinate2D] = [:]
|
|
|
|
for stop in trip.stops {
|
|
if let coord = stop.coordinate {
|
|
stops[stop.id.uuidString] = coord
|
|
}
|
|
}
|
|
|
|
let routeIds = trip.stops.map { $0.id.uuidString }
|
|
return verify(proposedRoute: routeIds, stops: stops)
|
|
}
|
|
|
|
/// Verify a list of stadiums forms an optimal route
|
|
static func verifyStadiumRoute(_ stadiums: [Stadium]) -> VerificationResult {
|
|
let stops = Dictionary(uniqueKeysWithValues: stadiums.map { ($0.id, $0.coordinate) })
|
|
let routeIds = stadiums.map { $0.id }
|
|
return verify(proposedRoute: routeIds, stops: stops)
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Assertions
|
|
|
|
extension BruteForceRouteVerifier.VerificationResult {
|
|
/// Returns a detailed failure message if not optimal
|
|
var failureMessage: String? {
|
|
guard !isOptimal else { return nil }
|
|
|
|
var message = "Route is not optimal. "
|
|
message += "Proposed: \(String(format: "%.1f", proposedRouteDistance)) miles, "
|
|
message += "Optimal: \(String(format: "%.1f", optimalRouteDistance)) miles"
|
|
|
|
if let improvement = improvementPercentage {
|
|
message += " (\(String(format: "%.1f", improvement))% longer)"
|
|
}
|
|
|
|
message += ". Checked \(permutationsChecked) permutations."
|
|
|
|
return message
|
|
}
|
|
}
|