UNPKG

@lovebowls/leagueelements

Version:

League Elements package for LoveBowls

376 lines (327 loc) 12.7 kB
// league-test-data.js - Test data for league element testing import { League, Team, Match } from '@lovebowls/leaguejs'; /** * Generate a unique ID for test purposes * @returns {string} A unique ID */ function generateUniqueId() { return `id-${Date.now()}-${Math.floor(Math.random() * 10000)}`; } /** * Generate a skill level for a team (0.1 to 1.0, where 1.0 is strongest) * @returns {number} Team skill level */ function generateTeamSkill() { // Generate skill levels with some variation but not too extreme // Most teams will be between 0.3 and 0.8, with occasional very strong/weak teams const random = Math.random(); if (random < 0.1) return 0.1 + Math.random() * 0.2; // 10% chance of weak team (0.1-0.3) if (random > 0.9) return 0.8 + Math.random() * 0.2; // 10% chance of strong team (0.8-1.0) return 0.3 + Math.random() * 0.5; // 80% chance of average team (0.3-0.8) } /** * Calculate match result based on team skills with some randomness * @param {number} homeSkill - Home team skill level (0.1-1.0) * @param {number} awaySkill - Away team skill level (0.1-1.0) * @returns {Object} Match result with homeScore and awayScore */ function calculateSkillBasedResult(homeSkill, awaySkill) { // Home advantage factor (small boost for home team) const homeAdvantage = 0.1; const adjustedHomeSkill = Math.min(1.0, homeSkill + homeAdvantage); // Calculate relative strength difference const totalSkill = adjustedHomeSkill + awaySkill; const homeWinProbability = adjustedHomeSkill / totalSkill; // Add some randomness to make results less predictable const randomFactor = 0.3; // 30% randomness const skillFactor = 1 - randomFactor; const finalHomeWinProb = (homeWinProbability * skillFactor) + (Math.random() * randomFactor); // Generate base scores (typically 8-20 for bowls) const baseScore = 8 + Math.floor(Math.random() * 13); // 8-20 const scoreDifference = Math.floor(Math.random() * 8) + 1; // 1-8 point difference let homeScore, awayScore; if (finalHomeWinProb > 0.6) { // Home team wins homeScore = baseScore + scoreDifference; awayScore = baseScore; } else if (finalHomeWinProb < 0.4) { // Away team wins homeScore = baseScore; awayScore = baseScore + scoreDifference; } else { // Close match or draw if (Math.random() < 0.15) { // 15% chance of draw in close matches homeScore = awayScore = baseScore; } else { // Narrow win for one team const narrowDiff = Math.floor(Math.random() * 3) + 1; // 1-3 point difference if (Math.random() < 0.5) { homeScore = baseScore + narrowDiff; awayScore = baseScore; } else { homeScore = baseScore; awayScore = baseScore + narrowDiff; } } } return { homeScore, awayScore }; } /** * Generate a test match object with skill-based results * @param {Team} homeTeam - The home team object * @param {Team} awayTeam - The away team object * @param {Date} matchDate - The date for this match * @param {number} homeSkill - Home team skill level * @param {number} awaySkill - Away team skill level * @param {boolean} shouldHaveResult - Whether this match should have a result * @returns {Object} A test match object in the format expected by the system */ function generateTestMatch(homeTeam, awayTeam, matchDate, homeSkill, awaySkill, shouldHaveResult = true) { const matchData = { _id: `match-${generateUniqueId()}`, homeTeam: { _id: homeTeam._id, name: homeTeam.name }, awayTeam: { _id: awayTeam._id, name: awayTeam.name }, date: matchDate.toISOString().split('T')[0], // Format as YYYY-MM-DD string like the modal does createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; // Add result if this is a past match if (shouldHaveResult) { const result = calculateSkillBasedResult(homeSkill, awaySkill); matchData.result = { played: true, homeScore: result.homeScore, awayScore: result.awayScore, homePoints: result.homeScore > result.awayScore ? 2 : (result.homeScore === result.awayScore ? 1 : 0), awayPoints: result.awayScore > result.homeScore ? 2 : (result.homeScore === result.awayScore ? 1 : 0), rinkPointsUsed: false }; } else { // Explicitly set result to null for future matches matchData.result = null; } // Return the plain object instead of creating a Match instance return matchData; } /** * Generate test league data for testing using LeagueJS with skill-based results * @param {Array} lovebowlsTeams - Array of lovebowls teams to use * @param {number} [teamCount=4] - Number of teams to include (min 4, max 12) * @param {string} [leagueName='Test League'] - Name for the test league * @returns {League} A complete test league object */ export function generateTestLeagueData(lovebowlsTeams = [], teamCount = 4, leagueName = 'Test League') { // Ensure teamCount is within valid range teamCount = Math.max(4, Math.min(12, teamCount)); // Create league settings const settings = { pointsForWin: 3, pointsForDraw: 1, pointsForLoss: 0, promotionPositions: 1, relegationPositions: 1, timesTeamsPlayOther: 2, // Each team plays each other twice (home and away) rinkPoints: { enabled: Math.random() > 0.5, pointsPerRinkWin: 2, pointsPerRinkDraw: 1, defaultRinks: 4 } }; // Create league instance const league = new League({ _id: `league-${generateUniqueId()}`, name: leagueName, settings, teams: [], matches: [], createdAt: new Date(), updatedAt: new Date() }); // Add teams to the league with skill levels const teams = []; const teamSkills = new Map(); // Store skill levels for each team const lovebowlsTeamsToUse = lovebowlsTeams.slice(0, teamCount); // Add LoveBowls teams first lovebowlsTeamsToUse.forEach(teamData => { const team = new Team({ _id: teamData._id || `team-${generateUniqueId()}`, name: teamData.name || `Team ${teams.length + 1}`, shortName: teamData.shortName || `T${teams.length + 1}`, createdAt: new Date(), updatedAt: new Date() }); league.addTeam(team); teams.push(team); teamSkills.set(team._id, generateTeamSkill()); }); // Add remaining teams if needed for (let i = teams.length; i < teamCount; i++) { const team = new Team({ _id: `team-${generateUniqueId()}`, name: `Custom Team ${i + 1}`, shortName: `CT${i + 1}`, createdAt: new Date(), updatedAt: new Date() }); league.addTeam(team); teams.push(team); teamSkills.set(team._id, generateTeamSkill()); } // Generate match schedule with dates const today = new Date(); const matches = []; // Calculate total number of matches (each team plays each other twice) const totalMatches = teamCount * (teamCount - 1) * settings.timesTeamsPlayOther; // 80% of matches should be in the past with results, 20% in the future without results const pastMatchCount = Math.floor(totalMatches * 0.8); // Generate all possible match combinations let matchIndex = 0; const allMatchPairs = []; for (let round = 0; round < settings.timesTeamsPlayOther; round++) { for (let i = 0; i < teams.length; i++) { for (let j = 0; j < teams.length; j++) { if (i !== j) { allMatchPairs.push({ homeTeam: teams[i], awayTeam: teams[j], round: round + 1 }); } } } } // Shuffle the matches to create a more realistic schedule for (let i = allMatchPairs.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [allMatchPairs[i], allMatchPairs[j]] = [allMatchPairs[j], allMatchPairs[i]]; } // Generate matches with dates, ensuring no team plays twice on the same date const teamSchedules = new Map(); // Track when each team is playing const scheduledMatches = []; // Initialize team schedules teams.forEach(team => { teamSchedules.set(team._id, new Set()); }); // Helper function to find a valid date for a match function findValidMatchDate(homeTeamId, awayTeamId, isPastMatch, baseDate, maxAttempts = 50) { for (let attempt = 0; attempt < maxAttempts; attempt++) { let candidateDate; if (isPastMatch) { // Past matches: spread over the last 12 weeks with some randomness const daysRange = 12 * 7; // 12 weeks const daysAgo = Math.floor(Math.random() * daysRange); candidateDate = new Date(baseDate); candidateDate.setDate(baseDate.getDate() - daysAgo); } else { // Future matches: spread over the next 8 weeks with some randomness const daysRange = 8 * 7; // 8 weeks const daysAhead = Math.floor(Math.random() * daysRange) + 1; candidateDate = new Date(baseDate); candidateDate.setDate(baseDate.getDate() + daysAhead); } const dateKey = candidateDate.toISOString().split('T')[0]; const homeTeamSchedule = teamSchedules.get(homeTeamId); const awayTeamSchedule = teamSchedules.get(awayTeamId); // Check if either team already has a match on this date if (!homeTeamSchedule.has(dateKey) && !awayTeamSchedule.has(dateKey)) { // Mark both teams as busy on this date homeTeamSchedule.add(dateKey); awayTeamSchedule.add(dateKey); return candidateDate; } } // If we can't find a conflict-free date, fall back to a sequential approach // This ensures we always find a date even if the random approach fails let fallbackDate = new Date(baseDate); if (isPastMatch) { fallbackDate.setDate(baseDate.getDate() - (scheduledMatches.length * 2)); // Space out by 2 days } else { fallbackDate.setDate(baseDate.getDate() + (scheduledMatches.length * 2)); // Space out by 2 days } const dateKey = fallbackDate.toISOString().split('T')[0]; teamSchedules.get(homeTeamId).add(dateKey); teamSchedules.get(awayTeamId).add(dateKey); return fallbackDate; } // Schedule past matches first (they have priority for realistic dates) const pastMatches = allMatchPairs.slice(0, pastMatchCount); const futureMatches = allMatchPairs.slice(pastMatchCount); // Schedule past matches pastMatches.forEach((matchPair, index) => { const matchDate = findValidMatchDate( matchPair.homeTeam._id, matchPair.awayTeam._id, true, today ); const homeSkill = teamSkills.get(matchPair.homeTeam._id); const awaySkill = teamSkills.get(matchPair.awayTeam._id); const matchData = generateTestMatch( matchPair.homeTeam, matchPair.awayTeam, matchDate, homeSkill, awaySkill, true // isPastMatch ); scheduledMatches.push(matchData); }); // Schedule future matches futureMatches.forEach((matchPair, index) => { const matchDate = findValidMatchDate( matchPair.homeTeam._id, matchPair.awayTeam._id, false, today ); const homeSkill = teamSkills.get(matchPair.homeTeam._id); const awaySkill = teamSkills.get(matchPair.awayTeam._id); const matchData = generateTestMatch( matchPair.homeTeam, matchPair.awayTeam, matchDate, homeSkill, awaySkill, false // isPastMatch ); scheduledMatches.push(matchData); }); // Sort matches by date for a more realistic schedule scheduledMatches.sort((a, b) => new Date(a.date) - new Date(b.date)); // Add all scheduled matches to the league scheduledMatches.forEach(matchData => { league.matches.push(matchData); }); // Don't call initialiseFixtures as it might override our match data // league.initialiseFixtures(); return league; } /** * Generate multiple test leagues for testing * @param {Array} lovebowlsTeams - Array of lovebowls teams to use * @param {number} [leagueCount=3] - Number of leagues to generate (1-5) * @returns {Array<League>} An array of test league objects */ export function generateMultipleTestLeagues(lovebowlsTeams = [], leagueCount = 3) { const leagues = []; leagueCount = Math.max(1, Math.min(5, leagueCount)); // Ensure between 1-5 leagues for (let i = 0; i < leagueCount; i++) { const teamCount = Math.floor(Math.random() * 5) + 4; // 4-8 teams per league leagues.push( generateTestLeagueData( lovebowlsTeams, teamCount, `Test League ${i + 1}` ) ); } return leagues; }