Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea1e548e61 | ||
|
|
d43829ad48 | ||
|
|
dbcedb7144 | ||
|
|
46a80cbcc8 | ||
|
|
5d89ad7ada | ||
|
|
30541304dd | ||
|
|
376c8eb901 | ||
|
|
66992e8770 | ||
|
|
52019a24c2 | ||
|
|
54b46e771e | ||
|
|
334a49c92e | ||
|
|
739e928c6e | ||
|
|
86af2fe902 | ||
|
|
60ce9c0eb1 |
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** train now moves in sprint mode ([54b46e7](https://github.com/antialias/soroban-abacus-flashcards/commit/54b46e771e654721e7fabb1f45ecd45daf8e447f))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* simplify train debug logs to strings only ([334a49c](https://github.com/antialias/soroban-abacus-flashcards/commit/334a49c92e112c852c483b5dbe3a3d0aef8a5c03))
|
||||
|
||||
## [4.4.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.1...v4.4.2) (2025-10-17)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **complement-race:** remove verbose logging, keep only train debug logs ([86af2fe](https://github.com/antialias/soroban-abacus-flashcards/commit/86af2fe902b3d3790b7b4659fdc698caed8e4dd9))
|
||||
|
||||
## [4.4.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.0...v4.4.1) (2025-10-17)
|
||||
|
||||
|
||||
|
||||
@@ -70,12 +70,6 @@ export function GameDisplay() {
|
||||
// Only process number keys
|
||||
if (/^[0-9]$/.test(e.key)) {
|
||||
const newInput = state.currentInput + e.key
|
||||
console.log('⌨️ [KeyPress] Number key pressed:', {
|
||||
key: e.key,
|
||||
oldInput: state.currentInput,
|
||||
newInput,
|
||||
currentQuestion: state.currentQuestion?.number,
|
||||
})
|
||||
dispatch({ type: 'UPDATE_INPUT', input: newInput })
|
||||
|
||||
// Check if answer is complete
|
||||
@@ -83,27 +77,12 @@ export function GameDisplay() {
|
||||
const answer = parseInt(newInput, 10)
|
||||
const correctAnswer = state.currentQuestion.correctAnswer
|
||||
|
||||
console.log('🔍 [KeyPress] Checking answer:', {
|
||||
newInput,
|
||||
newInputLength: newInput.length,
|
||||
correctAnswer,
|
||||
correctAnswerLength: correctAnswer.toString().length,
|
||||
willSubmit: newInput.length >= correctAnswer.toString().length,
|
||||
})
|
||||
|
||||
// If we have enough digits to match the answer, submit
|
||||
if (newInput.length >= correctAnswer.toString().length) {
|
||||
const responseTime = Date.now() - state.questionStartTime
|
||||
const isCorrect = answer === correctAnswer
|
||||
const pairKey = `${state.currentQuestion.number}_${state.currentQuestion.correctAnswer}_${state.currentQuestion.targetSum}`
|
||||
|
||||
console.log('📝 [KeyPress] Submitting answer:', {
|
||||
answer,
|
||||
correctAnswer,
|
||||
isCorrect,
|
||||
responseTime,
|
||||
})
|
||||
|
||||
if (isCorrect) {
|
||||
// Correct answer
|
||||
dispatch({ type: 'SUBMIT_ANSWER', answer })
|
||||
@@ -154,7 +133,6 @@ export function GameDisplay() {
|
||||
dispatch({ type: 'SHOW_ADAPTIVE_FEEDBACK', feedback })
|
||||
}
|
||||
|
||||
console.log('➡️ [KeyPress] Dispatching NEXT_QUESTION after correct answer')
|
||||
dispatch({ type: 'NEXT_QUESTION' })
|
||||
} else {
|
||||
// Incorrect answer
|
||||
@@ -172,16 +150,11 @@ export function GameDisplay() {
|
||||
dispatch({ type: 'SHOW_ADAPTIVE_FEEDBACK', feedback })
|
||||
}
|
||||
|
||||
console.log('❌ [KeyPress] Incorrect answer - clearing input')
|
||||
dispatch({ type: 'UPDATE_INPUT', input: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'Backspace') {
|
||||
console.log('⌫ [KeyPress] Backspace pressed:', {
|
||||
oldInput: state.currentInput,
|
||||
newInput: state.currentInput.slice(0, -1),
|
||||
})
|
||||
dispatch({
|
||||
type: 'UPDATE_INPUT',
|
||||
input: state.currentInput.slice(0, -1),
|
||||
@@ -189,16 +162,8 @@ export function GameDisplay() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔄 [KeyPress Effect] Setting up keyboard listener with state:', {
|
||||
currentInput: state.currentInput,
|
||||
currentQuestion: state.currentQuestion?.number,
|
||||
})
|
||||
|
||||
window.addEventListener('keydown', handleKeyPress)
|
||||
return () => {
|
||||
console.log('🧹 [KeyPress Effect] Cleaning up keyboard listener')
|
||||
window.removeEventListener('keydown', handleKeyPress)
|
||||
}
|
||||
return () => window.removeEventListener('keydown', handleKeyPress)
|
||||
}, [
|
||||
state.currentInput,
|
||||
state.currentQuestion,
|
||||
@@ -231,19 +196,6 @@ export function GameDisplay() {
|
||||
|
||||
if (!state.currentQuestion) return null
|
||||
|
||||
// DEBUG: Log state on every render
|
||||
console.log('🎮 [GameDisplay] Render:', {
|
||||
currentInput: state.currentInput,
|
||||
currentInputLength: state.currentInput?.length,
|
||||
currentInputType: typeof state.currentInput,
|
||||
currentQuestion: state.currentQuestion,
|
||||
questionNumber: state.currentQuestion?.number,
|
||||
correctAnswer: state.currentQuestion?.correctAnswer,
|
||||
targetSum: state.currentQuestion?.targetSum,
|
||||
score: state.score,
|
||||
streak: state.streak,
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="game-display"
|
||||
|
||||
@@ -94,6 +94,10 @@ export function SteamTrainJourney({
|
||||
currentInput,
|
||||
}: SteamTrainJourneyProps) {
|
||||
const { state } = useComplementRace()
|
||||
console.log(
|
||||
`🚂 Train: mom=${momentum} pos=${trainPosition} stations=${state.stations.length} passengers=${state.passengers.length}`
|
||||
)
|
||||
|
||||
const { getSkyGradient, getTimeOfDayPeriod } = useSteamJourney()
|
||||
const _skyGradient = getSkyGradient()
|
||||
const period = getTimeOfDayPeriod()
|
||||
|
||||
@@ -113,13 +113,6 @@ const initialState: GameState = {
|
||||
}
|
||||
|
||||
function gameReducer(state: GameState, action: GameAction): GameState {
|
||||
console.log('🔄 [Reducer] Action dispatched:', {
|
||||
type: action.type,
|
||||
action,
|
||||
currentInput: state.currentInput,
|
||||
currentQuestion: state.currentQuestion?.number,
|
||||
})
|
||||
|
||||
switch (action.type) {
|
||||
case 'SET_MODE':
|
||||
return { ...state, mode: action.mode }
|
||||
@@ -178,8 +171,6 @@ function gameReducer(state: GameState, action: GameAction): GameState {
|
||||
}
|
||||
|
||||
case 'NEXT_QUESTION': {
|
||||
console.log('➡️ [Reducer] NEXT_QUESTION - clearing input and generating new question')
|
||||
|
||||
// Generate new question based on mode
|
||||
const generateQuestion = () => {
|
||||
let targetSum: number
|
||||
@@ -221,28 +212,16 @@ function gameReducer(state: GameState, action: GameAction): GameState {
|
||||
}
|
||||
}
|
||||
|
||||
const newQuestion = generateQuestion()
|
||||
console.log('📊 [Reducer] NEXT_QUESTION result:', {
|
||||
oldQuestion: state.currentQuestion,
|
||||
newQuestion,
|
||||
oldInput: state.currentInput,
|
||||
newInput: '',
|
||||
})
|
||||
|
||||
return {
|
||||
...state,
|
||||
previousQuestion: state.currentQuestion,
|
||||
currentQuestion: newQuestion,
|
||||
currentQuestion: generateQuestion(),
|
||||
questionStartTime: Date.now(),
|
||||
currentInput: '',
|
||||
}
|
||||
}
|
||||
|
||||
case 'UPDATE_INPUT':
|
||||
console.log('✏️ [Reducer] UPDATE_INPUT:', {
|
||||
oldInput: state.currentInput,
|
||||
newInput: action.input,
|
||||
})
|
||||
return { ...state, currentInput: action.input }
|
||||
|
||||
case 'SUBMIT_ANSWER': {
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
|
||||
'use client'
|
||||
|
||||
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
type GameMove,
|
||||
buildPlayerMetadata,
|
||||
@@ -240,6 +249,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 +322,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,7 +345,69 @@ 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(() => {
|
||||
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(() => {
|
||||
@@ -482,8 +561,6 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
// Compatibility dispatch function for existing UI components
|
||||
const dispatch = useCallback(
|
||||
(action: { type: string; [key: string]: any }) => {
|
||||
console.log('[ComplementRaceProvider] dispatch called (compatibility layer):', action.type)
|
||||
|
||||
// Map old reducer actions to new action creators
|
||||
switch (action.type) {
|
||||
case 'START_COUNTDOWN':
|
||||
@@ -497,7 +574,6 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
break
|
||||
case 'NEXT_QUESTION':
|
||||
console.log('🧹 [Provider] NEXT_QUESTION - clearing local input state')
|
||||
setLocalUIState((prev) => ({ ...prev, currentInput: '' }))
|
||||
nextQuestion()
|
||||
break
|
||||
|
||||
@@ -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,12 +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
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -522,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.4.1",
|
||||
"version": "4.4.7",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user