Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release-bot
ea1e548e61 chore(release): 4.4.7 [skip ci]
## [4.4.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.6...v4.4.7) (2025-10-17)

### Bug Fixes

* **complement-race:** add missing useRef import ([d43829a](d43829ad48))
2025-10-17 12:32:46 +00:00
Thomas Hallock
d43829ad48 fix(complement-race): add missing useRef import
- TypeScript error: Cannot find name 'useRef'
- Added useRef to React imports in Provider.tsx

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:31:43 -05:00
semantic-release-bot
dbcedb7144 chore(release): 4.4.6 [skip ci]
## [4.4.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.5...v4.4.6) (2025-10-17)

### Bug Fixes

* **complement-race:** restore smooth train movement with client-side game loop ([46a80cb](46a80cbcc8))
2025-10-17 12:30:51 +00:00
Thomas Hallock
46a80cbcc8 fix(complement-race): restore smooth train movement with client-side game loop
**Problem**: Train was jumping discretely on each answer instead of moving smoothly

**Root Cause**: Ported incorrectly - position updated on answer submission instead of continuously

**Original Mechanics** (from useSteamJourney.ts):
- 50ms game loop (20fps) runs continuously
- Position calculated from momentum: `position += (momentum * 0.15 * deltaTime) / 1000`
- Pressure calculated from momentum: `pressure = (momentum / 100) * 150` (0-150 PSI)
- Answers only affect momentum (+15 correct, -10 wrong)

**Fixed Implementation**:
- Client-side game loop at 50ms interval
- Position calculated continuously from server momentum
- Pressure calculated continuously from momentum (0-150 PSI)
- Server only tracks momentum (authoritative)
- Removed discrete position jumps from Validator
- Position/pressure are derived values, not stored

**Files Changed**:
- Provider.tsx: Added client game loop, use calculated position/pressure
- Validator.ts: Removed position updates, only track momentum
- types.ts: Removed pressure field (calculated client-side)

This matches the original smooth movement behavior.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:29:34 -05:00
5 changed files with 68 additions and 22 deletions

View File

@@ -1,3 +1,17 @@
## [4.4.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.6...v4.4.7) (2025-10-17)
### Bug Fixes
* **complement-race:** add missing useRef import ([d43829a](https://github.com/antialias/soroban-abacus-flashcards/commit/d43829ad48f7ee879a46879f5e6ac1256db1f564))
## [4.4.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.5...v4.4.6) (2025-10-17)
### Bug Fixes
* **complement-race:** restore smooth train movement with client-side game loop ([46a80cb](https://github.com/antialias/soroban-abacus-flashcards/commit/46a80cbcc8ec39224d4edaf540da25611d48fbdd))
## [4.4.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.4...v4.4.5) (2025-10-17)

View File

@@ -11,6 +11,7 @@ import {
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from 'react'
@@ -251,6 +252,11 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
// Debug logging ref (track last logged values)
const lastLogRef = useState({ key: '', count: 0 })[0]
// Client-side smooth movement state
const [clientPosition, setClientPosition] = useState(0)
const [clientPressure, setClientPressure] = useState(0)
const lastUpdateRef = useRef(Date.now())
// Transform multiplayer state to look like single-player state
const compatibleState = useMemo((): CompatibleGameState => {
const localPlayer = localPlayerId ? multiplayerState.players[localPlayerId] : null
@@ -316,8 +322,8 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
// Sprint mode specific
momentum: localPlayer?.momentum || 0,
trainPosition: localPlayer?.position || 0,
pressure: localPlayer?.pressure || 0, // Use actual pressure from server (has decay)
trainPosition: clientPosition, // Use client-calculated smooth position
pressure: clientPressure, // Use client-calculated smooth pressure
elapsedTime: multiplayerState.gameStartTime ? Date.now() - multiplayerState.gameStartTime : 0,
lastCorrectAnswerTime: localPlayer?.lastAnswerTime || Date.now(),
currentRoute: multiplayerState.currentRoute,
@@ -339,7 +345,46 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
adaptiveFeedback: localUIState.adaptiveFeedback,
difficultyTracker: localUIState.difficultyTracker,
}
}, [multiplayerState, localPlayerId, localUIState])
}, [multiplayerState, localPlayerId, localUIState, clientPosition, clientPressure])
// Client-side game loop for smooth train movement
useEffect(() => {
if (compatibleState.style !== 'sprint' || !compatibleState.isGameActive) return
const UPDATE_INTERVAL = 50 // 50ms = ~20fps
const SPEED_MULTIPLIER = 0.15 // speed = momentum * 0.15 (% per second)
const interval = setInterval(() => {
const now = Date.now()
const deltaTime = now - lastUpdateRef.current
lastUpdateRef.current = now
// Get server momentum (authoritative)
const serverMomentum = compatibleState.momentum
// Calculate speed from momentum
const speed = serverMomentum * SPEED_MULTIPLIER
// Update position continuously based on momentum
const positionDelta = (speed * deltaTime) / 1000
setClientPosition((prev) => prev + positionDelta)
// Calculate pressure from momentum (0-150 PSI)
const pressure = Math.min(150, (serverMomentum / 100) * 150)
setClientPressure(pressure)
}, UPDATE_INTERVAL)
return () => clearInterval(interval)
}, [compatibleState.style, compatibleState.isGameActive, compatibleState.momentum])
// Sync client position with server position on route changes/resets
useEffect(() => {
const serverPosition = multiplayerState.players[localPlayerId || '']?.position || 0
// Only sync if there's a significant jump (route change)
if (Math.abs(serverPosition - clientPosition) > 10) {
setClientPosition(serverPosition)
}
}, [multiplayerState.players, localPlayerId, clientPosition])
// Debug logging: only log on answer submission or significant events
useEffect(() => {

View File

@@ -169,8 +169,7 @@ export class ComplementRaceValidator
correctAnswers: 0,
totalQuestions: 0,
position: 0,
momentum: 50, // Start with some momentum in sprint mode
pressure: 60, // Start with initial pressure
momentum: 50, // Start with some momentum (position/pressure calculated client-side)
isReady: false,
isActive: true,
currentAnswer: null,
@@ -318,25 +317,15 @@ export class ComplementRaceValidator
updatedPlayer.position = Math.min(100, player.position + 100 / state.config.raceGoal)
}
} else if (state.config.style === 'sprint') {
// Sprint: Update momentum, pressure, AND position
// Sprint: Update momentum only (position calculated client-side for smooth movement)
if (correct) {
updatedPlayer.momentum = Math.min(100, player.momentum + 15)
// Add pressure on correct answer (add steam to boiler)
updatedPlayer.pressure = Math.min(100, player.pressure + 20)
} else {
updatedPlayer.momentum = Math.max(0, player.momentum - 10)
// Less pressure added on wrong answer
updatedPlayer.pressure = Math.min(100, player.pressure + 5)
}
// Pressure decay: Every answer causes some steam to escape
// Decay rate: 8 points per answer (pressure naturally decreases over time)
updatedPlayer.pressure = Math.max(0, updatedPlayer.pressure - 8)
// Move train based on momentum (momentum/20 = position change per answer)
// Higher momentum = faster movement
const moveDistance = updatedPlayer.momentum / 20
updatedPlayer.position = Math.min(100, player.position + moveDistance)
// Position is calculated client-side continuously based on momentum
// This allows for smooth 20fps movement instead of discrete jumps per answer
} else if (state.config.style === 'survival') {
// Survival: Always move forward, speed based on accuracy
const moveDistance = correct ? 5 : 2
@@ -543,7 +532,6 @@ export class ComplementRaceValidator
...player,
position: 0,
momentum: 50, // Reset momentum to starting value
pressure: 60, // Reset pressure to starting value
passengers: [], // Clear any remaining passengers
}
}

View File

@@ -62,8 +62,7 @@ export interface PlayerState {
// Position & Progress
position: number // 0-100% for practice/sprint, lap count for survival
momentum: number // 0-100 (sprint mode only)
pressure: number // 0-100 (sprint mode only, decays over time)
momentum: number // 0-100 (sprint mode only, position/pressure calculated client-side)
// Current state
isReady: boolean

View File

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