Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0541c115c5 | ||
|
|
325e07de59 | ||
|
|
03262dbf40 | ||
|
|
d8fdfeef74 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -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.15",
|
||||
"version": "4.6.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user