UK/Irish Horse Racing Handicap Analyzer

Automated scoring system for UK/Irish handicap races with 8-factor analysis (T1-T8) and intelligent selection logic

Size

29.5 KB

Version

1.0.1

Created

Nov 7, 2025

Updated

about 1 month ago

1// ==UserScript==
2// @name		UK/Irish Horse Racing Handicap Analyzer
3// @description		Automated scoring system for UK/Irish handicap races with 8-factor analysis (T1-T8) and intelligent selection logic
4// @version		1.0.1
5// @match		https://*.racingpost.com/racecards/*
6// @match		https://www.racingpost.com/racecards/*
7// @icon		https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg
8// ==/UserScript==
9(function() {
10    'use strict';
11
12    console.log('UK/Irish Horse Racing Handicap Analyzer - Starting...');
13
14    // ========================================
15    // 1. CLASS CEILING THRESHOLD (CCT) LOOKUP
16    // ========================================
17    const CCT_FLAT = {
18        2: 110,
19        3: 95,
20        4: 85,
21        5: 75,
22        6: 65
23    };
24
25    const CCT_JUMPS = {
26        2: 140,
27        3: 135,
28        4: 120,
29        5: 100,
30        6: 80
31    };
32
33    // ========================================
34    // 2. UTILITY FUNCTIONS
35    // ========================================
36    
37    function debounce(func, wait) {
38        let timeout;
39        return function executedFunction(...args) {
40            const later = () => {
41                clearTimeout(timeout);
42                func(...args);
43            };
44            clearTimeout(timeout);
45            timeout = setTimeout(later, wait);
46        };
47    }
48
49    function parseOdds(oddsString) {
50        // Convert fractional odds to decimal
51        if (!oddsString || oddsString === 'SP') return null;
52        
53        if (oddsString.includes('/')) {
54            const [num, den] = oddsString.split('/').map(Number);
55            return (num / den) + 1;
56        }
57        return parseFloat(oddsString);
58    }
59
60    function parseWeight(weightElement) {
61        // Weight is in format "12-0" (12 stone 0 pounds)
62        // data-order-wgt contains total pounds
63        const totalPounds = parseInt(weightElement?.getAttribute('data-order-wgt') || '0');
64        return totalPounds;
65    }
66
67    function parseClass(classString) {
68        // Extract class number from "(Class 3)" format
69        const match = classString?.match(/Class\s+(\d+)/i);
70        return match ? parseInt(match[1]) : null;
71    }
72
73    function parseDistance(distanceString) {
74        // Parse distance like "2m4f" or "1m2f"
75        if (!distanceString) return 0;
76        
77        const milesMatch = distanceString.match(/(\d+)m/);
78        const furlongsMatch = distanceString.match(/(\d+)f/);
79        
80        const miles = milesMatch ? parseInt(milesMatch[1]) : 0;
81        const furlongs = furlongsMatch ? parseInt(furlongsMatch[1]) : 0;
82        
83        // Convert to furlongs (1 mile = 8 furlongs)
84        return (miles * 8) + furlongs;
85    }
86
87    function isLongFlatRace(raceType, distanceInFurlongs) {
88        // Long Flat Race is 1m2f+ (10+ furlongs)
89        return raceType === 'Flat' && distanceInFurlongs >= 10;
90    }
91
92    // ========================================
93    // 3. DATA EXTRACTION FUNCTIONS
94    // ========================================
95
96    function extractRaceData() {
97        console.log('Extracting race data...');
98        
99        const distanceEl = document.querySelector('[data-test-selector="RC-header__raceDistanceRound"]');
100        const titleEl = document.querySelector('[data-test-selector="RC-header__raceInstanceTitle"]');
101        const classEl = document.querySelector('[data-test-selector="RC-header__raceClass"]');
102        const agesEl = document.querySelector('[data-test-selector="RC-header__rpAges"]');
103        
104        const distanceText = distanceEl?.textContent?.trim() || '';
105        const title = titleEl?.textContent?.trim() || '';
106        const classText = classEl?.textContent?.trim() || '';
107        const agesText = agesEl?.textContent?.trim() || '';
108        
109        // Determine race type (Flat or Jumps)
110        const isJumps = title.toLowerCase().includes('chase') || 
111                       title.toLowerCase().includes('hurdle') || 
112                       title.toLowerCase().includes('national hunt');
113        
114        const raceType = isJumps ? 'Jumps' : 'Flat';
115        const raceClass = parseClass(classText);
116        const distanceInFurlongs = parseDistance(distanceText);
117        
118        // Extract course name from URL or page
119        const urlParts = window.location.pathname.split('/');
120        const course = urlParts[2] || 'Unknown';
121        
122        console.log('Race Data:', {
123            raceType,
124            raceClass,
125            distance: distanceText,
126            distanceInFurlongs,
127            course,
128            title
129        });
130        
131        return {
132            raceType,
133            raceClass,
134            distance: distanceText,
135            distanceInFurlongs,
136            course,
137            title,
138            agesText
139        };
140    }
141
142    async function extractTrainerStats(trainerName) {
143        // Try to find trainer stats in the stats accordion
144        // This might require expanding the stats section
145        const statsAccordion = document.querySelector('.RC-stats');
146        
147        if (statsAccordion) {
148            const trainerRows = Array.from(statsAccordion.querySelectorAll('tr.ui-table__row'));
149            
150            for (const row of trainerRows) {
151                const nameCell = row.querySelector('[data-test-selector="RC-trainerName__row"]');
152                const name = nameCell?.textContent?.trim();
153                
154                if (name && name.toLowerCase().includes(trainerName.toLowerCase())) {
155                    const percentCell = row.querySelector('[data-test-selector="RC-lastPercent__row"]');
156                    const percentText = percentCell?.textContent?.trim() || '0%';
157                    const percent = parseFloat(percentText.replace('%', ''));
158                    
159                    console.log(`Trainer ${trainerName} L14d Win %: ${percent}%`);
160                    return percent;
161                }
162            }
163        }
164        
165        // Default to 0 if not found
166        console.log(`Trainer ${trainerName} stats not found, defaulting to 0%`);
167        return 0;
168    }
169
170    function extractRunnerData(runnerElement, raceData) {
171        console.log('Extracting runner data from element...');
172        
173        // Basic info
174        const nameEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerName"]');
175        const name = nameEl?.textContent?.trim() || 'Unknown';
176        
177        // Odds
178        const oddsEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerPrice"]');
179        const oddsDecimalAttr = oddsEl?.getAttribute('data-diffusion-decimal');
180        const oddsFractional = oddsEl?.textContent?.trim();
181        const oddsDecimal = oddsDecimalAttr ? parseFloat(oddsDecimalAttr) : parseOdds(oddsFractional);
182        
183        // Days since last run
184        const dslrEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerStats-lastRun"]');
185        const dslr = parseInt(dslrEl?.textContent?.trim() || '0');
186        
187        // Trainer
188        const trainerEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerTrainer-name"]');
189        const trainer = trainerEl?.textContent?.trim() || 'Unknown';
190        
191        // Age
192        const ageEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerAge"]');
193        const age = parseInt(ageEl?.textContent?.trim() || '0');
194        
195        // Official Rating (OR)
196        const orEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerOr"]');
197        const officialRating = parseInt(orEl?.textContent?.trim() || '0');
198        
199        // Weight
200        const weightEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerWgt-carried"]');
201        const weightCarried = parseWeight(weightEl);
202        
203        // RPR (Racing Post Rating)
204        const rprEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerRpr"]');
205        const rprLTO = parseInt(rprEl?.textContent?.trim() || '0');
206        
207        // TS (Topspeed)
208        const tsEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerTs"]');
209        const ts = parseInt(tsEl?.textContent?.trim() || '0');
210        
211        // Form
212        const formEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerForm"]');
213        const form = formEl?.textContent?.trim() || '';
214        
215        // Course & Distance indicators
216        const cdIndicatorC = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerStats-c"]');
217        const cdIndicatorD = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerStats-d"]');
218        const cdIndicatorCD = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerStats-cd"]');
219        
220        const hasCourseWin = !!cdIndicatorC;
221        const hasDistanceWin = !!cdIndicatorD;
222        const hasCDWin = !!cdIndicatorCD;
223        
224        // Draw/Stall (Flat only)
225        const drawEl = runnerElement.querySelector('[data-test-selector="RC-cardPage-runnerNumber-draw"]');
226        const draw = drawEl?.textContent?.trim() ? parseInt(drawEl.textContent.trim()) : null;
227        
228        // Last Winning Mark - extract from form or profile (simplified for now)
229        // This would require additional data extraction from horse profile
230        const lastWinningMark = officialRating; // Placeholder - would need actual LWM
231        
232        const runnerData = {
233            name,
234            oddsFractional,
235            oddsDecimal,
236            dslr,
237            trainer,
238            age,
239            officialRating,
240            weightCarried,
241            rprLTO,
242            ts,
243            form,
244            hasCourseWin,
245            hasDistanceWin,
246            hasCDWin,
247            draw,
248            lastWinningMark,
249            // Will be populated later
250            trainerL14dWinPercent: 0
251        };
252        
253        console.log('Runner data extracted:', name, runnerData);
254        return runnerData;
255    }
256
257    // ========================================
258    // 4. SCORING FUNCTIONS (T1-T8)
259    // ========================================
260
261    function calculateT1(dslr) {
262        // T1: 7-Day Energy Shift
263        // 1 Point if DSLR between 5 and 35 days
264        const score = (dslr >= 5 && dslr <= 35) ? 1 : 0;
265        console.log(`T1 (Energy Shift): DSLR=${dslr}, Score=${score}`);
266        return score;
267    }
268
269    function calculateT2(trainerL14dWinPercent) {
270        // T2: Trainer Sequence Pattern
271        // 1 Point if Trainer L14d Win % >= 10%
272        const score = (trainerL14dWinPercent >= 10) ? 1 : 0;
273        console.log(`T2 (Trainer Pattern): Win%=${trainerL14dWinPercent}%, Score=${score}`);
274        return score;
275    }
276
277    function calculateT3(hasCDWin, hasCourseWin, hasDistanceWin) {
278        // T3: Distance-to-Form Ratio
279        // 2 Points for CD Winner
280        // 1 Point for C or D Winner/Placed
281        let score = 0;
282        
283        if (hasCDWin) {
284            score = 2;
285        } else if (hasCourseWin || hasDistanceWin) {
286            score = 1;
287        }
288        
289        console.log(`T3 (Distance-to-Form): CD=${hasCDWin}, C=${hasCourseWin}, D=${hasDistanceWin}, Score=${score}`);
290        return score;
291    }
292
293    function calculateT4(raceClass, age) {
294        // T4: Bloodline/Age Trigger
295        // Class 2-4: Age 3-6 = 2 points
296        // Class 5-6: Age 3-8 = 1 point
297        let score = 0;
298        
299        if (raceClass <= 4 && age >= 3 && age <= 6) {
300            score = 2;
301        } else if (raceClass >= 5 && raceClass <= 6 && age >= 3 && age <= 8) {
302            score = 1;
303        }
304        
305        console.log(`T4 (Age Trigger): Class=${raceClass}, Age=${age}, Score=${score}`);
306        return score;
307    }
308
309    function calculateT6(weightCarried, lowestWeight, lastWinningMark, currentOR) {
310        // T6: Weight/Handicap Angle
311        // 2 Points if: Carrying lowest weight OR (LWM - Current OR >= 2)
312        const lwCondition = (weightCarried === lowestWeight);
313        const orCondition = (lastWinningMark - currentOR >= 2);
314        
315        const score = (lwCondition || orCondition) ? 2 : 0;
316        
317        console.log(`T6 (Weight/Handicap): Weight=${weightCarried}, Lowest=${lowestWeight}, LWM=${lastWinningMark}, OR=${currentOR}, Score=${score}`);
318        return score;
319    }
320
321    function calculateT7(raceType, distanceInFurlongs, draw, course) {
322        // T7: Draw Bias
323        // 0 Points for Jumps or Long Flat Races (1m2f+)
324        // 1 Point if in top 33% of favorable draws for course/distance
325        
326        if (raceType === 'Jumps' || distanceInFurlongs >= 10) {
327            console.log(`T7 (Draw Bias): Jumps or Long Flat, Score=0`);
328            return 0;
329        }
330        
331        if (!draw) {
332            console.log(`T7 (Draw Bias): No draw info, Score=0`);
333            return 0;
334        }
335        
336        // Simplified: Low draws (1-5) are often favorable on many courses
337        // In a full implementation, this would use course-specific data
338        const score = (draw >= 1 && draw <= 5) ? 1 : 0;
339        
340        console.log(`T7 (Draw Bias): Draw=${draw}, Course=${course}, Score=${score}`);
341        return score;
342    }
343
344    function calculateT8(rprLTO, raceClass, raceType) {
345        // T8: Speed/Class Edge
346        // Use CCT lookup table
347        const cctTable = raceType === 'Jumps' ? CCT_JUMPS : CCT_FLAT;
348        const cct = cctTable[raceClass] || 0;
349        
350        let score = 0;
351        
352        if (rprLTO >= cct) {
353            score = 1;
354            
355            // Bonus point if RPR >= CCT + 5
356            if (rprLTO >= cct + 5) {
357                score = 2;
358            }
359        }
360        
361        console.log(`T8 (Speed/Class): RPR=${rprLTO}, CCT=${cct}, Score=${score}`);
362        return score;
363    }
364
365    // ========================================
366    // 5. SELECTION LOGIC & FILTERING
367    // ========================================
368
369    function calculateTotalScore(scores) {
370        return scores.t1 + scores.t2 + scores.t3 + scores.t4 + scores.t6 + scores.t7 + scores.t8;
371    }
372
373    function checkT5Qualifier(oddsDecimal) {
374        // T5: Market Reversal Filter
375        // Odds between 8/1 (9.0) and 33/1 (34.0)
376        if (!oddsDecimal) return false;
377        return oddsDecimal >= 9.0 && oddsDecimal <= 34.0;
378    }
379
380    function determineContenderType(totalScore, isT5Qualifier) {
381        // Primary Contender: Total Score >= 4
382        // HVC: Primary Contender AND T5 Qualifier
383        
384        const isPrimary = totalScore >= 4;
385        const isHVC = isPrimary && isT5Qualifier;
386        
387        if (isHVC) return 'HVC';
388        if (isPrimary) return 'Primary';
389        return 'None';
390    }
391
392    function applyTieBreaker(runners) {
393        // Find highest score
394        const maxScore = Math.max(...runners.map(r => r.totalScore));
395        const topRunners = runners.filter(r => r.totalScore === maxScore);
396        
397        if (topRunners.length === 1) {
398            return topRunners[0];
399        }
400        
401        console.log(`Tie-breaker needed for ${topRunners.length} runners with score ${maxScore}`);
402        
403        // Primary Tie-Breaker: Highest T8 (RPR LTO)
404        const maxRPR = Math.max(...topRunners.map(r => r.rprLTO));
405        let candidates = topRunners.filter(r => r.rprLTO === maxRPR);
406        
407        if (candidates.length === 1) {
408            console.log('Tie broken by T8 (RPR)');
409            return candidates[0];
410        }
411        
412        // Secondary Tie-Breaker: Highest T3 Score
413        const maxT3 = Math.max(...candidates.map(r => r.scores.t3));
414        candidates = candidates.filter(r => r.scores.t3 === maxT3);
415        
416        if (candidates.length === 1) {
417            console.log('Tie broken by T3');
418            return candidates[0];
419        }
420        
421        // Tertiary Tie-Breaker: T5 Status and Odds
422        const hvcs = candidates.filter(r => r.contenderType === 'HVC');
423        
424        if (hvcs.length > 0) {
425            // Among HVCs, prefer shortest odds
426            hvcs.sort((a, b) => (a.oddsDecimal || 999) - (b.oddsDecimal || 999));
427            console.log('Tie broken by HVC status and odds');
428            return hvcs[0];
429        }
430        
431        // If no HVCs, prefer shortest odds overall
432        candidates.sort((a, b) => (a.oddsDecimal || 999) - (b.oddsDecimal || 999));
433        console.log('Tie broken by shortest odds');
434        return candidates[0];
435    }
436
437    // ========================================
438    // 6. MAIN ANALYSIS FUNCTION
439    // ========================================
440
441    async function analyzeRace() {
442        console.log('=== Starting Race Analysis ===');
443        
444        // Extract race data
445        const raceData = extractRaceData();
446        
447        if (!raceData.raceClass) {
448            console.error('Could not determine race class. Aborting analysis.');
449            return;
450        }
451        
452        // Get all runners
453        const runnerElements = Array.from(document.querySelectorAll('.RC-runnerRow'));
454        console.log(`Found ${runnerElements.length} runners`);
455        
456        if (runnerElements.length === 0) {
457            console.error('No runners found. Aborting analysis.');
458            return;
459        }
460        
461        // Extract runner data
462        const runners = runnerElements.map(el => extractRunnerData(el, raceData));
463        
464        // Calculate lowest weight
465        const lowestWeight = Math.min(...runners.map(r => r.weightCarried).filter(w => w > 0));
466        console.log(`Lowest weight in race: ${lowestWeight} lbs`);
467        
468        // Expand stats accordion to get trainer data (if not already expanded)
469        const statsButton = document.querySelector('.js-accordion__header');
470        if (statsButton && !statsButton.classList.contains('ui-accordion__header_active')) {
471            statsButton.click();
472            // Wait for stats to load
473            await new Promise(resolve => setTimeout(resolve, 1000));
474        }
475        
476        // Get trainer stats for each runner
477        for (const runner of runners) {
478            runner.trainerL14dWinPercent = await extractTrainerStats(runner.trainer);
479        }
480        
481        // Calculate scores for each runner
482        for (const runner of runners) {
483            const scores = {
484                t1: calculateT1(runner.dslr),
485                t2: calculateT2(runner.trainerL14dWinPercent),
486                t3: calculateT3(runner.hasCDWin, runner.hasCourseWin, runner.hasDistanceWin),
487                t4: calculateT4(raceData.raceClass, runner.age),
488                t6: calculateT6(runner.weightCarried, lowestWeight, runner.lastWinningMark, runner.officialRating),
489                t7: calculateT7(raceData.raceType, raceData.distanceInFurlongs, runner.draw, raceData.course),
490                t8: calculateT8(runner.rprLTO, raceData.raceClass, raceData.raceType)
491            };
492            
493            runner.scores = scores;
494            runner.totalScore = calculateTotalScore(scores);
495            runner.isT5Qualifier = checkT5Qualifier(runner.oddsDecimal);
496            runner.contenderType = determineContenderType(runner.totalScore, runner.isT5Qualifier);
497            
498            console.log(`${runner.name}: Total Score = ${runner.totalScore}, Type = ${runner.contenderType}`);
499        }
500        
501        // Sort by total score (descending)
502        runners.sort((a, b) => b.totalScore - a.totalScore);
503        
504        // Apply tie-breaker for top selection
505        const topSelection = applyTieBreaker(runners);
506        console.log(`Top Selection: ${topSelection.name} (Score: ${topSelection.totalScore})`);
507        
508        // Display results
509        displayResults(runners, topSelection, raceData);
510        
511        return { runners, topSelection, raceData };
512    }
513
514    // ========================================
515    // 7. UI DISPLAY FUNCTIONS
516    // ========================================
517
518    function displayResults(runners, topSelection, raceData) {
519        console.log('Displaying results in UI...');
520        
521        // Add scores to each runner row
522        runners.forEach(runner => {
523            const runnerElement = Array.from(document.querySelectorAll('.RC-runnerRow')).find(el => {
524                const nameEl = el.querySelector('[data-test-selector="RC-cardPage-runnerName"]');
525                return nameEl?.textContent?.trim().includes(runner.name);
526            });
527            
528            if (runnerElement) {
529                addScoreDisplay(runnerElement, runner, topSelection);
530            }
531        });
532        
533        // Add summary panel
534        addSummaryPanel(runners, topSelection, raceData);
535    }
536
537    function addScoreDisplay(runnerElement, runner, topSelection) {
538        // Remove existing score display if any
539        const existing = runnerElement.querySelector('.handicap-analyzer-score');
540        if (existing) existing.remove();
541        
542        // Create score display
543        const scoreDiv = document.createElement('div');
544        scoreDiv.className = 'handicap-analyzer-score';
545        scoreDiv.style.cssText = `
546            display: flex;
547            align-items: center;
548            gap: 8px;
549            padding: 8px 12px;
550            margin: 4px 0;
551            border-radius: 4px;
552            font-weight: bold;
553            font-size: 14px;
554        `;
555        
556        // Color coding based on contender type
557        let bgColor = '#f5f5f5';
558        let textColor = '#333';
559        let badge = '';
560        
561        if (runner.contenderType === 'HVC') {
562            bgColor = '#4CAF50';
563            textColor = '#fff';
564            badge = '🏆 HVC';
565        } else if (runner.contenderType === 'Primary') {
566            bgColor = '#2196F3';
567            textColor = '#fff';
568            badge = '⭐ PRIMARY';
569        }
570        
571        if (runner === topSelection) {
572            scoreDiv.style.border = '3px solid #FF9800';
573            badge = '👑 ' + (badge || 'TOP PICK');
574        }
575        
576        scoreDiv.style.backgroundColor = bgColor;
577        scoreDiv.style.color = textColor;
578        
579        // Build score breakdown
580        const scoreBreakdown = `
581            <div style="display: flex; align-items: center; gap: 12px; width: 100%;">
582                <div style="font-size: 18px; font-weight: bold; min-width: 60px;">
583                    ${badge ? badge : `Score: ${runner.totalScore}`}
584                </div>
585                <div style="font-size: 16px; font-weight: bold; color: ${textColor};">
586                    Total: ${runner.totalScore}/10
587                </div>
588                <div style="font-size: 12px; opacity: 0.9;">
589                    T1:${runner.scores.t1} T2:${runner.scores.t2} T3:${runner.scores.t3} 
590                    T4:${runner.scores.t4} T6:${runner.scores.t6} T7:${runner.scores.t7} T8:${runner.scores.t8}
591                </div>
592                ${runner.isT5Qualifier ? '<div style="background: #FF9800; color: white; padding: 2px 6px; border-radius: 3px; font-size: 11px;">T5✓</div>' : ''}
593            </div>
594        `;
595        
596        scoreDiv.innerHTML = scoreBreakdown;
597        
598        // Insert after runner info
599        const infoWrapper = runnerElement.querySelector('.RC-runnerRowInfoWrapper');
600        if (infoWrapper) {
601            infoWrapper.insertAdjacentElement('afterend', scoreDiv);
602        }
603    }
604
605    function addSummaryPanel(runners, topSelection, raceData) {
606        // Remove existing panel if any
607        const existing = document.querySelector('.handicap-analyzer-summary');
608        if (existing) existing.remove();
609        
610        // Create summary panel
611        const panel = document.createElement('div');
612        panel.className = 'handicap-analyzer-summary';
613        panel.style.cssText = `
614            position: fixed;
615            top: 80px;
616            right: 20px;
617            width: 320px;
618            background: white;
619            border: 2px solid #2196F3;
620            border-radius: 8px;
621            padding: 16px;
622            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
623            z-index: 10000;
624            font-family: Arial, sans-serif;
625            max-height: 80vh;
626            overflow-y: auto;
627        `;
628        
629        const primaryContenders = runners.filter(r => r.contenderType === 'Primary' || r.contenderType === 'HVC');
630        const hvcContenders = runners.filter(r => r.contenderType === 'HVC');
631        
632        panel.innerHTML = `
633            <div style="margin-bottom: 12px; padding-bottom: 12px; border-bottom: 2px solid #2196F3;">
634                <h3 style="margin: 0 0 8px 0; color: #2196F3; font-size: 16px;">
635                    🏇 Handicap Analyzer
636                </h3>
637                <div style="font-size: 12px; color: #666;">
638                    ${raceData.raceType} | Class ${raceData.raceClass} | ${raceData.distance}
639                </div>
640            </div>
641            
642            <div style="margin-bottom: 12px; padding: 12px; background: #FFF3E0; border-radius: 6px; border-left: 4px solid #FF9800;">
643                <div style="font-weight: bold; color: #FF9800; margin-bottom: 6px;">👑 TOP SELECTION</div>
644                <div style="font-size: 14px; font-weight: bold;">${topSelection.name}</div>
645                <div style="font-size: 12px; color: #666; margin-top: 4px;">
646                    Score: ${topSelection.totalScore}/10 | ${topSelection.oddsFractional || 'SP'}
647                </div>
648            </div>
649            
650            ${hvcContenders.length > 0 ? `
651                <div style="margin-bottom: 12px; padding: 12px; background: #E8F5E9; border-radius: 6px; border-left: 4px solid #4CAF50;">
652                    <div style="font-weight: bold; color: #4CAF50; margin-bottom: 6px;">🏆 HVC CONTENDERS (${hvcContenders.length})</div>
653                    ${hvcContenders.map(r => `
654                        <div style="font-size: 13px; padding: 4px 0; border-bottom: 1px solid #ddd;">
655                            ${r.name} - ${r.totalScore}/10 @ ${r.oddsFractional || 'SP'}
656                        </div>
657                    `).join('')}
658                </div>
659            ` : ''}
660            
661            ${primaryContenders.length > 0 ? `
662                <div style="margin-bottom: 12px; padding: 12px; background: #E3F2FD; border-radius: 6px; border-left: 4px solid #2196F3;">
663                    <div style="font-weight: bold; color: #2196F3; margin-bottom: 6px;">⭐ PRIMARY CONTENDERS (${primaryContenders.length})</div>
664                    ${primaryContenders.slice(0, 5).map(r => `
665                        <div style="font-size: 13px; padding: 4px 0; border-bottom: 1px solid #ddd;">
666                            ${r.name} - ${r.totalScore}/10
667                        </div>
668                    `).join('')}
669                </div>
670            ` : ''}
671            
672            <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ddd;">
673                <div style="font-size: 11px; color: #999; text-align: center;">
674                    Analysis based on 8-factor scoring system (T1-T8)
675                </div>
676            </div>
677        `;
678        
679        document.body.appendChild(panel);
680        
681        // Make panel draggable (optional enhancement)
682        makeDraggable(panel);
683    }
684
685    function makeDraggable(element) {
686        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
687        
688        element.onmousedown = dragMouseDown;
689        
690        function dragMouseDown(e) {
691            e = e || window.event;
692            e.preventDefault();
693            pos3 = e.clientX;
694            pos4 = e.clientY;
695            document.onmouseup = closeDragElement;
696            document.onmousemove = elementDrag;
697        }
698        
699        function elementDrag(e) {
700            e = e || window.event;
701            e.preventDefault();
702            pos1 = pos3 - e.clientX;
703            pos2 = pos4 - e.clientY;
704            pos3 = e.clientX;
705            pos4 = e.clientY;
706            element.style.top = (element.offsetTop - pos2) + "px";
707            element.style.left = (element.offsetLeft - pos1) + "px";
708            element.style.right = 'auto';
709        }
710        
711        function closeDragElement() {
712            document.onmouseup = null;
713            document.onmousemove = null;
714        }
715    }
716
717    // ========================================
718    // 8. INITIALIZATION
719    // ========================================
720
721    function init() {
722        console.log('Initializing Handicap Analyzer...');
723        
724        // Check if we're on a race card page
725        if (!window.location.pathname.includes('/racecards/')) {
726            console.log('Not on a race card page. Extension inactive.');
727            return;
728        }
729        
730        // Wait for page to load
731        if (document.readyState === 'loading') {
732            document.addEventListener('DOMContentLoaded', init);
733            return;
734        }
735        
736        // Wait for runners to be present
737        const checkRunners = setInterval(() => {
738            const runners = document.querySelectorAll('.RC-runnerRow');
739            if (runners.length > 0) {
740                clearInterval(checkRunners);
741                console.log('Runners detected. Starting analysis...');
742                
743                // Run analysis after a short delay to ensure all data is loaded
744                setTimeout(() => {
745                    analyzeRace().catch(err => {
746                        console.error('Analysis error:', err);
747                    });
748                }, 2000);
749            }
750        }, 500);
751        
752        // Timeout after 10 seconds
753        setTimeout(() => {
754            clearInterval(checkRunners);
755        }, 10000);
756        
757        // Re-run analysis if page content changes (e.g., odds update)
758        const debouncedAnalyze = debounce(() => {
759            console.log('Page updated, re-analyzing...');
760            analyzeRace().catch(err => {
761                console.error('Re-analysis error:', err);
762            });
763        }, 3000);
764        
765        // Observe DOM changes
766        const observer = new MutationObserver(debouncedAnalyze);
767        observer.observe(document.body, {
768            childList: true,
769            subtree: true
770        });
771    }
772
773    // Start the extension
774    init();
775
776})();
UK/Irish Horse Racing Handicap Analyzer | Robomonkey