Compare commits

...

5 Commits

Author SHA1 Message Date
semantic-release-bot
627ca68cff chore(release): 4.4.14 [skip ci]
## [4.4.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.13...v4.4.14) (2025-10-18)

### Bug Fixes

* **complement-race:** remove dual game loop conflict preventing route progression ([84d42e2](84d42e22ac))
2025-10-18 00:54:05 +00:00
Thomas Hallock
84d42e22ac fix(complement-race): remove dual game loop conflict preventing route progression
PROBLEM:
Route progression was broken - train would never advance to next route after
completing Route 1. This was a regression after fixing passenger display issues.

ROOT CAUSE:
There were TWO conflicting game loops both managing train position:

1. Provider.tsx (lines 448-491): Updates clientPosition based on clientMomentum
2. useSteamJourney.ts (lines 87-126): Was calculating its own trainPosition and
   dispatching UPDATE_STEAM_JOURNEY

The critical issue: useSteamJourney dispatched UPDATE_STEAM_JOURNEY to update
position, but Provider IGNORES this action (line 764). This meant:
- useSteamJourney calculated a local trainPosition variable each frame
- It used this to check route completion threshold
- But state.trainPosition was stale because the dispatch never updated it
- So the route completion condition never triggered

FIX:
1. useSteamJourney.ts: Removed redundant position calculation (lines 95-125)
   - Now just reads state.trainPosition from Provider instead of calculating own
   - Game logic (boarding, delivery, route completion) uses authoritative position

2. useTrackManagement.ts: Changed trainPosition < 0 to <= 0 (lines 70, 82)
   - Provider resets clientPosition to exactly 0 (not negative)
   - This allows track and passenger display to update on route reset

3. Provider.tsx: Added debug logging for route changes and START_NEW_ROUTE
   - Helps diagnose route progression issues in console

4. Test files: Fixed TypeScript errors from API changes
   - Added missing maxCars and carSpacing parameters
   - Added missing name property to mock passengers

ARCHITECTURE NOTE:
The game now has a clear separation of concerns:
- Provider.tsx: Manages visual state (position, momentum, pressure)
- useSteamJourney.ts: Reads visual state and handles game logic
- Single source of truth for train position

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 19:53:02 -05:00
Thomas Hallock
37866ebb6d debug(complement-race): add logging for route transitions
Add temporary debug logging to diagnose why passengers aren't appearing
after route 1:

1. Log when START_NEW_ROUTE move is dispatched with route number
2. Log when Provider detects route change and how many passengers exist

This will help identify if the issue is:
- Move not being sent
- Server not generating passengers
- State update not propagating to client
- Display logic not showing passengers

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 18:18:06 -05:00
semantic-release-bot
7030794fa1 chore(release): 4.4.13 [skip ci]
## [4.4.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.12...v4.4.13) (2025-10-17)

### Bug Fixes

* **complement-race:** show new passengers when route changes ([ec1c8ed](ec1c8ed263))
2025-10-17 23:05:10 +00:00
Thomas Hallock
ec1c8ed263 fix(complement-race): show new passengers when route changes
Fixed bug where passengers wouldn't appear after completing a route.

Problem: When a new route started, the Provider reset trainPosition to 0,
but useTrackManagement was checking `trainPosition < 0` to detect resets.
Since 0 is not < 0, the condition failed and passengers didn't update.

Root cause:
1. Route completes, train at position ~107%
2. Client dispatches START_NEW_ROUTE with routeNumber=2
3. Server validates and broadcasts new state (route=2, new passengers)
4. Provider resets clientPosition to 0
5. useTrackManagement checks:
   - trainReset = trainPosition < 0 → FALSE (0 is not < 0!)
   - sameRoute = currentRoute === displayRouteRef.current → FALSE (2 !== 1)
6. Neither condition met, so displayPassengers doesn't update

Solution: Changed trainReset condition from `trainPosition < 0` to
`trainPosition <= 0` to treat position 0 as a reset. Also updated
the pending track application logic with the same fix.

Now passengers appear immediately when a new route starts.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 18:04:23 -05:00
8 changed files with 50 additions and 36 deletions

View File

@@ -1,3 +1,17 @@
## [4.4.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.13...v4.4.14) (2025-10-18)
### Bug Fixes
* **complement-race:** remove dual game loop conflict preventing route progression ([84d42e2](https://github.com/antialias/soroban-abacus-flashcards/commit/84d42e22ac0cdd25e87e45dc698029ad7ed78559))
## [4.4.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.12...v4.4.13) (2025-10-17)
### Bug Fixes
* **complement-race:** show new passengers when route changes ([ec1c8ed](https://github.com/antialias/soroban-abacus-flashcards/commit/ec1c8ed263844f56477c1f709041339b42b48f4e))
## [4.4.12](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.11...v4.4.12) (2025-10-17)

View File

@@ -92,37 +92,9 @@ export function useSteamJourney() {
// Steam Sprint is infinite - no time limit
// Get decay rate based on timeout setting (skill level)
const decayRate =
MOMENTUM_DECAY_RATES[state.timeoutSetting as keyof typeof MOMENTUM_DECAY_RATES] ||
MOMENTUM_DECAY_RATES.normal
// Calculate momentum decay for this frame
const momentumLoss = (decayRate * deltaTime) / 1000
// Update momentum (don't go below 0)
const newMomentum = Math.max(0, state.momentum - momentumLoss)
// Calculate speed from momentum (% per second)
const speed = newMomentum * SPEED_MULTIPLIER
// Update train position (accumulate, never go backward)
// Allow position to go past 100% so entire train (including cars) can exit tunnel
const positionDelta = (speed * deltaTime) / 1000
const trainPosition = state.trainPosition + positionDelta
// Calculate pressure (0-150 PSI) - based on momentum as percentage of max
const maxMomentum = 100 // Theoretical max momentum
const pressure = Math.min(150, (newMomentum / maxMomentum) * 150)
// Update state
dispatch({
type: 'UPDATE_STEAM_JOURNEY',
momentum: newMomentum,
trainPosition,
pressure,
elapsedTime: elapsed,
})
// Train position, momentum, and pressure are all managed by the Provider's game loop
// This hook only reads those values and handles game logic (boarding, delivery, route completion)
const trainPosition = state.trainPosition
// Check for passengers that should board
// Passengers board when an EMPTY car reaches their station

View File

@@ -67,7 +67,7 @@ export function useTrackManagement({
// Apply pending track when train resets to beginning
useEffect(() => {
if (pendingTrackData && trainPosition < 0) {
if (pendingTrackData && trainPosition <= 0) {
setTrackData(pendingTrackData)
previousRouteRef.current = currentRoute
setPendingTrackData(null)
@@ -77,9 +77,9 @@ export function useTrackManagement({
// Manage passenger display during route transitions
useEffect(() => {
// Only switch to new passengers when:
// 1. Train has reset to start position (< 0) - track has changed, OR
// 1. Train has reset to start position (<= 0) - track has changed, OR
// 2. Same route AND (in middle of track OR passengers have changed state)
const trainReset = trainPosition < 0
const trainReset = trainPosition <= 0
const sameRoute = currentRoute === displayRouteRef.current
const inMiddleOfTrack = trainPosition >= 10 && trainPosition < 90 // Avoid start/end transition zones

View File

@@ -29,6 +29,7 @@ describe('GameHUD', () => {
const mockPassenger: Passenger = {
id: 'passenger-1',
name: 'Test Passenger',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',

View File

@@ -44,6 +44,7 @@ describe('usePassengerAnimations', () => {
// Create mock passengers
mockPassenger1 = {
id: 'passenger-1',
name: 'Passenger 1',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -54,6 +55,7 @@ describe('usePassengerAnimations', () => {
mockPassenger2 = {
id: 'passenger-2',
name: 'Passenger 2',
avatar: '👩',
originStationId: 'station-1',
destinationStationId: 'station-2',

View File

@@ -52,6 +52,7 @@ describe('useTrackManagement', () => {
mockPassengers = [
{
id: 'passenger-1',
name: 'Test Passenger',
avatar: '👨',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -73,6 +74,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -90,6 +93,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -107,6 +112,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -123,6 +130,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -142,6 +151,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)
@@ -161,6 +172,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
}),
{
initialProps: { route: 1, position: 0 },
@@ -187,6 +200,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
}),
{
initialProps: { route: 1, position: 0 },
@@ -214,6 +229,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
}),
{
initialProps: { route: 1, position: -5 },
@@ -233,6 +250,7 @@ describe('useTrackManagement', () => {
const newPassengers: Passenger[] = [
{
id: 'passenger-2',
name: 'New Passenger',
avatar: '👩',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -273,6 +291,7 @@ describe('useTrackManagement', () => {
const newPassengers: Passenger[] = [
{
id: 'passenger-2',
name: 'New Passenger',
avatar: '👩',
originStationId: 'station-1',
destinationStationId: 'station-2',
@@ -354,6 +373,8 @@ describe('useTrackManagement', () => {
pathRef: mockPathRef,
stations: mockStations,
passengers: mockPassengers,
maxCars: 3,
carSpacing: 7,
})
)

View File

@@ -495,10 +495,13 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
const currentRoute = multiplayerState.currentRoute
// When route changes, reset position and give starting momentum
if (currentRoute > 1 && compatibleState.style === 'sprint') {
console.log(
`[Provider] Route changed to ${currentRoute}, resetting position. Passengers: ${multiplayerState.passengers.length}`
)
setClientPosition(0)
setClientMomentum(10) // Reset to starting momentum (gentle push)
}
}, [multiplayerState.currentRoute, compatibleState.style])
}, [multiplayerState.currentRoute, compatibleState.style, multiplayerState.passengers.length])
// Keep lastLogRef for future debugging needs
// (removed debug logging)
@@ -713,6 +716,7 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
case 'START_NEW_ROUTE':
// Send route progression to server
if (action.routeNumber !== undefined) {
console.log(`[Provider] Dispatching START_NEW_ROUTE for route ${action.routeNumber}`)
sendMove({
type: 'START_NEW_ROUTE',
playerId: activePlayers[0] || '',

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.4.12",
"version": "4.4.14",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [