Initial commit: SportsTime trip planning app

- Three-scenario planning engine (A: date range, B: selected games, C: directional routes)
- GeographicRouteExplorer with anchor game support for route exploration
- Shared ItineraryBuilder for travel segment calculation
- TravelEstimator for driving time/distance estimation
- SwiftUI views for trip creation and detail display
- CloudKit integration for schedule data
- Python scraping scripts for sports schedules

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-07 00:46:40 -06:00
commit 9088b46563
84 changed files with 180371 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
//
// TripScorer.swift
// SportsTime
//
import Foundation
struct TripScorer {
// MARK: - Score Trip
func score(trip: Trip, request: PlanningRequest) -> Trip {
let gameQuality = calculateGameQualityScore(trip: trip, request: request)
let routeEfficiency = calculateRouteEfficiencyScore(trip: trip, request: request)
let leisureBalance = calculateLeisureBalanceScore(trip: trip, request: request)
let preferenceAlignment = calculatePreferenceAlignmentScore(trip: trip, request: request)
// Weighted average
let weights = (game: 0.35, route: 0.25, leisure: 0.20, preference: 0.20)
let overall = (
gameQuality * weights.game +
routeEfficiency * weights.route +
leisureBalance * weights.leisure +
preferenceAlignment * weights.preference
)
let score = TripScore(
overallScore: overall,
gameQualityScore: gameQuality,
routeEfficiencyScore: routeEfficiency,
leisureBalanceScore: leisureBalance,
preferenceAlignmentScore: preferenceAlignment
)
var scoredTrip = trip
scoredTrip.score = score
return scoredTrip
}
// MARK: - Game Quality Score
private func calculateGameQualityScore(trip: Trip, request: PlanningRequest) -> Double {
var score: Double = 0
let totalPossibleGames = Double(max(1, request.availableGames.count))
let gamesAttended = Double(trip.totalGames)
// Base score from number of games
let gameRatio = gamesAttended / min(totalPossibleGames, Double(trip.tripDuration))
score += gameRatio * 50
// Bonus for including must-see games
let mustSeeIncluded = trip.stops.flatMap { $0.games }
.filter { request.preferences.mustSeeGameIds.contains($0) }
.count
let mustSeeRatio = Double(mustSeeIncluded) / Double(max(1, request.preferences.mustSeeGameIds.count))
score += mustSeeRatio * 30
// Bonus for sport variety
let sportsAttended = Set(request.availableGames
.filter { trip.stops.flatMap { $0.games }.contains($0.id) }
.map { $0.sport }
)
let varietyBonus = Double(sportsAttended.count) / Double(max(1, request.preferences.sports.count)) * 20
score += varietyBonus
return min(100, score)
}
// MARK: - Route Efficiency Score
private func calculateRouteEfficiencyScore(trip: Trip, request: PlanningRequest) -> Double {
guard let startLocation = request.preferences.startLocation,
let endLocation = request.preferences.endLocation,
let startCoord = startLocation.coordinate,
let endCoord = endLocation.coordinate else {
return 50.0
}
// Calculate direct distance
let directDistance = CLLocation(latitude: startCoord.latitude, longitude: startCoord.longitude)
.distance(from: CLLocation(latitude: endCoord.latitude, longitude: endCoord.longitude))
guard trip.totalDistanceMeters > 0, directDistance > 0 else {
return 50.0
}
// Efficiency ratio (direct / actual)
let efficiency = directDistance / trip.totalDistanceMeters
// Score: 100 for perfect efficiency, lower for longer routes
// Allow up to 3x direct distance before score drops significantly
let normalizedEfficiency = min(1.0, efficiency * 2)
return normalizedEfficiency * 100
}
// MARK: - Leisure Balance Score
private func calculateLeisureBalanceScore(trip: Trip, request: PlanningRequest) -> Double {
let leisureLevel = request.preferences.leisureLevel
var score: Double = 100
// Check average driving hours
let avgDrivingHours = trip.averageDrivingHoursPerDay
let targetDrivingHours: Double = switch leisureLevel {
case .packed: 8.0
case .moderate: 6.0
case .relaxed: 4.0
}
if avgDrivingHours > targetDrivingHours {
let excess = avgDrivingHours - targetDrivingHours
score -= excess * 10
}
// Check rest day ratio
let restDays = trip.stops.filter { $0.isRestDay }.count
let targetRestRatio = leisureLevel.restDaysPerWeek / 7.0
let actualRestRatio = Double(restDays) / Double(max(1, trip.tripDuration))
let restDifference = abs(actualRestRatio - targetRestRatio)
score -= restDifference * 50
// Check games per day vs target
let gamesPerDay = Double(trip.totalGames) / Double(max(1, trip.tripDuration))
let targetGamesPerDay = Double(leisureLevel.maxGamesPerWeek) / 7.0
if gamesPerDay > targetGamesPerDay {
let excess = gamesPerDay - targetGamesPerDay
score -= excess * 20
}
return max(0, min(100, score))
}
// MARK: - Preference Alignment Score
private func calculatePreferenceAlignmentScore(trip: Trip, request: PlanningRequest) -> Double {
var score: Double = 100
// Check if must-stop locations are visited
let visitedCities = Set(trip.stops.map { $0.city.lowercased() })
for location in request.preferences.mustStopLocations {
if !visitedCities.contains(location.name.lowercased()) {
score -= 15
}
}
// Bonus for preferred cities
for city in request.preferences.preferredCities {
if visitedCities.contains(city.lowercased()) {
score += 5
}
}
// Check EV charging if needed
if request.preferences.needsEVCharging {
let hasEVStops = trip.travelSegments.contains { !$0.evChargingStops.isEmpty }
if !hasEVStops && trip.travelSegments.contains(where: { $0.distanceMiles > 200 }) {
score -= 20 // Long drive without EV stops
}
}
// Check lodging type alignment
let lodgingMatches = trip.stops.filter { stop in
stop.lodging?.type == request.preferences.lodgingType
}.count
let lodgingRatio = Double(lodgingMatches) / Double(max(1, trip.stops.count))
score = score * (0.5 + lodgingRatio * 0.5)
// Check if within stop limit
if let maxStops = request.preferences.numberOfStops {
if trip.stops.count > maxStops {
score -= Double(trip.stops.count - maxStops) * 10
}
}
return max(0, min(100, score))
}
}
// MARK: - CoreML Integration (Placeholder)
extension TripScorer {
/// Score using CoreML model if available
func scoreWithML(trip: Trip, request: PlanningRequest) -> Trip {
// In production, this would use a CoreML model for personalized scoring
// For now, fall back to rule-based scoring
return score(trip: trip, request: request)
}
}
import CoreLocation