feat: improve planning engine travel handling, itinerary reordering, and scenario planners
Add TravelInfo initializers and city normalization helpers to fix repeat city-pair disambiguation. Improve drag-and-drop reordering with segment index tracking and source-row-aware zone calculation. Enhance all five scenario planners with better next-day departure handling and travel segment placement. Add comprehensive tests across all planners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,9 +71,6 @@ enum GameDAGRouter {
|
||||
/// Buffer time after game ends before we can depart (hours)
|
||||
private static let gameEndBufferHours: Double = 3.0
|
||||
|
||||
/// Maximum days ahead to consider for next game (1 = next day only, 5 = allows multi-day drives)
|
||||
private static let maxDayLookahead = 5
|
||||
|
||||
// MARK: - Route Profile
|
||||
|
||||
/// Captures the key metrics of a route for diversity analysis
|
||||
@@ -176,14 +173,29 @@ enum GameDAGRouter {
|
||||
// Step 2.5: Calculate effective beam width for this dataset size
|
||||
let scaledBeamWidth = effectiveBeamWidth(gameCount: games.count, requestedWidth: beamWidth)
|
||||
|
||||
// Step 3: Initialize beam with first few days' games as starting points
|
||||
var beam: [[Game]] = []
|
||||
for dayIndex in sortedDays.prefix(maxDayLookahead) {
|
||||
if let dayGames = buckets[dayIndex] {
|
||||
for game in dayGames {
|
||||
beam.append([game])
|
||||
}
|
||||
}
|
||||
// Step 3: Initialize beam from all games so later-starting valid routes
|
||||
// (including anchor-driven routes) are not dropped up front.
|
||||
let initialBeam = sortedGames.map { [$0] }
|
||||
let beamSeedLimit = max(scaledBeamWidth * 2, 50)
|
||||
|
||||
let anchorSeeds = initialBeam.filter { path in
|
||||
guard let game = path.first else { return false }
|
||||
return anchorGameIds.contains(game.id)
|
||||
}
|
||||
let nonAnchorSeeds = initialBeam.filter { path in
|
||||
guard let game = path.first else { return false }
|
||||
return !anchorGameIds.contains(game.id)
|
||||
}
|
||||
|
||||
let reservedForAnchors = min(anchorSeeds.count, beamSeedLimit)
|
||||
let remainingSlots = max(0, beamSeedLimit - reservedForAnchors)
|
||||
let prunedNonAnchorSeeds = remainingSlots > 0
|
||||
? diversityPrune(nonAnchorSeeds, stadiums: stadiums, targetCount: remainingSlots)
|
||||
: []
|
||||
|
||||
var beam = Array(anchorSeeds.prefix(reservedForAnchors)) + prunedNonAnchorSeeds
|
||||
if beam.isEmpty {
|
||||
beam = diversityPrune(initialBeam, stadiums: stadiums, targetCount: beamSeedLimit)
|
||||
}
|
||||
|
||||
// Step 4: Expand beam day by day with early termination
|
||||
@@ -200,13 +212,6 @@ enum GameDAGRouter {
|
||||
|
||||
for path in beam {
|
||||
guard let lastGame = path.last else { continue }
|
||||
let lastGameDay = dayIndexFor(lastGame.startTime, referenceDate: sortedGames[0].startTime)
|
||||
|
||||
// Skip if this day is too far ahead for this route
|
||||
if dayIndex > lastGameDay + maxDayLookahead {
|
||||
nextBeam.append(path)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try adding each of today's games
|
||||
for candidate in todaysGames {
|
||||
|
||||
Reference in New Issue
Block a user