Compare commits

...

6 Commits

Author SHA1 Message Date
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
semantic-release-bot
5d89ad7ada chore(release): 4.4.5 [skip ci]
## [4.4.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.4...v4.4.5) (2025-10-17)

### Bug Fixes

* **complement-race:** add missing useEffect import ([3054130](30541304dd))
2025-10-17 12:24:38 +00:00
Thomas Hallock
30541304dd fix(complement-race): add missing useEffect import
- Runtime error: useEffect is not defined
- Added useEffect 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:23:39 -05:00
semantic-release-bot
376c8eb901 chore(release): 4.4.4 [skip ci]
## [4.4.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.3...v4.4.4) (2025-10-17)

### Bug Fixes

* **complement-race:** add pressure decay system and improve logging ([66992e8](66992e8770))
2025-10-17 12:23:29 +00:00
Thomas Hallock
66992e8770 fix(complement-race): add pressure decay system and improve logging
**1. Smart Logging (event-based instead of frame-based)**
- Only logs on answer submission, not every frame
- Format: "🚂 Answer #X: momentum=Y pos=Z pressure=P streak=S"
- Prevents console overflow in real-time game

**2. Pressure Decay System**
- Added `pressure` field to PlayerState type
- Pressure now independent from momentum (was stuck at 100)
- Correct answer: +20 pressure (add steam)
- Wrong answer: +5 pressure (less steam)
- Decay: -8 pressure per answer (steam escapes over time)
- Range: 0-100 with min/max caps

**3. Implementation**
- types.ts: Added pressure field to PlayerState
- Validator.ts: Initialize pressure=60, update with decay
- Provider.tsx: Use actual pressure from server (not calculated)
- Route reset: Reset pressure to 60 on new routes

This fixes the pressure gauge being pinned at 100 constantly.

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

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

View File

@@ -1,3 +1,24 @@
## [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)
### Bug Fixes
* **complement-race:** add missing useEffect import ([3054130](https://github.com/antialias/soroban-abacus-flashcards/commit/30541304dd0f0801860dd62967f7f7cae717bcdd))
## [4.4.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.3...v4.4.4) (2025-10-17)
### Bug Fixes
* **complement-race:** add pressure decay system and improve logging ([66992e8](https://github.com/antialias/soroban-abacus-flashcards/commit/66992e877065a42d00379ef8fae0a6e252b0ffcb))
## [4.4.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.2...v4.4.3) (2025-10-17)

View File

@@ -5,7 +5,15 @@
'use client'
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react'
import {
type GameMove,
buildPlayerMetadata,
@@ -240,6 +248,14 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
})
}, [activePlayers, players])
// 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
@@ -305,8 +321,8 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
// Sprint mode specific
momentum: localPlayer?.momentum || 0,
trainPosition: localPlayer?.position || 0,
pressure: localPlayer?.momentum ? Math.min(100, localPlayer.momentum + 10) : 0,
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,
@@ -328,11 +344,69 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
adaptiveFeedback: localUIState.adaptiveFeedback,
difficultyTracker: localUIState.difficultyTracker,
}
}, [multiplayerState, localPlayerId, localUIState])
}, [multiplayerState, localPlayerId, localUIState, clientPosition, clientPressure])
console.log(
`🚂 Sprint: momentum=${compatibleState.momentum} pos=${compatibleState.trainPosition} pressure=${compatibleState.pressure}`
)
// 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(() => {
if (compatibleState.style === 'sprint' && compatibleState.isGameActive) {
const key = `${compatibleState.correctAnswers}`
// Only log on new answers (not every frame)
if (lastLogRef.key !== key) {
console.log(
`🚂 Answer #${compatibleState.correctAnswers}: momentum=${compatibleState.momentum} pos=${Math.floor(compatibleState.trainPosition)} pressure=${compatibleState.pressure} streak=${compatibleState.streak}`
)
lastLogRef.key = key
}
}
}, [
compatibleState.correctAnswers,
compatibleState.momentum,
compatibleState.trainPosition,
compatibleState.pressure,
compatibleState.streak,
compatibleState.style,
compatibleState.isGameActive,
])
// Action creators
const startGame = useCallback(() => {

View File

@@ -169,7 +169,7 @@ export class ComplementRaceValidator
correctAnswers: 0,
totalQuestions: 0,
position: 0,
momentum: 50, // Start with some momentum in sprint mode
momentum: 50, // Start with some momentum (position/pressure calculated client-side)
isReady: false,
isActive: true,
currentAnswer: null,
@@ -317,17 +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 AND position
// Sprint: Update momentum only (position calculated client-side for smooth movement)
if (correct) {
updatedPlayer.momentum = Math.min(100, player.momentum + 15)
} else {
updatedPlayer.momentum = Math.max(0, player.momentum - 10)
}
// 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
@@ -527,12 +525,13 @@ export class ComplementRaceValidator
return { valid: false, error: 'Routes only available in sprint mode' }
}
// Reset all player positions to 0
// Reset all player positions to 0 for new route
const resetPlayers: Record<string, PlayerState> = {}
for (const [playerId, player] of Object.entries(state.players)) {
resetPlayers[playerId] = {
...player,
position: 0,
momentum: 50, // Reset momentum to starting value
passengers: [], // Clear any remaining passengers
}
}

View File

@@ -62,7 +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)
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.3",
"version": "4.4.6",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [