Compare commits

..

4 Commits

Author SHA1 Message Date
semantic-release-bot
10eb4df09c chore(release): 4.6.1 [skip ci]
## [4.6.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.0...v4.6.1) (2025-10-18)

### Code Refactoring

* **complement-race:** move AI opponents from server-side to client-side ([09e21fa](09e21fa493))
2025-10-18 13:08:37 +00:00
Thomas Hallock
09e21fa493 refactor(complement-race): move AI opponents from server-side to client-side
Migrates AI opponent system from server-side Validator to client-side Provider to align with original single-player implementation and enable sophisticated features.

Changes:
- Remove AI generation/updates from Validator (now returns empty aiOpponents array)
- Add clientAIRacers state to Provider (similar to clientMomentum/clientPosition)
- Initialize AI racers when game starts based on config.enableAI
- Handle UPDATE_AI_POSITIONS dispatch to update client-side AI state
- Map clientAIRacers to compatibleState.aiRacers for components to consume

This enables the existing useAIRacers hook to work properly with:
- Time-based position updates (200ms interval)
- Rubber-banding mechanic (AI speeds up 2x when >10 units behind)
- AI commentary system with personality-based messages
- Context detection and sound effects

AI wins are now detected client-side via useAIRacers hook instead of server-side Validator.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:07:38 -05:00
semantic-release-bot
0541c115c5 chore(release): 4.6.0 [skip ci]
## [4.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.5.0...v4.6.0) (2025-10-18)

### Features

* **complement-race:** restore AI opponents in practice and survival modes ([325e07d](325e07de59))
2025-10-18 12:59:01 +00:00
Thomas Hallock
325e07de59 feat(complement-race): restore AI opponents in practice and survival modes
PROBLEM:
Practice Race and Survival Circuit modes had no AI opponents visible, even
though the config had enableAI: true and aiOpponentCount: 2.

ROOT CAUSE:
The Validator's validateStartGame method (line 208) was initializing
aiOpponents as an empty array and never actually generating them, even
when config.enableAI was true.

This was likely lost during the multiplayer migration when the code was
refactored from single-player to multiplayer architecture.

FIX:
1. Added generateAIOpponents() method (lines 782-808)
   - Creates AI opponents with names like "Robo-Racer", "Calculator", etc.
   - Assigns personality types (competitive/analytical)
   - Gives each AI a random speed multiplier (0.8-1.2)

2. Call generateAIOpponents in validateStartGame (lines 209-212)
   - Only generates AI when config.enableAI is true and aiOpponentCount > 0

3. Added updateAIOpponents() method (lines 810-840)
   - Updates AI positions during the game
   - Practice mode: AI moves forward based on speed (simulates answering)
   - Survival mode: AI continuously moves forward
   - Sprint mode: AI doesn't participate (train journey is single-player)

4. Call updateAIOpponents in validateSubmitAnswer (lines 367-373)
   - AI opponents progress each time a human answers a question

5. Updated checkWinCondition (lines 904-909, 970-976)
   - Practice mode: Check if any AI reaches position 100
   - Survival mode: Check if any AI has highest position when time expires

BEHAVIOR:
- Practice Race now shows 2 AI opponents racing alongside you
- Survival Circuit now has AI competitors to beat
- AI opponents move at slightly randomized speeds for variety
- AI can win the race if they reach the goal first

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 07:58:09 -05:00
4 changed files with 98 additions and 14 deletions

View File

@@ -1,3 +1,17 @@
## [4.6.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.0...v4.6.1) (2025-10-18)
### Code Refactoring
* **complement-race:** move AI opponents from server-side to client-side ([09e21fa](https://github.com/antialias/soroban-abacus-flashcards/commit/09e21fa4934c634d0ce46381ef7e40238fc134c3))
## [4.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.5.0...v4.6.0) (2025-10-18)
### Features
* **complement-race:** restore AI opponents in practice and survival modes ([325e07d](https://github.com/antialias/soroban-abacus-flashcards/commit/325e07de5929169aa333ef16f7bca5b41eeb1622))
## [4.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.15...v4.5.0) (2025-10-18)

View File

@@ -317,6 +317,19 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
const [clientMomentum, setClientMomentum] = useState(10) // Start at 10 for gentle push
const [clientPosition, setClientPosition] = useState(0)
const [clientPressure, setClientPressure] = useState(0)
const [clientAIRacers, setClientAIRacers] = useState<
Array<{
id: string
name: string
position: number
speed: number
personality: 'competitive' | 'analytical'
icon: string
lastComment: number
commentCooldown: number
previousPosition: number
}>
>([])
const lastUpdateRef = useRef(Date.now())
const gameStartTimeRef = useRef(0)
@@ -388,17 +401,7 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
raceGoal: multiplayerState.config.raceGoal,
timeLimit: multiplayerState.config.timeLimit ?? null,
speedMultiplier: 1.0,
aiRacers: multiplayerState.aiOpponents.map((ai) => ({
id: ai.id,
name: ai.name,
position: ai.position,
speed: ai.speed,
personality: ai.personality,
icon: ai.personality === 'competitive' ? '🏃‍♂️' : '🏃',
lastComment: ai.lastCommentTime,
commentCooldown: 0,
previousPosition: ai.position,
})),
aiRacers: clientAIRacers, // Use client-side AI state
// Sprint mode specific (all client-side for smooth movement)
momentum: clientMomentum, // Client-only state with continuous decay
@@ -425,7 +428,15 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
adaptiveFeedback: localUIState.adaptiveFeedback,
difficultyTracker: localUIState.difficultyTracker,
}
}, [multiplayerState, localPlayerId, localUIState, clientPosition, clientPressure])
}, [
multiplayerState,
localPlayerId,
localUIState,
clientPosition,
clientPressure,
clientMomentum,
clientAIRacers,
])
// Initialize game start time when game becomes active
useEffect(() => {
@@ -444,6 +455,41 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
}
}, [compatibleState.isGameActive, compatibleState.style])
// Initialize AI racers when game starts
useEffect(() => {
if (compatibleState.isGameActive && multiplayerState.config.enableAI) {
const count = multiplayerState.config.aiOpponentCount
if (count > 0 && clientAIRacers.length === 0) {
const aiNames = ['Robo-Racer', 'Calculator', 'Speed Demon', 'Brain Bot']
const personalities: Array<'competitive' | 'analytical'> = ['competitive', 'analytical']
const newAI = []
for (let i = 0; i < Math.min(count, aiNames.length); i++) {
newAI.push({
id: `ai-${i}`,
name: aiNames[i],
personality: personalities[i % personalities.length] as 'competitive' | 'analytical',
position: 0,
speed: 0.8 + Math.random() * 0.4, // Speed multiplier 0.8-1.2
icon: personalities[i % personalities.length] === 'competitive' ? '🏃‍♂️' : '🏃',
lastComment: 0,
commentCooldown: 0,
previousPosition: 0,
})
}
setClientAIRacers(newAI)
}
} else if (!compatibleState.isGameActive) {
// Clear AI when game ends
setClientAIRacers([])
}
}, [
compatibleState.isGameActive,
multiplayerState.config.enableAI,
multiplayerState.config.aiOpponentCount,
clientAIRacers.length,
])
// Main client-side game loop: momentum decay and position calculation
useEffect(() => {
if (!compatibleState.isGameActive || compatibleState.style !== 'sprint') return
@@ -757,8 +803,27 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
})
break
}
case 'UPDATE_AI_POSITIONS': {
// Update client-side AI positions
if (action.positions && Array.isArray(action.positions)) {
setClientAIRacers((prevRacers) =>
prevRacers.map((racer) => {
const update = action.positions.find(
(p: { id: string; position: number }) => p.id === racer.id
)
return update
? {
...racer,
previousPosition: racer.position,
position: update.position,
}
: racer
})
)
}
break
}
// Other local actions that don't affect UI (can be ignored for now)
case 'UPDATE_AI_POSITIONS':
case 'UPDATE_MOMENTUM':
case 'UPDATE_TRAIN_POSITION':
case 'UPDATE_STEAM_JOURNEY':

View File

@@ -218,6 +218,7 @@ export class ComplementRaceValidator
routeStartTime: state.config.style === 'sprint' ? Date.now() : null,
raceStartTime: Date.now(), // Race starts immediately
gameStartTime: Date.now(),
aiOpponents: [], // AI handled client-side
}
return { valid: true, newState }
@@ -824,6 +825,7 @@ export class ComplementRaceValidator
return playerId
}
}
// AI wins handled client-side via useAIRacers hook
}
// Sprint mode: Check route-based, score-based, or time-based win conditions
@@ -875,12 +877,15 @@ export class ComplementRaceValidator
// Find player with highest position (most laps)
let maxPosition = 0
let winner: string | null = null
for (const [playerId, player] of Object.entries(players)) {
if (player.position > maxPosition) {
maxPosition = player.position
winner = playerId
}
}
// AI wins handled client-side via useAIRacers hook
return winner
}
}

View File

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