feat: dynamically calculate train cars based on max concurrent passengers
Instead of hard-coding 5 train cars, now calculate the number of cars needed based on the maximum number of passengers that will be on the train at any given moment during the route. For example: if 8 total passengers exist, but only 4 board at the start, all get off halfway, then the remaining 4 board and ride to the end, we only need 4 cars (not 8). Added calculateMaxConcurrentPassengers() utility that: - Tracks boarding/delivery events by station position - Sorts events to handle same-station boarding/delivery correctly - Returns the peak concurrent passenger count Updated SteamTrainJourney to calculate maxCars dynamically using this utility. Updated all hooks and tests to use required maxCars/carSpacing parameters. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { useTrackManagement } from '../../hooks/useTrackManagement'
|
||||
import { useComplementRace } from '../../context/ComplementRaceContext'
|
||||
import { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
|
||||
import { getRouteTheme } from '../../lib/routeThemes'
|
||||
import { calculateMaxConcurrentPassengers } from '../../lib/passengerGenerator'
|
||||
import { useGameMode } from '@/contexts/GameModeContext'
|
||||
import { useUserProfile } from '@/contexts/UserProfileContext'
|
||||
import { TrainTerrainBackground } from './TrainTerrainBackground'
|
||||
@@ -98,11 +99,22 @@ export function SteamTrainJourney({ momentum, trainPosition, pressure, elapsedTi
|
||||
const pathRef = useRef<SVGPathElement>(null)
|
||||
const [trackGenerator] = useState(() => new RailroadTrackGenerator(800, 600))
|
||||
|
||||
// Train transforms (extracted to hook) - called first to get maxCars and carSpacing
|
||||
const { trainTransform, trainCars, locomotiveOpacity, maxCars, carSpacing } = useTrainTransforms({
|
||||
// Calculate the number of train cars dynamically based on max concurrent passengers
|
||||
const maxCars = useMemo(() => {
|
||||
const maxPassengers = calculateMaxConcurrentPassengers(state.passengers, state.stations)
|
||||
// Ensure at least 1 car, even if no passengers
|
||||
return Math.max(1, maxPassengers)
|
||||
}, [state.passengers, state.stations])
|
||||
|
||||
const carSpacing = 7 // Distance between cars (in % of track)
|
||||
|
||||
// Train transforms (extracted to hook)
|
||||
const { trainTransform, trainCars, locomotiveOpacity } = useTrainTransforms({
|
||||
trainPosition,
|
||||
trackGenerator,
|
||||
pathRef
|
||||
pathRef,
|
||||
maxCars,
|
||||
carSpacing
|
||||
})
|
||||
|
||||
// Track management (extracted to hook)
|
||||
|
||||
@@ -31,13 +31,14 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: nullPathRef
|
||||
pathRef: nullPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.current.trainTransform).toEqual({ x: 50, y: 300, rotation: 0 })
|
||||
expect(result.current.maxCars).toBe(5)
|
||||
expect(result.current.carSpacing).toBe(7)
|
||||
expect(result.current.trainCars).toHaveLength(5)
|
||||
})
|
||||
|
||||
test('calculates train transform at given position', () => {
|
||||
@@ -45,7 +46,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
@@ -62,7 +65,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: position,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
}),
|
||||
{ initialProps: { position: 20 } }
|
||||
)
|
||||
@@ -73,17 +78,18 @@ describe('useTrainTransforms', () => {
|
||||
expect(result.current.trainTransform.x).toBe(600)
|
||||
})
|
||||
|
||||
test('calculates correct number of train cars with default maxCars', () => {
|
||||
test('calculates correct number of train cars', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useTrainTransforms({
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.current.trainCars).toHaveLength(5)
|
||||
expect(result.current.maxCars).toBe(5)
|
||||
})
|
||||
|
||||
test('respects custom maxCars parameter', () => {
|
||||
@@ -92,12 +98,12 @@ describe('useTrainTransforms', () => {
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 3
|
||||
maxCars: 3,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.current.trainCars).toHaveLength(3)
|
||||
expect(result.current.maxCars).toBe(3)
|
||||
})
|
||||
|
||||
test('respects custom carSpacing parameter', () => {
|
||||
@@ -106,11 +112,11 @@ describe('useTrainTransforms', () => {
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 10
|
||||
})
|
||||
)
|
||||
|
||||
expect(result.current.carSpacing).toBe(10)
|
||||
// First car should be at position 50 - 10 = 40
|
||||
expect(result.current.trainCars[0].position).toBe(40)
|
||||
})
|
||||
@@ -137,7 +143,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 3,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result1.current.locomotiveOpacity).toBe(0)
|
||||
@@ -146,7 +154,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 5.5, // Midpoint between 3 and 8
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result2.current.locomotiveOpacity).toBe(0.5)
|
||||
@@ -155,7 +165,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 8,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result3.current.locomotiveOpacity).toBe(1)
|
||||
@@ -167,7 +179,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 92,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result1.current.locomotiveOpacity).toBe(1)
|
||||
@@ -176,7 +190,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 94.5, // Midpoint between 92 and 97
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result2.current.locomotiveOpacity).toBe(0.5)
|
||||
@@ -185,7 +201,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 97,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
expect(result3.current.locomotiveOpacity).toBe(0)
|
||||
@@ -196,7 +214,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
@@ -261,7 +281,9 @@ describe('useTrainTransforms', () => {
|
||||
useTrainTransforms({
|
||||
trainPosition: 50,
|
||||
trackGenerator: mockTrackGenerator,
|
||||
pathRef: mockPathRef
|
||||
pathRef: mockPathRef,
|
||||
maxCars: 5,
|
||||
carSpacing: 7
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ interface UseTrackManagementParams {
|
||||
pathRef: React.RefObject<SVGPathElement>
|
||||
stations: Station[]
|
||||
passengers: Passenger[]
|
||||
maxCars?: number
|
||||
carSpacing?: number
|
||||
maxCars: number
|
||||
carSpacing: number
|
||||
}
|
||||
|
||||
export function useTrackManagement({
|
||||
@@ -21,8 +21,8 @@ export function useTrackManagement({
|
||||
pathRef,
|
||||
stations,
|
||||
passengers,
|
||||
maxCars = 5,
|
||||
carSpacing = 7
|
||||
maxCars,
|
||||
carSpacing
|
||||
}: UseTrackManagementParams) {
|
||||
const [trackData, setTrackData] = useState<ReturnType<typeof trackGenerator.generateTrack> | null>(null)
|
||||
const [tiesAndRails, setTiesAndRails] = useState<{
|
||||
|
||||
@@ -16,16 +16,16 @@ interface UseTrainTransformsParams {
|
||||
trainPosition: number
|
||||
trackGenerator: RailroadTrackGenerator
|
||||
pathRef: React.RefObject<SVGPathElement>
|
||||
maxCars?: number
|
||||
carSpacing?: number
|
||||
maxCars: number
|
||||
carSpacing: number
|
||||
}
|
||||
|
||||
export function useTrainTransforms({
|
||||
trainPosition,
|
||||
trackGenerator,
|
||||
pathRef,
|
||||
maxCars = 5,
|
||||
carSpacing = 7
|
||||
maxCars,
|
||||
carSpacing
|
||||
}: UseTrainTransformsParams) {
|
||||
const [trainTransform, setTrainTransform] = useState<TrainTransform>({ x: 50, y: 300, rotation: 0 })
|
||||
|
||||
@@ -102,8 +102,6 @@ export function useTrainTransforms({
|
||||
return {
|
||||
trainTransform,
|
||||
trainCars,
|
||||
locomotiveOpacity,
|
||||
maxCars,
|
||||
carSpacing
|
||||
locomotiveOpacity
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,4 +180,53 @@ export function findDeliverablePassengers(
|
||||
}
|
||||
|
||||
return deliverable
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the maximum number of passengers that will be on the train
|
||||
* concurrently at any given moment during the route
|
||||
*/
|
||||
export function calculateMaxConcurrentPassengers(
|
||||
passengers: Passenger[],
|
||||
stations: Station[]
|
||||
): number {
|
||||
// Create events for boarding and delivery
|
||||
interface StationEvent {
|
||||
position: number
|
||||
isBoarding: boolean // true = board, false = delivery
|
||||
}
|
||||
|
||||
const events: StationEvent[] = []
|
||||
|
||||
for (const passenger of passengers) {
|
||||
const originStation = stations.find(s => s.id === passenger.originStationId)
|
||||
const destStation = stations.find(s => s.id === passenger.destinationStationId)
|
||||
|
||||
if (originStation && destStation) {
|
||||
events.push({ position: originStation.position, isBoarding: true })
|
||||
events.push({ position: destStation.position, isBoarding: false })
|
||||
}
|
||||
}
|
||||
|
||||
// Sort events by position, with deliveries before boardings at the same position
|
||||
events.sort((a, b) => {
|
||||
if (a.position !== b.position) return a.position - b.position
|
||||
// At same position, deliveries happen before boarding
|
||||
return a.isBoarding ? 1 : -1
|
||||
})
|
||||
|
||||
// Track current passenger count and maximum
|
||||
let currentCount = 0
|
||||
let maxCount = 0
|
||||
|
||||
for (const event of events) {
|
||||
if (event.isBoarding) {
|
||||
currentCount++
|
||||
maxCount = Math.max(maxCount, currentCount)
|
||||
} else {
|
||||
currentCount--
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount
|
||||
}
|
||||
Reference in New Issue
Block a user