Compare commits

...

16 Commits

Author SHA1 Message Date
semantic-release-bot
cf997b9cbc chore(release): 4.6.8 [skip ci]
## [4.6.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.7...v4.6.8) (2025-10-18)

### Bug Fixes

* **complement-race:** counter-flip AI speech bubbles to make text readable ([07d5607](07d5607218))
2025-10-18 14:07:19 +00:00
Thomas Hallock
07d5607218 fix(complement-race): counter-flip AI speech bubbles to make text readable
When AI racers were flipped with scaleX(-1) to face right, their speech
bubbles were also flipped, making the text appear mirrored/backwards.

Added a wrapper div around SpeechBubble with scaleX(-1) to counter the
parent's flip transform, keeping the text readable while the emoji
remains flipped.

This matches the pattern used in CircularTrack which counter-rotates
speech bubbles to keep them upright.

LinearTrack.tsx:145-154

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:06:25 -05:00
semantic-release-bot
614a081ca6 chore(release): 4.6.7 [skip ci]
## [4.6.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.6...v4.6.7) (2025-10-18)

### Bug Fixes

* **complement-race:** use active local players pattern from navbar ([71cdc34](71cdc342c9))
2025-10-18 14:03:39 +00:00
Thomas Hallock
71cdc342c9 fix(complement-race): use active local players pattern from navbar
Fixed player emoji selection to be consistent with navbar's player
display logic. Both LinearTrack and CircularTrack now use the same
pattern as PageWithNav to get the current user's active local players.

Pattern:
- Get activePlayers Set from GameModeContext
- Map to player objects
- Filter for isLocal !== false (excludes remote players)
- Take first player's emoji

This ensures:
- Consistency with navbar display
- Correct handling of solo vs arcade room modes
- Proper filtering of remote players (isLocal === false)
- Future-proof for multi-player support

Files changed:
- LinearTrack.tsx:23,27-30
- CircularTrack.tsx:20,26-29

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 09:02:45 -05:00
semantic-release-bot
214b9077ab chore(release): 4.6.6 [skip ci]
## [4.6.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.5...v4.6.6) (2025-10-18)

### Bug Fixes

* **complement-race:** use local player emoji instead of first active player ([76eb051](76eb0517c2))
2025-10-18 13:58:33 +00:00
Thomas Hallock
76eb0517c2 fix(complement-race): use local player emoji instead of first active player
Fixed a bug where both LinearTrack and CircularTrack were displaying
the first active player's emoji instead of the current user's (local)
player emoji. This would cause incorrect avatar display in multi-player
arcade rooms.

Changed from:
- Getting first element of activePlayers array
- Would show wrong player in multi-player scenarios

To:
- Finding player with isLocal === true
- Correctly shows current user's avatar
- Matches pattern used throughout Provider.tsx

Files changed:
- LinearTrack.tsx: Use local player for emoji
- CircularTrack.tsx: Use local player for emoji

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:57:45 -05:00
semantic-release-bot
820000f93b chore(release): 4.6.5 [skip ci]
## [4.6.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.4...v4.6.5) (2025-10-18)

### Bug Fixes

* **complement-race:** flip player avatar to face right in practice mode ([fa6b3b6](fa6b3b69d5))
2025-10-18 13:48:07 +00:00
Thomas Hallock
fa6b3b69d5 fix(complement-race): flip player avatar to face right in practice mode
Adds scaleX(-1) to player racer icon so it faces the same direction as AI racers (racing to the right).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:47:11 -05:00
semantic-release-bot
ca4ba6e2d7 chore(release): 4.6.4 [skip ci]
## [4.6.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.3...v4.6.4) (2025-10-18)

### Bug Fixes

* **complement-race:** flip AI racers to face right in practice mode ([ebfff1a](ebfff1a62f))
2025-10-18 13:39:34 +00:00
Thomas Hallock
ebfff1a62f fix(complement-race): flip AI racers to face right in practice mode
Adds horizontal flip (scaleX(-1)) to AI racer icons in LinearTrack so they face the correct direction (right) when racing.

CircularTrack doesn't need this fix as racers are already rotated to face the direction they're traveling around the track.

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

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

### Bug Fixes

* **complement-race:** balance AI speeds to match original implementation ([054f0c0](054f0c0d23))
2025-10-18 13:32:32 +00:00
Thomas Hallock
054f0c0d23 fix(complement-race): balance AI speeds to match original implementation
Fixes AI opponents racing away too fast and ending the game in 2 seconds by restoring the original balanced speeds and mode-specific multipliers.

Changes:
- Reduce AI base speeds from 0.8-1.2 to 0.32 (Swift AI) and 0.2 (Math Bot)
- Add mode-specific speedMultipliers: practice (0.7), sprint (0.9), survival (1.0)
- Update AI names to match original: "Swift AI" and "Math Bot"

The original system uses much lower base speeds combined with:
- Random variance (0.6-1.4x per update)
- Rubber-banding (2x speed when >10 units behind)
- Adaptive difficulty adjustments based on player performance

This makes AI opponents challenging but fair, adapting to player skill rather than just racing ahead at fixed high speeds.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:31:38 -05:00
semantic-release-bot
45ff01e1fe chore(release): 4.6.2 [skip ci]
## [4.6.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.1...v4.6.2) (2025-10-18)

### Bug Fixes

* **build:** resolve Docker build failures preventing deployment ([7801dbb](7801dbb25f))
2025-10-18 13:13:31 +00:00
Thomas Hallock
7801dbb25f fix(build): resolve Docker build failures preventing deployment
Fixed two critical issues blocking deployment:

1. **TypeScript build failure**: Added @types/minimatch dependency and created
   proper tsconfig.json files for @soroban/core and @soroban/client packages.
   The DTS build was failing because TypeScript couldn't find the minimatch
   type definitions.

2. **Next.js prerendering error**: Fixed complement-race pages importing from
   wrong provider. Pages were using ./context/ComplementRaceContext but game
   components were using @/arcade-games/complement-race/Provider, causing
   "useComplementRace must be used within ComplementRaceProvider" errors
   during static page generation.

Deployment was blocked for 2 days. Container on NAS is from Oct 16th while
latest commits are from Oct 18th.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 08:12:41 -05:00
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
15 changed files with 205 additions and 128 deletions

View File

@@ -1,3 +1,59 @@
## [4.6.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.7...v4.6.8) (2025-10-18)
### Bug Fixes
* **complement-race:** counter-flip AI speech bubbles to make text readable ([07d5607](https://github.com/antialias/soroban-abacus-flashcards/commit/07d5607218aee03e813eceff5d161a7838d66bcb))
## [4.6.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.6...v4.6.7) (2025-10-18)
### Bug Fixes
* **complement-race:** use active local players pattern from navbar ([71cdc34](https://github.com/antialias/soroban-abacus-flashcards/commit/71cdc342c97ca53b5e7e4202d4d344199e8ddd98))
## [4.6.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.5...v4.6.6) (2025-10-18)
### Bug Fixes
* **complement-race:** use local player emoji instead of first active player ([76eb051](https://github.com/antialias/soroban-abacus-flashcards/commit/76eb0517c202d1b9160b49dec0b99ff4972daff2))
## [4.6.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.4...v4.6.5) (2025-10-18)
### Bug Fixes
* **complement-race:** flip player avatar to face right in practice mode ([fa6b3b6](https://github.com/antialias/soroban-abacus-flashcards/commit/fa6b3b69d5a4a7eb70f8c18fc8c122c54c4d504a))
## [4.6.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.3...v4.6.4) (2025-10-18)
### Bug Fixes
* **complement-race:** flip AI racers to face right in practice mode ([ebfff1a](https://github.com/antialias/soroban-abacus-flashcards/commit/ebfff1a62fd104d531a8158345c8c012ec8a55d3))
## [4.6.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.2...v4.6.3) (2025-10-18)
### Bug Fixes
* **complement-race:** balance AI speeds to match original implementation ([054f0c0](https://github.com/antialias/soroban-abacus-flashcards/commit/054f0c0d235dc2b0042a0f6af48840d23a4c5ff8))
## [4.6.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.6.1...v4.6.2) (2025-10-18)
### Bug Fixes
* **build:** resolve Docker build failures preventing deployment ([7801dbb](https://github.com/antialias/soroban-abacus-flashcards/commit/7801dbb25fb0a33429c70f11294264f7238ce7a4))
## [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)

View File

@@ -17,15 +17,16 @@ interface CircularTrackProps {
export function CircularTrack({ playerProgress, playerLap, aiRacers, aiLaps }: CircularTrackProps) {
const { state, dispatch } = useComplementRace()
const { players } = useGameMode()
const { players, activePlayers } = useGameMode()
const { profile: _profile } = useUserProfile()
const { playSound } = useSoundEffects()
const [celebrationCooldown, setCelebrationCooldown] = useState<Set<string>>(new Set())
// Get the first active player's emoji
const activePlayers = Array.from(players.values()).filter((p) => p.id)
const firstActivePlayer = activePlayers[0]
const playerEmoji = firstActivePlayer?.emoji ?? '👤'
// Get the current user's active local players (consistent with navbar pattern)
const activeLocalPlayers = Array.from(activePlayers)
.map((id) => players.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false)
const playerEmoji = activeLocalPlayers[0]?.emoji ?? '👤'
const [dimensions, setDimensions] = useState({ width: 600, height: 400 })
// Update dimensions on mount and resize

View File

@@ -20,13 +20,14 @@ export function LinearTrack({
showFinishLine = true,
}: LinearTrackProps) {
const { state, dispatch } = useComplementRace()
const { players } = useGameMode()
const { players, activePlayers } = useGameMode()
const { profile: _profile } = useUserProfile()
// Get the first active player's emoji
const activePlayers = Array.from(players.values()).filter((p) => p.id)
const firstActivePlayer = activePlayers[0]
const playerEmoji = firstActivePlayer?.emoji ?? '👤'
// Get the current user's active local players (consistent with navbar pattern)
const activeLocalPlayers = Array.from(activePlayers)
.map((id) => players.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined && p.isLocal !== false)
const playerEmoji = activeLocalPlayers[0]?.emoji ?? '👤'
// Position calculation: leftPercent = Math.min(98, (progress / raceGoal) * 96 + 2)
// 2% minimum (start), 98% maximum (near finish), 96% range for race
@@ -110,7 +111,7 @@ export function LinearTrack({
position: 'absolute',
left: `${playerPosition}%`,
top: '50%',
transform: 'translate(-50%, -50%)',
transform: 'translate(-50%, -50%) scaleX(-1)',
fontSize: '32px',
transition: 'left 0.3s ease-out',
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))',
@@ -132,7 +133,7 @@ export function LinearTrack({
position: 'absolute',
left: `${aiPosition}%`,
top: `${35 + index * 15}%`,
transform: 'translate(-50%, -50%)',
transform: 'translate(-50%, -50%) scaleX(-1)',
fontSize: '28px',
transition: 'left 0.2s linear',
filter: 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))',
@@ -141,10 +142,16 @@ export function LinearTrack({
>
{racer.icon}
{activeBubble && (
<SpeechBubble
message={activeBubble}
onHide={() => dispatch({ type: 'CLEAR_AI_COMMENT', racerId: racer.id })}
/>
<div
style={{
transform: 'scaleX(-1)', // Counter-flip bubble to make text readable
}}
>
<SpeechBubble
message={activeBubble}
onHide={() => dispatch({ type: 'CLEAR_AI_COMMENT', racerId: racer.id })}
/>
</div>
)}
</div>
)

View File

@@ -2,7 +2,7 @@
import { PageWithNav } from '@/components/PageWithNav'
import { ComplementRaceGame } from './components/ComplementRaceGame'
import { ComplementRaceProvider } from './context/ComplementRaceContext'
import { ComplementRaceProvider } from '@/arcade-games/complement-race/Provider'
export default function ComplementRacePage() {
return (

View File

@@ -2,7 +2,7 @@
import { PageWithNav } from '@/components/PageWithNav'
import { ComplementRaceGame } from '../components/ComplementRaceGame'
import { ComplementRaceProvider } from '../context/ComplementRaceContext'
import { ComplementRaceProvider } from '@/arcade-games/complement-race/Provider'
export default function PracticeModePage() {
return (

View File

@@ -2,7 +2,7 @@
import { PageWithNav } from '@/components/PageWithNav'
import { ComplementRaceGame } from '../components/ComplementRaceGame'
import { ComplementRaceProvider } from '../context/ComplementRaceContext'
import { ComplementRaceProvider } from '@/arcade-games/complement-race/Provider'
export default function SprintModePage() {
return (

View File

@@ -2,7 +2,7 @@
import { PageWithNav } from '@/components/PageWithNav'
import { ComplementRaceGame } from '../components/ComplementRaceGame'
import { ComplementRaceProvider } from '../context/ComplementRaceContext'
import { ComplementRaceProvider } from '@/arcade-games/complement-race/Provider'
export default function SurvivalModePage() {
return (

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)
@@ -387,18 +400,13 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
// Race mechanics
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,
})),
speedMultiplier:
multiplayerState.config.style === 'practice'
? 0.7
: multiplayerState.config.style === 'sprint'
? 0.9
: 1.0, // Base speed multipliers by mode
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 +433,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 +460,43 @@ 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 = ['Swift AI', 'Math Bot', 'Speed Demon', 'Brain Bot']
const personalities: Array<'competitive' | 'analytical'> = ['competitive', 'analytical']
const newAI = []
for (let i = 0; i < Math.min(count, aiNames.length); i++) {
// Use original balanced speeds: 0.32 for Swift AI, 0.2 for Math Bot
const baseSpeed = i === 0 ? 0.32 : 0.2
newAI.push({
id: `ai-${i}`,
name: aiNames[i],
personality: personalities[i % personalities.length] as 'competitive' | 'analytical',
position: 0,
speed: baseSpeed, // Balanced speed from original single-player version
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 +810,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

@@ -205,12 +205,6 @@ export class ComplementRaceValidator
}
}
// Generate AI opponents if enabled
const aiOpponents =
state.config.enableAI && state.config.aiOpponentCount > 0
? this.generateAIOpponents(state.config.aiOpponentCount)
: []
const newState: ComplementRaceState = {
...state,
config: updatedConfig,
@@ -224,7 +218,7 @@ export class ComplementRaceValidator
routeStartTime: state.config.style === 'sprint' ? Date.now() : null,
raceStartTime: Date.now(), // Race starts immediately
gameStartTime: Date.now(),
aiOpponents,
aiOpponents: [], // AI handled client-side
}
return { valid: true, newState }
@@ -363,15 +357,6 @@ export class ComplementRaceValidator
},
}
// Update AI opponents (make them progress at their speed)
if (state.config.enableAI && state.aiOpponents.length > 0) {
newState.aiOpponents = this.updateAIOpponents(
state.aiOpponents,
state.config.style,
state.config.raceGoal
)
}
// Check win conditions
const winner = this.checkWinCondition(newState)
if (winner) {
@@ -779,66 +764,6 @@ export class ComplementRaceValidator
return passengers
}
private generateAIOpponents(count: number): Array<{
id: string
name: string
personality: 'competitive' | 'analytical'
position: number
speed: number
lastComment: string | null
lastCommentTime: number
}> {
const aiNames = ['Robo-Racer', 'Calculator', 'Speed Demon', 'Brain Bot']
const personalities: Array<'competitive' | 'analytical'> = ['competitive', 'analytical']
const opponents = []
for (let i = 0; i < Math.min(count, aiNames.length); i++) {
opponents.push({
id: `ai-${i}`,
name: aiNames[i],
personality: personalities[i % personalities.length],
position: 0,
speed: 0.8 + Math.random() * 0.4, // Speed multiplier 0.8-1.2
lastComment: null,
lastCommentTime: 0,
})
}
return opponents
}
private updateAIOpponents(
opponents: Array<{
id: string
name: string
personality: 'competitive' | 'analytical'
position: number
speed: number
lastComment: string | null
lastCommentTime: number
}>,
gameStyle: 'practice' | 'sprint' | 'survival',
raceGoal: number
) {
return opponents.map((opponent) => {
let newPosition = opponent.position
if (gameStyle === 'practice') {
// AI moves forward based on their speed (simulates answering questions)
newPosition = Math.min(100, opponent.position + (100 / raceGoal) * opponent.speed)
} else if (gameStyle === 'survival') {
// AI always moves forward
newPosition = opponent.position + 4 * opponent.speed
}
// Sprint mode: AI doesn't participate (train journey is single-player focused)
return {
...opponent,
position: newPosition,
}
})
}
/**
* Calculate the maximum number of passengers that will be on the train
* concurrently at any given moment during the route
@@ -895,18 +820,12 @@ export class ComplementRaceValidator
// Practice mode: First to reach goal
if (config.style === 'practice') {
// Check human players
for (const [playerId, player] of Object.entries(players)) {
if (player.correctAnswers >= config.raceGoal) {
return playerId
}
}
// Check AI opponents
for (const ai of state.aiOpponents) {
if (ai.position >= 100) {
return ai.id
}
}
// AI wins handled client-side via useAIRacers hook
}
// Sprint mode: Check route-based, score-based, or time-based win conditions
@@ -955,25 +874,17 @@ export class ComplementRaceValidator
if (config.style === 'survival' && config.timeLimit) {
const elapsed = state.raceStartTime ? (Date.now() - state.raceStartTime) / 1000 : 0
if (elapsed >= config.timeLimit) {
// Find player or AI with highest position (most laps)
// Find player with highest position (most laps)
let maxPosition = 0
let winner: string | null = null
// Check human players
for (const [playerId, player] of Object.entries(players)) {
if (player.position > maxPosition) {
maxPosition = player.position
winner = playerId
}
}
// Check AI opponents
for (const ai of state.aiOpponents) {
if (ai.position > maxPosition) {
maxPosition = ai.position
winner = ai.id
}
}
// AI wins handled client-side via useAIRacers hook
return winner
}

View File

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

View File

@@ -22,6 +22,7 @@
"python-shell": "^5.0.0"
},
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -29,6 +29,7 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/minimatch": "^6.0.0",
"@types/node": "^20.0.0",
"tsup": "^7.0.0",
"typescript": "^5.0.0",

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

6
pnpm-lock.yaml generated
View File

@@ -402,6 +402,9 @@ importers:
specifier: ^5.0.0
version: 5.0.0
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19
@@ -417,6 +420,9 @@ importers:
packages/core/client/typescript:
devDependencies:
'@types/minimatch':
specifier: ^6.0.0
version: 6.0.0
'@types/node':
specifier: ^20.0.0
version: 20.19.19