team rating
This commit is contained in:
@@ -21,10 +21,17 @@ import type { IProtestRepository } from '@core/racing/domain/repositories/IProte
|
||||
import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenaltyRepository';
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { IDriverStatsRepository } from '@core/racing/domain/repositories/IDriverStatsRepository';
|
||||
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
|
||||
import { createRacingSeed } from './racing/RacingSeed';
|
||||
import { seedId } from './racing/SeedIdHelper';
|
||||
import { DriverStatsStore } from '@adapters/racing/services/DriverStatsStore';
|
||||
import { TeamStatsStore } from '@adapters/racing/services/TeamStatsStore';
|
||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||
import { Result } from '@core/racing/domain/entities/result/Result';
|
||||
import { Standing } from '@core/racing/domain/entities/Standing';
|
||||
import { Team } from '@core/racing/domain/entities/Team';
|
||||
import type { DriverStats } from '@core/racing/application/use-cases/IDriverStatsUseCase';
|
||||
import type { TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||
|
||||
export type RacingSeedDependencies = {
|
||||
driverRepository: IDriverRepository;
|
||||
@@ -47,6 +54,9 @@ export type RacingSeedDependencies = {
|
||||
sponsorRepository: ISponsorRepository;
|
||||
feedRepository: IFeedRepository;
|
||||
socialGraphRepository: ISocialGraphRepository;
|
||||
driverStatsRepository: IDriverStatsRepository;
|
||||
teamStatsRepository: ITeamStatsRepository;
|
||||
mediaRepository: IMediaRepository;
|
||||
};
|
||||
|
||||
export class SeedRacingData {
|
||||
@@ -92,19 +102,12 @@ export class SeedRacingData {
|
||||
driverCount: 150 // Expanded from 100 to 150
|
||||
});
|
||||
|
||||
// Populate the driver stats store for the InMemoryDriverStatsService
|
||||
const driverStatsStore = DriverStatsStore.getInstance();
|
||||
driverStatsStore.clear(); // Clear any existing stats
|
||||
driverStatsStore.loadStats(seed.driverStats);
|
||||
// Clear existing stats repositories
|
||||
await this.seedDeps.driverStatsRepository.clear();
|
||||
await this.seedDeps.teamStatsRepository.clear();
|
||||
await this.seedDeps.mediaRepository.clear();
|
||||
|
||||
this.logger.info(`[Bootstrap] Loaded driver stats for ${seed.driverStats.size} drivers`);
|
||||
|
||||
// Populate the team stats store for the AllTeamsPresenter
|
||||
const teamStatsStore = TeamStatsStore.getInstance();
|
||||
teamStatsStore.clear(); // Clear any existing stats
|
||||
teamStatsStore.loadStats(seed.teamStats);
|
||||
|
||||
this.logger.info(`[Bootstrap] Loaded team stats for ${seed.teamStats.size} teams`);
|
||||
this.logger.info('[Bootstrap] Cleared existing stats and media repositories');
|
||||
|
||||
let sponsorshipRequestsSeededViaRepo = false;
|
||||
const seedableSponsorshipRequests = this.seedDeps
|
||||
@@ -304,11 +307,236 @@ export class SeedRacingData {
|
||||
});
|
||||
}
|
||||
|
||||
// Compute and store driver stats from real data
|
||||
await this.computeAndStoreDriverStats();
|
||||
|
||||
// Compute and store team stats from real data
|
||||
await this.computeAndStoreTeamStats();
|
||||
|
||||
// Seed media assets (logos, images)
|
||||
await this.seedMediaAssets(seed);
|
||||
|
||||
this.logger.info(
|
||||
`[Bootstrap] Seeded racing data: drivers=${seed.drivers.length}, leagues=${seed.leagues.length}, races=${seed.races.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async computeAndStoreDriverStats(): Promise<void> {
|
||||
const drivers = await this.seedDeps.driverRepository.findAll();
|
||||
const standings = await this.seedDeps.standingRepository.findAll();
|
||||
const results = await this.seedDeps.resultRepository.findAll();
|
||||
|
||||
this.logger.info(`[Bootstrap] Computing stats for ${drivers.length} drivers from ${standings.length} standings and ${results.length} results`);
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverResults = results.filter(r => r.driverId.toString() === driver.id);
|
||||
const driverStandings = standings.filter(s => s.driverId.toString() === driver.id);
|
||||
|
||||
if (driverResults.length === 0) continue;
|
||||
|
||||
const stats = this.calculateDriverStats(driver, driverResults, driverStandings);
|
||||
await this.seedDeps.driverStatsRepository.saveDriverStats(driver.id, stats);
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Computed and stored stats for ${drivers.length} drivers`);
|
||||
}
|
||||
|
||||
private calculateDriverStats(driver: Driver, results: Result[], standings: Standing[]): DriverStats {
|
||||
const wins = results.filter(r => r.position.toNumber() === 1).length;
|
||||
const podiums = results.filter(r => r.position.toNumber() <= 3).length;
|
||||
const dnfs = results.filter(r => r.position.toNumber() > 20).length;
|
||||
const totalRaces = results.length;
|
||||
|
||||
const positions = results.map(r => r.position.toNumber());
|
||||
const avgFinish = positions.reduce((sum, pos) => sum + pos, 0) / totalRaces;
|
||||
const bestFinish = Math.min(...positions);
|
||||
const worstFinish = Math.max(...positions);
|
||||
|
||||
// Calculate rating based on performance
|
||||
let rating = 1000;
|
||||
const driverStanding = standings.find(s => s.driverId.toString() === driver.id);
|
||||
if (driverStanding) {
|
||||
const pointsBonus = driverStanding.points.toNumber() * 2;
|
||||
const positionBonus = Math.max(0, 50 - (driverStanding.position.toNumber() * 2));
|
||||
const winBonus = driverStanding.wins * 100;
|
||||
rating = Math.round(1000 + pointsBonus + positionBonus + winBonus);
|
||||
} else {
|
||||
const performanceBonus = ((totalRaces - wins) * 5) + ((totalRaces - podiums) * 2);
|
||||
rating = Math.round(1000 + (wins * 100) + (podiums * 50) - performanceBonus);
|
||||
}
|
||||
|
||||
// Calculate consistency
|
||||
const avgPosition = avgFinish;
|
||||
const variance = positions.reduce((sum, pos) => sum + Math.pow(pos - avgPosition, 2), 0) / totalRaces;
|
||||
const consistency = Math.round(Math.max(0, 100 - (variance * 2)));
|
||||
|
||||
// Safety rating (based on incidents)
|
||||
const totalIncidents = results.reduce((sum, r) => sum + r.incidents.toNumber(), 0);
|
||||
const safetyRating = Math.round(Math.max(0, 100 - (totalIncidents / totalRaces)));
|
||||
|
||||
// Sportsmanship rating (placeholder)
|
||||
const sportsmanshipRating = 4.5;
|
||||
|
||||
// Experience level
|
||||
const experienceLevel = this.determineExperienceLevel(totalRaces);
|
||||
|
||||
// Overall rank
|
||||
const overallRank = driverStanding ? driverStanding.position.toNumber() : null;
|
||||
|
||||
return {
|
||||
rating,
|
||||
safetyRating,
|
||||
sportsmanshipRating,
|
||||
totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
dnfs,
|
||||
avgFinish: Math.round(avgFinish * 10) / 10,
|
||||
bestFinish,
|
||||
worstFinish,
|
||||
consistency,
|
||||
experienceLevel,
|
||||
overallRank
|
||||
};
|
||||
}
|
||||
|
||||
private async computeAndStoreTeamStats(): Promise<void> {
|
||||
const teams = await this.seedDeps.teamRepository.findAll();
|
||||
const results = await this.seedDeps.resultRepository.findAll();
|
||||
const drivers = await this.seedDeps.driverRepository.findAll();
|
||||
|
||||
this.logger.info(`[Bootstrap] Computing stats for ${teams.length} teams`);
|
||||
|
||||
for (const team of teams) {
|
||||
// Get team members using the correct method
|
||||
const teamMemberships = await this.seedDeps.teamMembershipRepository.getTeamMembers(team.id);
|
||||
const teamMemberIds = teamMemberships.map(m => m.driverId.toString());
|
||||
|
||||
// Get results for team members
|
||||
const teamResults = results.filter(r => teamMemberIds.includes(r.driverId.toString()));
|
||||
|
||||
// Get team drivers for name resolution
|
||||
const teamDrivers = drivers.filter(d => teamMemberIds.includes(d.id));
|
||||
|
||||
const stats = this.calculateTeamStats(team, teamResults, teamDrivers);
|
||||
await this.seedDeps.teamStatsRepository.saveTeamStats(team.id, stats);
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Computed and stored stats for ${teams.length} teams`);
|
||||
}
|
||||
|
||||
private calculateTeamStats(team: Team, results: Result[], drivers: Driver[]): TeamStats {
|
||||
const wins = results.filter(r => r.position.toNumber() === 1).length;
|
||||
const totalRaces = results.length;
|
||||
|
||||
// Calculate rating
|
||||
const baseRating = 1000;
|
||||
const winBonus = wins * 50;
|
||||
const raceBonus = Math.min(totalRaces * 5, 200);
|
||||
const rating = Math.round(baseRating + winBonus + raceBonus);
|
||||
|
||||
// Determine performance level
|
||||
let performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
if (wins >= 20) performanceLevel = 'pro';
|
||||
else if (wins >= 10) performanceLevel = 'advanced';
|
||||
else if (wins >= 5) performanceLevel = 'intermediate';
|
||||
else performanceLevel = 'beginner';
|
||||
|
||||
// Determine specialization (based on race types - simplified)
|
||||
const specialization: 'endurance' | 'sprint' | 'mixed' = 'mixed';
|
||||
|
||||
// Get region from team name or first driver
|
||||
const region = drivers.length > 0 && drivers[0] ? drivers[0].country.toString() : 'International';
|
||||
|
||||
// Languages (based on drivers)
|
||||
const languages = Array.from(new Set(drivers.map(d => {
|
||||
// Simplified language mapping based on country
|
||||
const country = d.country.toString().toLowerCase();
|
||||
if (country === 'us' || country === 'gb' || country === 'ca') return 'en';
|
||||
if (country === 'de') return 'de';
|
||||
if (country === 'fr') return 'fr';
|
||||
if (country === 'es') return 'es';
|
||||
if (country === 'it') return 'it';
|
||||
if (country === 'jp') return 'ja';
|
||||
return 'en';
|
||||
})));
|
||||
|
||||
return {
|
||||
logoUrl: `https://api.gridpilot.io/media/team/${team.id}/logo.png`,
|
||||
performanceLevel,
|
||||
specialization,
|
||||
region,
|
||||
languages,
|
||||
totalWins: wins,
|
||||
totalRaces,
|
||||
rating
|
||||
};
|
||||
}
|
||||
|
||||
private determineExperienceLevel(totalRaces: number): string {
|
||||
if (totalRaces >= 100) return 'Veteran';
|
||||
if (totalRaces >= 50) return 'Experienced';
|
||||
if (totalRaces >= 20) return 'Intermediate';
|
||||
if (totalRaces >= 10) return 'Rookie';
|
||||
return 'Beginner';
|
||||
}
|
||||
|
||||
private async seedMediaAssets(seed: any): Promise<void> {
|
||||
// Seed driver avatars
|
||||
for (const driver of seed.drivers) {
|
||||
const avatarUrl = `https://api.gridpilot.io/media/driver/${driver.id}/avatar.png`;
|
||||
|
||||
// Type assertion to access the helper method
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setDriverAvatar) {
|
||||
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed team logos
|
||||
for (const team of seed.teams) {
|
||||
const logoUrl = `https://api.gridpilot.io/media/team/${team.id}/logo.png`;
|
||||
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setTeamLogo) {
|
||||
mediaRepo.setTeamLogo(team.id, logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed track images
|
||||
for (const track of seed.tracks || []) {
|
||||
const trackImageUrl = `https://api.gridpilot.io/media/track/${track.id}/image.png`;
|
||||
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setTrackImage) {
|
||||
mediaRepo.setTrackImage(track.id, trackImageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed category icons (if categories exist)
|
||||
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
|
||||
for (const category of categories) {
|
||||
const iconUrl = `https://api.gridpilot.io/media/category/${category}/icon.png`;
|
||||
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setCategoryIcon) {
|
||||
mediaRepo.setCategoryIcon(category, iconUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed sponsor logos
|
||||
for (const sponsor of seed.sponsors || []) {
|
||||
const logoUrl = `https://api.gridpilot.io/media/sponsor/${sponsor.id}/logo.png`;
|
||||
|
||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||
if (mediaRepo.setSponsorLogo) {
|
||||
mediaRepo.setSponsorLogo(sponsor.id, logoUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Seeded media assets for ${seed.drivers.length} drivers, ${seed.teams.length} teams`);
|
||||
}
|
||||
|
||||
private async clearExistingRacingData(): Promise<void> {
|
||||
// Get all existing drivers
|
||||
const drivers = await this.seedDeps.driverRepository.findAll();
|
||||
|
||||
@@ -19,39 +19,35 @@ export class RacingRaceFactory {
|
||||
const races: Race[] = [];
|
||||
|
||||
// Create races with systematic coverage of different statuses and scenarios
|
||||
const statuses: Array<'scheduled' | 'running' | 'completed' | 'cancelled'> = ['scheduled', 'running', 'completed', 'cancelled'];
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
for (let i = 1; i <= 500; i++) {
|
||||
const leagueId = leagueIds[(i - 1) % leagueIds.length] ?? demoLeagueId;
|
||||
const trackId = trackIds[(i - 1) % trackIds.length]!;
|
||||
const track = tracks.find(t => t.id === trackId)!;
|
||||
|
||||
// Determine status systematically to ensure coverage
|
||||
// Determine status systematically to ensure good coverage
|
||||
let status: 'scheduled' | 'running' | 'completed' | 'cancelled';
|
||||
let scheduledAt: Date;
|
||||
|
||||
if (i <= 4) {
|
||||
// First 4 races: one of each status
|
||||
status = statuses[i - 1]!;
|
||||
scheduledAt = this.addDays(this.baseDate, i <= 2 ? -35 + i : 1 + (i - 2) * 2);
|
||||
} else if (i <= 10) {
|
||||
// Next 6: completed races
|
||||
status = 'completed';
|
||||
scheduledAt = this.addDays(this.baseDate, -35 + i);
|
||||
} else if (i <= 15) {
|
||||
// Next 5: scheduled future races
|
||||
status = 'scheduled';
|
||||
scheduledAt = this.addDays(this.baseDate, 1 + (i - 10) * 3);
|
||||
} else if (i <= 20) {
|
||||
// Next 5: cancelled races
|
||||
// Use modulo to create a balanced distribution across 500 races
|
||||
const statusMod = i % 20; // 20 different patterns
|
||||
|
||||
if (statusMod === 1 || statusMod === 2 || statusMod === 3) {
|
||||
// 15% running (3 out of 20)
|
||||
status = 'running';
|
||||
scheduledAt = this.addDays(this.baseDate, -1 + (statusMod * 0.5)); // Recent past/current
|
||||
} else if (statusMod === 4 || statusMod === 5 || statusMod === 6 || statusMod === 7) {
|
||||
// 20% cancelled (4 out of 20)
|
||||
status = 'cancelled';
|
||||
scheduledAt = this.addDays(this.baseDate, -20 + (i - 15));
|
||||
scheduledAt = this.addDays(this.baseDate, -30 + (statusMod * 2));
|
||||
} else if (statusMod === 8 || statusMod === 9 || statusMod === 10 || statusMod === 11 || statusMod === 12) {
|
||||
// 25% completed (5 out of 20)
|
||||
status = 'completed';
|
||||
scheduledAt = this.addDays(this.baseDate, -50 + (statusMod * 3));
|
||||
} else {
|
||||
// Rest: mix of scheduled and completed
|
||||
status = i % 3 === 0 ? 'completed' : 'scheduled';
|
||||
scheduledAt = status === 'completed'
|
||||
? this.addDays(this.baseDate, -10 + (i - 20))
|
||||
: this.addDays(this.baseDate, 5 + (i - 20) * 2);
|
||||
// 40% scheduled (8 out of 20)
|
||||
status = 'scheduled';
|
||||
scheduledAt = this.addDays(this.baseDate, 1 + ((statusMod - 13) * 2));
|
||||
}
|
||||
|
||||
const base = {
|
||||
@@ -63,58 +59,48 @@ export class RacingRaceFactory {
|
||||
car: cars[(i - 1) % cars.length]!,
|
||||
};
|
||||
|
||||
// Special case for running race
|
||||
// Create race based on status with appropriate data
|
||||
if (status === 'running') {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'running',
|
||||
strengthOfField: 45 + (i % 50), // Valid SOF: 0-100
|
||||
registeredCount: 12 + (i % 5), // Varying registration counts
|
||||
maxParticipants: 24, // Ensure max is set
|
||||
strengthOfField: 40 + (i % 60), // Valid SOF: 0-100
|
||||
registeredCount: 10 + (i % 15), // Varying registration counts
|
||||
maxParticipants: 20 + (i % 8), // 20-28 participants
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add varying SOF and registration counts for completed races
|
||||
if (status === 'completed') {
|
||||
} else if (status === 'completed') {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'completed',
|
||||
strengthOfField: 35 + (i % 60), // Valid SOF: 0-100
|
||||
registeredCount: 8 + (i % 8),
|
||||
maxParticipants: 20, // Ensure max is set
|
||||
strengthOfField: 30 + (i % 70), // Valid SOF: 0-100
|
||||
registeredCount: 6 + (i % 12),
|
||||
maxParticipants: 16 + (i % 10),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Scheduled races with some having registration data
|
||||
if (status === 'scheduled') {
|
||||
const hasRegistrations = i % 4 !== 0; // 75% have registrations
|
||||
} else if (status === 'scheduled') {
|
||||
const hasRegistrations = i % 3 !== 0; // 66% have registrations
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'scheduled',
|
||||
...(hasRegistrations && {
|
||||
strengthOfField: 40 + (i % 55), // Valid SOF: 0-100
|
||||
registeredCount: 5 + (i % 10),
|
||||
maxParticipants: 16 + (i % 10), // Ensure max is set and reasonable
|
||||
strengthOfField: 35 + (i % 65), // Valid SOF: 0-100
|
||||
registeredCount: 4 + (i % 12),
|
||||
maxParticipants: 14 + (i % 12),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
} else if (status === 'cancelled') {
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'cancelled',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Cancelled races
|
||||
races.push(
|
||||
Race.create({
|
||||
...base,
|
||||
status: 'cancelled',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return races;
|
||||
|
||||
Reference in New Issue
Block a user