Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0541c115c5 | ||
|
|
325e07de59 | ||
|
|
03262dbf40 | ||
|
|
d8fdfeef74 | ||
|
|
005d945ca8 | ||
|
|
a6c20aab3b |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -1,3 +1,24 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **complement-race:** add infinite win condition for Steam Sprint mode ([d8fdfee](https://github.com/antialias/soroban-abacus-flashcards/commit/d8fdfeef74a5d3bb9684254af1c9d64d264b46ad))
|
||||
|
||||
## [4.4.15](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.14...v4.4.15) (2025-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** track previous position to detect route threshold crossing ([a6c20aa](https://github.com/antialias/soroban-abacus-flashcards/commit/a6c20aab3b245d9893808d188d16a35ab80cfca9))
|
||||
|
||||
## [4.4.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.4.13...v4.4.14) (2025-10-18)
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ export function useSteamJourney() {
|
||||
const routeExitThresholdRef = useRef<number>(107) // Default for 1 car: 100 + 7
|
||||
const missedPassengersRef = useRef<Set<string>>(new Set()) // Track which passengers have been logged as missed
|
||||
const pendingBoardingRef = useRef<Set<string>>(new Set()) // Track passengers with pending boarding requests across frames
|
||||
const previousTrainPositionRef = useRef<number>(0) // Track previous position to detect threshold crossings
|
||||
|
||||
// Initialize game start time
|
||||
useEffect(() => {
|
||||
@@ -78,6 +79,7 @@ export function useSteamJourney() {
|
||||
useEffect(() => {
|
||||
pendingBoardingRef.current.clear()
|
||||
missedPassengersRef.current.clear()
|
||||
previousTrainPositionRef.current = 0 // Reset previous position for new route
|
||||
}, [state.currentRoute])
|
||||
|
||||
// Momentum decay and position update loop
|
||||
@@ -292,10 +294,11 @@ export function useSteamJourney() {
|
||||
|
||||
// Check for route completion (entire train exits tunnel)
|
||||
const ENTIRE_TRAIN_EXIT_THRESHOLD = routeExitThresholdRef.current
|
||||
const previousPosition = previousTrainPositionRef.current
|
||||
|
||||
if (
|
||||
trainPosition >= ENTIRE_TRAIN_EXIT_THRESHOLD &&
|
||||
state.trainPosition < ENTIRE_TRAIN_EXIT_THRESHOLD
|
||||
previousPosition < ENTIRE_TRAIN_EXIT_THRESHOLD
|
||||
) {
|
||||
// Play celebration whistle
|
||||
playSound('train_whistle', 0.6)
|
||||
@@ -305,6 +308,9 @@ export function useSteamJourney() {
|
||||
|
||||
// Auto-advance to next route
|
||||
const nextRoute = state.currentRoute + 1
|
||||
console.log(
|
||||
`🏁 ROUTE COMPLETE: Train crossed exit threshold (${trainPosition.toFixed(1)} >= ${ENTIRE_TRAIN_EXIT_THRESHOLD}). Advancing to Route ${nextRoute}`
|
||||
)
|
||||
dispatch({
|
||||
type: 'START_NEW_ROUTE',
|
||||
routeNumber: nextRoute,
|
||||
@@ -313,6 +319,9 @@ export function useSteamJourney() {
|
||||
|
||||
// Note: New passengers will be generated by the server when it handles START_NEW_ROUTE
|
||||
}
|
||||
|
||||
// Update previous position for next frame
|
||||
previousTrainPositionRef.current = trainPosition
|
||||
}, UPDATE_INTERVAL)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
|
||||
@@ -205,6 +205,12 @@ 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,
|
||||
@@ -218,6 +224,7 @@ export class ComplementRaceValidator
|
||||
routeStartTime: state.config.style === 'sprint' ? Date.now() : null,
|
||||
raceStartTime: Date.now(), // Race starts immediately
|
||||
gameStartTime: Date.now(),
|
||||
aiOpponents,
|
||||
}
|
||||
|
||||
return { valid: true, newState }
|
||||
@@ -356,6 +363,15 @@ 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) {
|
||||
@@ -763,6 +779,66 @@ 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
|
||||
@@ -812,13 +888,25 @@ export class ComplementRaceValidator
|
||||
private checkWinCondition(state: ComplementRaceState): string | null {
|
||||
const { config, players } = state
|
||||
|
||||
// Infinite mode: Never end the game
|
||||
if (config.winCondition === 'infinite') {
|
||||
return null
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sprint mode: Check route-based, score-based, or time-based win conditions
|
||||
@@ -867,15 +955,26 @@ 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 with highest position (most laps)
|
||||
// Find player or AI 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
|
||||
}
|
||||
}
|
||||
|
||||
return winner
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const defaultConfig: ComplementRaceConfig = {
|
||||
passengerCount: 6,
|
||||
maxConcurrentPassengers: 3,
|
||||
raceGoal: 20,
|
||||
winCondition: 'route-based',
|
||||
winCondition: 'infinite', // Sprint mode is infinite by default (Steam Sprint)
|
||||
routeCount: 3,
|
||||
targetScore: 100,
|
||||
timeLimit: 300,
|
||||
|
||||
@@ -89,7 +89,7 @@ export interface ComplementRaceGameConfig {
|
||||
raceGoal: number // questions to win practice mode (default 20)
|
||||
|
||||
// Win Conditions
|
||||
winCondition: 'route-based' | 'score-based' | 'time-based'
|
||||
winCondition: 'route-based' | 'score-based' | 'time-based' | 'infinite'
|
||||
targetScore?: number // for score-based (e.g., 100)
|
||||
timeLimit?: number // for time-based (e.g., 300 seconds)
|
||||
routeCount?: number // for route-based (e.g., 3 routes)
|
||||
@@ -171,7 +171,7 @@ export const DEFAULT_COMPLEMENT_RACE_CONFIG: ComplementRaceGameConfig = {
|
||||
raceGoal: 20,
|
||||
|
||||
// Win conditions
|
||||
winCondition: 'route-based',
|
||||
winCondition: 'infinite', // Sprint mode is infinite by default (Steam Sprint)
|
||||
routeCount: 3,
|
||||
targetScore: 100,
|
||||
timeLimit: 300,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.4.14",
|
||||
"version": "4.6.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user