feat: restore steam train journey enhancements

- Restore dynamically generated curved train tracks with proper railroad construction (ties and dual rails)
- Restore floating math display speech bubble that follows the train along the track
- Restore enhanced pressure system with dramatic coal spilling effects for wrong answers (momentum drops to zero)
- Enhanced coal shoveling animations with particle effects for correct answers
- Add CSS animations for coal spilling, pressure alarm, and floating display

🚂 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-12 21:54:18 -05:00
parent 84334f9d5a
commit 045dc9fb32

View File

@@ -4592,6 +4592,97 @@ def generate_web_flashcards(numbers, config, output_path):
margin-bottom: 30px;
}}
/* Floating Speech Bubble for Train Mode */
.floating-math-display {{
position: absolute;
background: rgba(255, 255, 255, 0.95);
border: 3px solid #4a90e2;
border-radius: 20px;
padding: 15px 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
z-index: 100;
display: none;
min-width: 200px;
text-align: center;
color: #333;
font-weight: 600;
transform: translate(-50%, -100%);
pointer-events: none;
animation: floatGently 3s ease-in-out infinite;
}}
.floating-math-display::after {{
content: '';
position: absolute;
bottom: -15px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-top: 15px solid #4a90e2;
}}
.floating-math-display::before {{
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 12px solid transparent;
border-right: 12px solid transparent;
border-top: 12px solid rgba(255, 255, 255, 0.95);
}}
@keyframes floatGently {{
0%, 100% {{ transform: translate(-50%, -100%) translateY(0px); }}
50% {{ transform: translate(-50%, -100%) translateY(-5px); }}
}}
/* Coal Spilling Animation */
@keyframes coalSpill {{
0% {{
transform: translateY(0px) rotate(0deg);
opacity: 1;
}}
50% {{
transform: translateY(40px) translateX(20px) rotate(180deg);
opacity: 0.8;
}}
100% {{
transform: translateY(80px) translateX(40px) rotate(360deg);
opacity: 0;
}}
}}
.coal-chunk {{
color: #2c2c2c;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}}
/* Pressure Alarm Animation */
@keyframes pressureAlarm {{
0%, 100% {{
filter: brightness(1) saturate(1);
transform: scale(1);
}}
25% {{
filter: brightness(1.5) saturate(2) hue-rotate(0deg);
transform: scale(1.05);
}}
50% {{
filter: brightness(2) saturate(3) hue-rotate(15deg);
transform: scale(1.1);
}}
75% {{
filter: brightness(1.5) saturate(2) hue-rotate(-15deg);
transform: scale(1.05);
}}
}}
.equation-visual {{
display: flex;
@@ -5303,6 +5394,81 @@ def generate_web_flashcards(numbers, config, output_path):
z-index: 6;
}}
/* Steam Pressure Instrument Panel */
.instrument-panel {{
position: absolute;
bottom: 20px;
left: 20px;
z-index: 10;
}}
.panel-frame {{
background: linear-gradient(135deg, #667eea, #764ba2);
border: 3px solid #5a67d8;
border-radius: 20px;
padding: 18px;
box-shadow:
0 10px 30px rgba(102, 126, 234, 0.3),
inset 0 2px 8px rgba(255, 255, 255, 0.2);
position: relative;
}}
.panel-background {{
position: absolute;
top: 6px;
left: 6px;
right: 6px;
bottom: 6px;
background: linear-gradient(45deg, #667eea, #764ba2);
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.1);
}}
.pressure-gauge-container {{
position: relative;
z-index: 2;
text-align: center;
}}
.pressure-gauge {{
display: block;
margin: 0 auto 10px auto;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}}
.gauge-background {{
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.4));
}}
.gauge-progress {{
transition: stroke-dashoffset 0.5s ease-out;
filter: drop-shadow(0 0 8px rgba(231, 76, 60, 0.6));
}}
.pressure-needle {{
transition: transform 0.5s ease-out;
transform-origin: 100px 100px;
}}
.gauge-title {{
color: #ffffff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}}
.gauge-value {{
color: #ffd700;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 0.9rem;
font-weight: 800;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}}
/* Momentum gauge */
.momentum-display {{
position: absolute;
@@ -7451,10 +7617,7 @@ def generate_web_flashcards(numbers, config, output_path):
<!-- SVG Route Path -->
<div class="route-path">
<svg viewBox="0 0 800 600" width="800" height="600">
<!-- Background route -->
<path class="train-route-bg" d="M 50 300 Q 200 200 300 250 Q 400 300 500 200 Q 600 100 700 150 Q 750 200 750 300" />
<!-- Foreground route -->
<path class="train-route" d="M 50 300 Q 200 200 300 250 Q 400 300 500 200 Q 600 100 700 150 Q 750 200 750 300" />
<!-- Dynamic track elements will be generated here -->
</svg>
<!-- Train locomotive -->
@@ -7464,6 +7627,11 @@ def generate_web_flashcards(numbers, config, output_path):
<div class="steam-effect" style="animation-delay: 0.6s;"></div>
</div>
<!-- Floating Math Display Speech Bubble -->
<div class="floating-math-display" id="floating-math-display">
<div class="floating-challenge-text" id="floating-challenge-text">Find the complement of 7</div>
</div>
<!-- Coal shoveler -->
<div class="coal-shoveler" id="coal-worker">👷</div>
@@ -7483,12 +7651,58 @@ def generate_web_flashcards(numbers, config, output_path):
<div class="landmark tree" style="left: 680px; top: 170px;">🌳</div>
</div>
<!-- Momentum gauge -->
<div class="momentum-display">
<div class="momentum-gauge">
<div class="momentum-bar" id="momentum-bar"></div>
<!-- Steam Pressure Instrument Panel -->
<div class="instrument-panel">
<div class="panel-frame">
<div class="panel-background"></div>
<div class="pressure-gauge-container">
<svg class="pressure-gauge" viewBox="0 0 200 120" width="160" height="96">
<!-- Gauge background arc -->
<path class="gauge-background"
d="M 20 100 A 80 80 0 0 1 180 100"
fill="none"
stroke="rgba(255, 255, 255, 0.3)"
stroke-width="12"/>
<!-- Gauge progress arc -->
<path class="gauge-progress"
id="pressure-arc"
d="M 20 100 A 80 80 0 0 1 180 100"
fill="none"
stroke="#ff6b6b"
stroke-width="10"
stroke-dasharray="251.2"
stroke-dashoffset="251.2"/>
<!-- Gauge tick marks -->
<g class="gauge-ticks">
<line x1="100" y1="20" x2="100" y2="30" stroke="rgba(255,255,255,0.6)" stroke-width="2"/>
<line x1="150" y1="35" x2="145" y2="43" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
<line x1="180" y1="100" x2="170" y2="100" stroke="rgba(255,255,255,0.6)" stroke-width="2"/>
<line x1="150" y1="165" x2="145" y2="157" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
<line x1="50" y1="35" x2="55" y2="43" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
<line x1="20" y1="100" x2="30" y2="100" stroke="rgba(255,255,255,0.6)" stroke-width="2"/>
<line x1="50" y1="165" x2="55" y2="157" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
</g>
<!-- Pressure indicator needle (narrow isosceles triangle) -->
<g class="pressure-needle" id="pressure-needle" transform="rotate(0 100 100)">
<polygon points="100,95 98,100 102,100"
fill="#ffd700"
stroke="#ffed4e"
stroke-width="1"/>
<circle cx="100" cy="100" r="6" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.6)" stroke-width="2"/>
</g>
<!-- Gauge labels -->
<text x="30" y="105" fill="rgba(255,255,255,0.7)" font-family="sans-serif" font-size="10" font-weight="600">0</text>
<text x="95" y="25" fill="rgba(255,255,255,0.7)" font-family="sans-serif" font-size="10" font-weight="600">50</text>
<text x="175" y="105" fill="rgba(255,255,255,0.7)" font-family="sans-serif" font-size="10" font-weight="600">100</text>
</svg>
<div class="gauge-title">🚂 Steam Power</div>
<div class="gauge-value" id="pressure-value">0 PSI</div>
</div>
</div>
<div class="momentum-label" id="momentum-label">Momentum: 0%</div>
</div>
<!-- Passenger car -->
@@ -10979,6 +11193,9 @@ def generate_web_flashcards(numbers, config, output_path):
document.getElementById('input-feedback').textContent = '';
document.getElementById('visual-complement').style.display = 'none';
// Handle display switching for train mode (static → floating after delay)
this.handleDisplaySwitching();
// Show persistent learning mode feedback
if (this.difficultyTracker.learningMode && this.totalQuestions % 3 === 0) {{
const pairKey = `${{this.currentNumber}}_${{this.correctAnswer}}_${{this.targetSum}}`;
@@ -12048,10 +12265,218 @@ def generate_web_flashcards(numbers, config, output_path):
this.lapCelebrationCooldown.clear();
}}
// Generate dynamic track for train variant
if (this.style === 'sprint') {{
this.generateDynamicTrack();
}}
// Start AI movement
this.startAIRacers();
}}
generateDynamicTrack() {{
// Generate a dynamic curved railroad track with interesting topology
const svg = document.querySelector('.route-path svg');
if (!svg) return;
// Clear existing track elements
svg.innerHTML = '';
// Generate track waypoints with some randomness for variety
const waypoints = this.generateTrackWaypoints();
// Generate smooth curved path through waypoints
const pathData = this.generateSmoothPath(waypoints);
// Create main reference path for train positioning
const referencePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
referencePath.setAttribute('class', 'train-route');
referencePath.setAttribute('d', pathData);
referencePath.setAttribute('fill', 'none');
referencePath.setAttribute('stroke', 'transparent');
referencePath.setAttribute('stroke-width', '2');
// Create railroad bed/ballast
const ballast = document.createElementNS('http://www.w3.org/2000/svg', 'path');
ballast.setAttribute('class', 'railroad-ballast');
ballast.setAttribute('d', pathData);
ballast.setAttribute('fill', 'none');
ballast.setAttribute('stroke', '#8B7355');
ballast.setAttribute('stroke-width', '24');
ballast.setAttribute('stroke-linecap', 'round');
// Create containers for ties and rails
const tiesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
tiesGroup.setAttribute('class', 'railroad-ties');
const railsGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
railsGroup.setAttribute('class', 'railroad-rails');
// Add elements to SVG in correct order
svg.appendChild(ballast);
svg.appendChild(tiesGroup);
svg.appendChild(railsGroup);
svg.appendChild(referencePath);
// Generate ties and rails properly
this.generateRailroadTiesAndRails();
console.log('🛤️ Generated dynamic curved railroad track with proper rail construction');
}}
generateTrackWaypoints() {{
// Create interesting waypoints for a scenic railroad journey
const waypoints = [
{{ x: 50, y: 300 }}, // Start at depot
{{ x: 150, y: 220 }}, // Climb into hills
{{ x: 280, y: 180 }}, // Mountain pass
{{ x: 420, y: 240 }}, // Descent to valley
{{ x: 550, y: 160 }}, // Bridge over canyon
{{ x: 680, y: 200 }}, // Rolling hills
{{ x: 750, y: 280 }} // Arrive at destination
];
// Add some controlled randomness for variety (but keep it playable)
return waypoints.map((point, index) => {{
if (index === 0 || index === waypoints.length - 1) {{
return point; // Keep start/end points fixed
}}
return {{
x: point.x + (Math.random() - 0.5) * 30,
y: point.y + (Math.random() - 0.5) * 40
}};
}});
}}
generateSmoothPath(waypoints) {{
// Generate smooth cubic bezier curves through waypoints
if (waypoints.length < 2) return '';
let pathData = `M ${{waypoints[0].x}} ${{waypoints[0].y}}`;
for (let i = 1; i < waypoints.length; i++) {{
const current = waypoints[i];
const previous = waypoints[i - 1];
// Calculate control points for smooth curves
const dx = current.x - previous.x;
const dy = current.y - previous.y;
const cp1x = previous.x + dx * 0.3;
const cp1y = previous.y + dy * 0.2;
const cp2x = current.x - dx * 0.3;
const cp2y = current.y - dy * 0.2;
pathData += ` C ${{cp1x}} ${{cp1y}}, ${{cp2x}} ${{cp2y}}, ${{current.x}} ${{current.y}}`;
}}
return pathData;
}}
offsetPath(pathData, offset) {{
// Simplified path offset - in a full implementation would use proper path offsetting
// For now, just offset the waypoints and regenerate
const points = this.extractPointsFromPath(pathData);
const offsetPoints = points.map(point => ({{
x: point.x + (Math.random() - 0.5) * 2, // Slight randomness
y: point.y + offset
}}));
return this.generateSmoothPath(offsetPoints);
}}
extractPointsFromPath(pathData) {{
// Extract key points from path data (simplified)
const matches = pathData.match(/(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)/g);
if (!matches) return [];
return matches.map(match => {{
const [x, y] = match.split(/\s+/).map(Number);
return {{ x, y }};
}});
}}
generateRailroadTiesAndRails() {{
// Generate railroad properly: ties first, then rails connecting the tie ends
const tiesContainer = document.querySelector('.railroad-ties');
const railsContainer = document.querySelector('.railroad-rails');
const routePath = document.querySelector('.train-route');
if (!tiesContainer || !railsContainer || !routePath) return;
// Clear existing elements
tiesContainer.innerHTML = '';
railsContainer.innerHTML = '';
const pathLength = routePath.getTotalLength();
const tieSpacing = 12; // Distance between ties
const tieCount = Math.floor(pathLength / tieSpacing);
const gaugeWidth = 15; // Standard gauge (tie extends 15px each side)
const leftRailPoints = [];
const rightRailPoints = [];
// Generate ties and collect rail points
for (let i = 0; i < tieCount; i++) {{
const distance = i * tieSpacing;
const point = routePath.getPointAtLength(distance);
// Calculate perpendicular angle for tie orientation
const nextDistance = Math.min(distance + 2, pathLength);
const nextPoint = routePath.getPointAtLength(nextDistance);
const angle = Math.atan2(nextPoint.y - point.y, nextPoint.x - point.x);
const perpAngle = angle + Math.PI / 2;
// Calculate tie end points
const leftX = point.x + Math.cos(perpAngle) * gaugeWidth;
const leftY = point.y + Math.sin(perpAngle) * gaugeWidth;
const rightX = point.x - Math.cos(perpAngle) * gaugeWidth;
const rightY = point.y - Math.sin(perpAngle) * gaugeWidth;
// Create railroad tie
const tie = document.createElementNS('http://www.w3.org/2000/svg', 'line');
tie.setAttribute('x1', leftX);
tie.setAttribute('y1', leftY);
tie.setAttribute('x2', rightX);
tie.setAttribute('y2', rightY);
tie.setAttribute('stroke', '#654321');
tie.setAttribute('stroke-width', '3');
tie.setAttribute('stroke-linecap', 'round');
tie.setAttribute('opacity', '0.8');
tiesContainer.appendChild(tie);
// Collect points for rails
leftRailPoints.push(`${{leftX}},${{leftY}}`);
rightRailPoints.push(`${{rightX}},${{rightY}}`);
}}
// Create left rail by connecting left ends of ties
if (leftRailPoints.length > 1) {{
const leftRail = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
leftRail.setAttribute('points', leftRailPoints.join(' '));
leftRail.setAttribute('fill', 'none');
leftRail.setAttribute('stroke', '#C0C0C0');
leftRail.setAttribute('stroke-width', '3');
leftRail.setAttribute('stroke-linecap', 'round');
railsContainer.appendChild(leftRail);
}}
// Create right rail by connecting right ends of ties
if (rightRailPoints.length > 1) {{
const rightRail = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
rightRail.setAttribute('points', rightRailPoints.join(' '));
rightRail.setAttribute('fill', 'none');
rightRail.setAttribute('stroke', '#C0C0C0');
rightRail.setAttribute('stroke-width', '3');
rightRail.setAttribute('stroke-linecap', 'round');
railsContainer.appendChild(rightRail);
}}
console.log(`🛤️ Generated ${{tieCount}} ties and connected rails properly`);
}}
updatePlayerRace(isCorrectAnswer = true) {{
if (this.style === 'sprint') {{
// Update steam train journey instead of race track
@@ -12611,6 +13036,40 @@ def generate_web_flashcards(numbers, config, output_path):
momentumLabel.textContent = `Momentum: ${{Math.round(this.momentum)}}% - ${{status}}`;
}}
// Update pressure gauge
this.updatePressureGauge();
}}
updatePressureGauge() {{
const pressureArc = document.getElementById('pressure-arc');
const pressureNeedle = document.getElementById('pressure-needle');
const pressureValue = document.getElementById('pressure-value');
if (pressureArc && pressureNeedle && pressureValue) {{
// Convert momentum (0-100) to gauge values
const pressure = Math.round(this.momentum);
const psi = Math.round(pressure * 1.5); // Scale to 0-150 PSI range
// Update arc progress (circumference is ~251.2 pixels)
const circumference = 251.2;
const offset = circumference - (pressure / 100) * circumference;
pressureArc.style.strokeDashoffset = offset;
// Update needle rotation (-90 to +90 degrees for half-circle)
const rotation = -90 + (pressure / 100) * 180;
pressureNeedle.style.transform = `rotate(${{rotation}}deg)`;
// Update pressure value display
pressureValue.textContent = `${{psi}} PSI`;
// Change gauge color based on pressure (playful colors)
let gaugeColor = '#ff6b6b'; // Coral red for low pressure
if (pressure > 70) gaugeColor = '#4ecdc4'; // Turquoise for high pressure
else if (pressure > 40) gaugeColor = '#feca57'; // Sunny yellow for medium pressure
pressureArc.style.stroke = gaugeColor;
}}
}}
generatePassengers() {{
@@ -12869,14 +13328,86 @@ def generate_web_flashcards(numbers, config, output_path):
}}
loseMomentum() {{
// Called when player gets wrong answer or times out - loses momentum due to friction
const config = this.getMomentumConfig();
const currentPenalty = config.wrongAnswerPenalty;
this.momentum = Math.max(0, this.momentum - currentPenalty);
this.updateMomentumDisplay();
// Called when player gets wrong answer or times out - DRAMATIC CONSEQUENCES!
// Wrong answers cause complete momentum loss and coal spilling
const previousMomentum = this.momentum;
this.momentum = 0; // Complete momentum loss for wrong answers!
console.log(`🚂 [${{this.timeoutSetting}}] Train losing steam! Lost ${{currentPenalty}}% momentum, now: ${{this.momentum}}%`);
this.playSound('incorrect');
// Trigger dramatic coal spilling animation
this.triggerCoalSpilling();
// Update displays
this.updateMomentumDisplay();
this.updatePressureGauge();
console.log(`💥 COAL SPILLING! Train momentum dropped from ${{previousMomentum}}% to 0% (DRAMATIC CONSEQUENCES)`);
// Play coal spilling sound with multiple chunks
this.playCoalSpillingSounds();
}}
triggerCoalSpilling() {{
// Create dramatic coal spilling visual effects
const routeContainer = document.querySelector('.route-path');
if (!routeContainer) return;
// Get train position for coal spilling location
const locomotive = document.getElementById('train-position');
if (!locomotive) return;
const trainRect = locomotive.getBoundingClientRect();
const containerRect = routeContainer.getBoundingClientRect();
// Create multiple coal chunks that spill from the train
for (let i = 0; i < 8; i++) {{
const coalChunk = document.createElement('div');
coalChunk.className = 'coal-chunk';
coalChunk.textContent = '';
coalChunk.style.position = 'absolute';
coalChunk.style.fontSize = '1.2rem';
coalChunk.style.zIndex = '50';
coalChunk.style.pointerEvents = 'none';
// Start position near the train
const startX = (trainRect.left - containerRect.left) + (Math.random() - 0.5) * 30;
const startY = (trainRect.top - containerRect.top) + (Math.random() - 0.5) * 20;
coalChunk.style.left = startX + 'px';
coalChunk.style.top = startY + 'px';
// Add dramatic spilling animation
coalChunk.style.animation = `coalSpill ${{0.8 + Math.random() * 0.4}}s ease-out forwards`;
coalChunk.style.animationDelay = `${{i * 0.1}}s`;
routeContainer.appendChild(coalChunk);
// Remove chunk after animation
setTimeout(() => {{
if (coalChunk.parentNode) {{
coalChunk.parentNode.removeChild(coalChunk);
}}
}}, 1500 + (i * 100));
}}
// Flash pressure gauge red to show dramatic failure
const pressureGauge = document.querySelector('.pressure-gauge-container');
if (pressureGauge) {{
pressureGauge.style.animation = 'pressureAlarm 0.8s ease-in-out';
setTimeout(() => {{
pressureGauge.style.animation = '';
}}, 800);
}}
}}
playCoalSpillingSounds() {{
// Play multiple coal chunk falling sounds in sequence
const sounds = ['incorrect', 'timeout']; // Using available sound types
sounds.forEach((sound, index) => {{
setTimeout(() => {{
this.playSound(sound);
}}, index * 200); // Staggered sound effects
}});
console.log('🔊 Playing coal spilling sound effects');
}}
updateTrainPosition() {{
@@ -12912,9 +13443,50 @@ def generate_web_flashcards(numbers, config, output_path):
locomotive.style.left = point.x + 'px';
locomotive.style.top = point.y + 'px';
// Update floating math display position to follow the train
const floatingDisplay = document.getElementById('floating-math-display');
if (floatingDisplay) {{
floatingDisplay.style.left = point.x + 'px';
floatingDisplay.style.top = (point.y - 50) + 'px'; // Position above the train
}}
console.log(`🚂 Train at position ${{Math.round(this.trainPosition)}}% - ${{Math.round(point.x)}}, ${{Math.round(point.y)}} (on track)`);
}}
handleDisplaySwitching() {{
// Only apply display switching logic in Lightning Sprint train mode
if (!document.querySelector('.route-path')) return;
const staticDisplay = document.querySelector('.complement-display');
const floatingDisplay = document.getElementById('floating-math-display');
const floatingText = document.getElementById('floating-challenge-text');
if (!staticDisplay || !floatingDisplay || !floatingText) return;
// Clear any existing timeout
if (this.displaySwitchTimeout) {{
clearTimeout(this.displaySwitchTimeout);
}}
// Initially show static display and hide floating display
staticDisplay.style.display = 'block';
floatingDisplay.style.display = 'none';
// Update floating display content to match current question
const challengeNumber = document.getElementById('challenge-number');
const targetNumber = document.getElementById('target-number');
if (challengeNumber && targetNumber) {{
floatingText.textContent = `Find the complement of ${{challengeNumber.textContent}} (target: ${{targetNumber.textContent}})`;
}}
// After 2 seconds, switch to floating display that follows the train
this.displaySwitchTimeout = setTimeout(() => {{
staticDisplay.style.display = 'none';
floatingDisplay.style.display = 'block';
console.log('🎈 Switched to floating math display following train');
}}, 2000);
}}
checkStationArrivals() {{
// Check if we've reached any stations where passengers want to get off
this.stations.forEach(station => {{