My Full Code

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>๐Ÿ Elite Race Predictor + Pattern Oracle</title> <script src="https://cdn.tailwindcss.com"></script> <style> body { font-family: 'Inter', system-ui, sans-serif; } .glow { box-shadow: 0 0 20px rgba(59, 130, 246, 0.5); } .winner-glow { box-shadow: 0 0 25px rgba(34, 197, 94, 0.6); } .pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; } .gradient-bg { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); } .card-dark { background: linear-gradient(145deg, #1e293b, #0f172a); } .neon-text { text-shadow: 0 0 10px currentColor; } /* Pattern timeline */ .timeline{display:flex;flex-wrap:wrap;gap:8px;max-height:300px;overflow-y:auto;padding:8px;align-content:flex-start}.timeline span{font-size:20px} .dot { width:18px; height:18px; display:flex; align-items:center; justify-content:center; font-size:16px; } .dot:hover { transform: scale(1.2); transition: transform .1s ease-in-out; } .badge { font-size: 11px; padding: 2px 6px; border-radius: 9999px; } </style> </head> <body class="gradient-bg text-white min-h-screen"> <div class="max-w-6xl mx-auto p-6"> <div class="text-center mb-8"> <h1 class="text-4xl font-bold neon-text text-blue-400 mb-2">๐Ÿ Elite Race Predictor</h1> <p class="text-gray-300">Advanced AI-powered racing calculator with Pattern Oracle & Trend Forecasting</p> </div> <div class="grid lg:grid-cols-3 gap-6"> <!-- Game Controls --> <div class="lg:col-span-2 space-y-6"> <!-- Road Selection --> <div class="card-dark p-6 rounded-xl border border-gray-700"> <h2 class="text-xl font-bold mb-4 text-blue-300">๐Ÿ›ฃ๏ธ Track Configuration</h2> <div class="grid grid-cols-3 gap-4 mb-4"> <div> <label class="block text-sm text-gray-400 mb-2">Left Segment</label> <select id="leftRoad" class="w-full p-2 bg-gray-800 border border-gray-600 rounded-lg text-white"> <option value="">Select Road</option> <option value="Expressway">Expressway</option> <option value="Highway">Highway</option> <option value="Dirt">Dirt</option> <option value="Bumpy">Bumpy</option> <option value="Potholes">Potholes</option> <option value="Desert">Desert</option> </select> </div> <div> <label class="block text-sm text-gray-400 mb-2">Middle Segment</label> <select id="middleRoad" class="w-full p-2 bg-gray-800 border border-gray-600 rounded-lg text-white"> <option value="">Select Road</option> <option value="Expressway">Expressway</option> <option value="Highway">Highway</option> <option value="Dirt">Dirt</option> <option value="Bumpy">Bumpy</option> <option value="Potholes">Potholes</option> <option value="Desert">Desert</option> </select> </div> <div> <label class="block text-sm text-gray-400 mb-2">Right Segment</label> <select id="rightRoad" class="w-full p-2 bg-gray-800 border border-gray-600 rounded-lg text-white"> <option value="">Select Road</option> <option value="Expressway">Expressway</option> <option value="Highway">Highway</option> <option value="Dirt">Dirt</option> <option value="Bumpy">Bumpy</option> <option value="Potholes">Potholes</option> <option value="Desert">Desert</option> </select> </div> </div> <div class="bg-yellow-900/30 border border-yellow-600 rounded-lg p-3 mb-4"> <p class="text-yellow-300 text-sm">๐Ÿ’ก <strong>Pro Tip:</strong> The game will reveal one segment. Set that one with certainty, then our AI will calculate probabilities for unknown segments.</p> </div> </div> <!-- Vehicle Selection --> <div class="card-dark p-6 rounded-xl border border-gray-700"> <h2 class="text-xl font-bold mb-4 text-blue-300">๐Ÿš— Vehicle Selection</h2> <p class="text-gray-400 text-sm mb-4">Choose exactly 3 vehicles for the race:</p> <div class="grid grid-cols-3 gap-3" id="vehicleGrid"> <!-- Vehicle buttons will be generated here --> </div> <div id="selectedVehicles" class="mt-4 p-3 bg-gray-800 rounded-lg"> <p class="text-sm text-gray-400">Selected: <span id="vehicleCount" class="text-blue-400 font-semibold">0/3</span></p> <div id="vehicleList" class="flex flex-wrap gap-2 mt-2"></div> </div> </div> <!-- Calculate Button --> <button id="calculateBtn" class="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white py-4 px-6 rounded-xl font-bold text-lg hover:from-blue-700 hover:to-purple-700 transform hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"> ๐Ÿงฎ Calculate Winner Probability </button> </div> <!-- Results Panel --> <div class="card-dark p-6 rounded-xl border border-gray-700"> <h2 class="text-xl font-bold mb-4 text-green-400">๐Ÿ† Prediction Results</h2> <div id="resultsContainer" class="space-y-4"> <div class="text-center text-gray-400 py-8"> <div class="text-4xl mb-2">๐ŸŽฏ</div> <p>Select 3 vehicles and configure the track to see AI predictions</p> </div> </div> <div id="advancedToggle" class="mt-6 hidden"> <button id="showAdvanced" class="w-full bg-gray-700 hover:bg-gray-600 text-white py-2 px-4 rounded-lg transition-colors"> ๐Ÿ“Š Show Advanced Analytics </button> </div> <div id="advancedResults" class="mt-4 hidden bg-gray-800 rounded-lg p-4"> <h3 class="font-bold text-purple-400 mb-3">Advanced Analytics</h3> <div id="advancedContent"></div> </div> </div> </div> <!-- Pattern Oracle Section --> <div class="mt-8 card-dark p-6 rounded-xl border border-gray-700"> <div class="flex items-center justify-between mb-4"> <h2 class="text-xl font-bold text-cyan-300">๐Ÿ”ฎ Pattern Oracle</h2> <!-- ๐Ÿ”’ Strict Next Pattern (100% historical certainty) --> <div id="strictNextPatternPanel" class="mt-3 mb-4 rounded-xl border border-cyan-500/30 bg-cyan-900/20 p-3"> <div class="flex items-center gap-2"> <span class="text-sm px-2 py-0.5 rounded-full bg-cyan-500/20 text-cyan-300 border border-cyan-400/30">Strict</span> <span class="text-sm text-cyan-100/90">Next pattern (100% historical):</span> <span id="strictNextPatternOutput" class="text-sm font-semibold text-white">โ€“</span> </div> <div id="strictNextPatternContext" class="mt-2 text-xs text-cyan-200/80"></div> </div> <!-- ๐Ÿ”ฅ HL Trend Oracle --> <div id="hlTrendPanel" class="mt-3 mb-4 rounded-xl border border-green-500/30 bg-green-900/20 p-3"> <div class="flex items-center gap-2"> <span class="text-sm px-2 py-0.5 rounded-full bg-green-500/20 text-green-300 border border-green-400/30">HL</span> <span class="text-sm text-green-100/90">Trend Oracle:</span> <span id="hlTrendOutput" class="text-sm font-semibold text-white">โ€“</span> </div> </div> <!-- ๐ŸŒ Global Oracle (Weighted) --> <div id="globalOraclePanel" class="mt-3 mb-4 rounded-xl border border-purple-500/30 bg-purple-900/20 p-3"> <div class="flex items-center gap-2"> <span class="text-sm px-2 py-0.5 rounded-full bg-purple-500/20 text-purple-300 border border-purple-400/30">Global</span> <span class="text-sm text-purple-100/90">Oracle (weighted):</span> <span id="globalOracleOutput" class="text-sm font-semibold text-white">โ€“</span> </div> <div id="globalOracleContext" class="mt-2 text-xs text-purple-200/80"></div> </div> <div id="strictNextPatternContext" class="mt-2 text-xs text-cyan-200/80"></div> </div> <div class="flex items-center gap-2 text-xs"> <button id="exportPatternBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded text-sm transition-colors ml-2"> ๐Ÿ“ค Export Patterns </button> <span class="badge bg-green-600/20 border border-green-500/30">๐ŸŸข HL</span> <span class="badge bg-yellow-600/20 border border-yellow-500/30">๐ŸŸก LL</span> <span class="badge bg-red-600/20 border border-red-500/30">๐Ÿ”ด NC</span> <span class="badge bg-gray-600/30 border border-gray-400/30">โšช Random</span> </div> </div> <!-- Filters --> <div class="grid md:grid-cols-4 gap-3 mb-3"> <div class="col-span-2"> <label class="block text-sm text-gray-400 mb-1">๐Ÿ“… Pick Date</label> <input id="dateFilter" type="date" class="w-full p-2 bg-gray-800 border border-gray-600 rounded-lg text-white"> </div> <div> <label class="block text-sm text-gray-400 mb-1">๐Ÿ•’ Time Slot</label> <select id="timeSlotFilter" class="w-full p-2 bg-gray-800 border border-gray-600 rounded-lg text-white"> <option value="All">All Times</option> <option value="Night">00:00 - 06:00 (Night)</option> <option value="Morning">06:00 - 12:00 (Morning)</option> <option value="Afternoon">12:00 - 18:00 (Afternoon)</option> <option value="Evening">18:00 - 00:00 (Evening)</option> </select> </div> <div class="flex items-end"> <button id="clearFilters" class="w-full bg-gray-700 hover:bg-gray-600 text-white py-2 px-4 rounded-lg transition-colors">โ™ป๏ธ Clear Filters</button> </div> </div> <div id="patternSummary" class="text-sm text-gray-300 mb-2"> No pattern data yet. Complete some races to build history. </div> <div id="patternTimeline" class="timeline bg-gray-900/40 border border-gray-700 rounded-lg"></div> </div> <!-- History Section --> <div class="mt-8 card-dark p-6 rounded-xl border border-gray-700"> <div class="flex justify-between items-center mb-4"> <h2 class="text-xl font-bold text-yellow-400">๐Ÿ“Š Race History & Learning</h2> <!-- Data Management Controls --> <div class="flex gap-2"> <button id="exportBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition-colors" title="Download your race history"> ๐Ÿ’พ Export Data </button> <label for="importFile" class="bg-green-600 hover:bg-green-700 text-white px-3 py-1 rounded text-sm cursor-pointer transition-colors" title="Upload your saved race history"> ๐Ÿ“ค Import Data </label> <input type="file" id="importFile" accept=".json" class="hidden"> <button id="clearBtn" class="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm transition-colors" title="Clear all data"> ๐Ÿ—‘๏ธ Clear </button> </div> </div> <div id="historyContainer" class="text-sm"> <p class="text-gray-400">No races completed yet. Results will appear here to improve future predictions.</p> </div> <div id="winnerSelection" class="mt-4 hidden"> <h3 class="font-semibold text-green-400 mb-2">Who actually won the race?</h3> <div id="winnerButtons" class="flex gap-2 mb-4"></div> <div id="secondPlaceSelection" class="hidden"> <h3 class="font-semibold text-blue-400 mb-2">Who came in second place?</h3> <div id="secondPlaceButtons" class="flex gap-2"></div> </div> </div> <!-- Import Success/Error Messages --> <div id="importMessage" class="mt-3 hidden p-2 rounded text-sm"></div> </div> </div> <script> // ===== Vehicles & Base Data ===== const vehicles = { "ATV": {Expressway:82, Highway:81, Dirt:77, Bumpy:73, Potholes:76, Desert:80.03, baseVictory:26.0, odds:3.8, consistency:0.85}, "Monster Truck": {Expressway:107, Highway:102.71, Dirt:54.03, Bumpy:95.34, Potholes:77, Desert:57.52, baseVictory:37.5, odds:2.6, consistency:0.72}, "Motorcycle": {Expressway:92.84, Highway:78.06, Dirt:65.08, Bumpy:82.09, Potholes:62.78, Desert:67.08, baseVictory:26.0, odds:6.2, consistency:0.71}, "Supercar": {Expressway:197.77, Highway:197.94, Dirt:100.48, Bumpy:61.41, Potholes:25.12, Desert:36.06, baseVictory:25.8, odds:3.8, consistency:0.65}, "Sports Car": {Expressway:174.02, Highway:155.8, Dirt:112.26, Bumpy:68.57, Potholes:26.74, Desert:48.3, baseVictory:29.6, odds:3.3, consistency:0.71}, "Car": {Expressway:155.3, Highway:139.4, Dirt:112.99, Bumpy:74.55, Potholes:49.64, Desert:55, baseVictory:44.4, odds:2.2, consistency:0.88}, "SUV": {Expressway:136.46, Highway:124.42, Dirt:115.77, Bumpy:75.55, Potholes:50.58, Desert:61.75, baseVictory:46.1, odds:2.1, consistency:0.82}, "ORV": {Expressway:122.69, Highway:110.89, Dirt:97.29, Bumpy:90.93, Potholes:54.58, Desert:80, baseVictory:41.0, odds:2.4, consistency:0.78}, "Stock Car": {Expressway:104.65, Highway:98.59, Dirt:81.39, Bumpy:76.75, Potholes:61.55, Desert:87.41, baseVictory:33.5, odds:2.9, consistency:0.75} }; // ===== Pattern Data from CSV ===== const patternData = { "morning_Bumpy-end": { HL: 39, LL: 35, NC: 26, topVehicles: {"Monster Truck":33.33, "ORV":27.78, "Motorcycle":16.67, "SUV":11.11} }, "morning_Bumpy-mid": { HL: 42, LL: 40, NC: 17, topVehicles: {"ORV":36.36, "Stock Car":18.18, "Monster Truck":13.64, "SUV":13.64} }, "morning_Bumpy-start": { HL: 50, LL: 18, NC: 32, topVehicles: {"Monster Truck":32.0, "Stock Car":28.0, "Car":16.0, "ORV":12.0} }, "morning_Desert-end": { HL: 48, LL: 23, NC: 30, topVehicles: {"Stock Car":28.57, "ATV":23.81, "ORV":19.05, "SUV":14.29} }, "morning_Desert-mid": { HL: 48, LL: 27, NC: 25, topVehicles: {"Stock Car":33.33, "ORV":18.52, "Motorcycle":14.81, "ATV":14.81} }, "morning_Desert-start": { HL: 55, LL: 24, NC: 21, topVehicles: {"Stock Car":47.83, "ORV":13.04, "ATV":13.04, "Car":8.7} }, "morning_Dirt-end": { HL: 56, LL: 18, NC: 26, topVehicles: {"Car":31.82, "Sports Car":22.73, "Supercar":13.64, "ORV":13.64} }, "morning_Dirt-mid": { HL: 50, LL: 25, NC: 25, topVehicles: {"Car":27.27, "ORV":22.73, "SUV":22.73, "Sports Car":13.64} }, "morning_Dirt-start": { HL: 47, LL: 28, NC: 26, topVehicles: {"SUV":48.15, "Car":22.22, "Supercar":14.81, "ORV":7.41} }, "morning_Expressway-end": { HL: 39, LL: 32, NC: 30, topVehicles: {"Car":31.82, "Supercar":22.73, "Sports Car":22.73, "Monster Truck":9.09} }, "morning_Expressway-mid": { HL: 44, LL: 25, NC: 31, topVehicles: {"Car":30.43, "Supercar":21.74, "SUV":13.04, "Sports Car":13.04} }, "morning_Expressway-start": { HL: 49, LL: 24, NC: 27, topVehicles: {"Sports Car":40.0, "ORV":20.0, "Supercar":20.0, "Car":12.0} }, "morning_Highway-end": { HL: 48, LL: 23, NC: 29, topVehicles: {"SUV":26.09, "ORV":21.74, "Sports Car":21.74, "Supercar":17.39} }, "morning_Highway-mid": { HL: 40, LL: 29, NC: 31, topVehicles: {"Car":31.58, "Supercar":31.58, "ORV":15.79, "Sports Car":15.79} }, "morning_Highway-start": { HL: 44, LL: 34, NC: 22, topVehicles: {"SUV":26.92, "Car":26.92, "Supercar":26.92, "Sports Car":11.54} }, "morning_Potholes-end": { HL: 44, LL: 29, NC: 27, topVehicles: {"Monster Truck":23.81, "Stock Car":23.81, "ATV":19.05, "Motorcycle":19.05} }, "morning_Potholes-mid": { HL: 41, LL: 31, NC: 27, topVehicles: {"Monster Truck":38.1, "ATV":23.81, "SUV":19.05, "Motorcycle":9.52} }, "morning_Potholes-start": { HL: 53, LL: 32, NC: 15, topVehicles: {"ATV":33.33, "Stock Car":22.22, "Monster Truck":22.22, "SUV":16.67} }, // Afternoon patterns "afternoon_Bumpy-end": { HL: 43, LL: 29, NC: 28, topVehicles: {"Monster Truck":36.0, "ORV":32.0, "Car":16.0, "Stock Car":12.0} }, "afternoon_Bumpy-mid": { HL: 42, LL: 30, NC: 28, topVehicles: {"Monster Truck":28.57, "ORV":23.81, "Car":19.05, "SUV":14.29} }, "afternoon_Bumpy-start": { HL: 47, LL: 27, NC: 25, topVehicles: {"Monster Truck":46.43, "ORV":17.86, "Stock Car":14.29, "SUV":10.71} }, "afternoon_Desert-end": { HL: 44, LL: 28, NC: 28, topVehicles: {"Stock Car":36.84, "ORV":36.84, "ATV":10.53, "Monster Truck":10.53} }, "afternoon_Desert-mid": { HL: 41, LL: 32, NC: 26, topVehicles: {"ORV":28.57, "Motorcycle":17.86, "SUV":17.86, "ATV":14.29} }, "afternoon_Desert-start": { HL: 50, LL: 17, NC: 33, topVehicles: {"Stock Car":42.86, "ATV":23.81, "ORV":14.29, "Car":14.29} }, "afternoon_Dirt-end": { HL: 60, LL: 21, NC: 19, topVehicles: {"Car":34.48, "SUV":31.03, "Sports Car":27.59, "ORV":3.45} }, "afternoon_Dirt-mid": { HL: 52, LL: 20, NC: 28, topVehicles: {"SUV":42.86, "Sports Car":25.0, "Car":17.86, "ORV":7.14} }, "afternoon_Dirt-start": { HL: 47, LL: 26, NC: 27, topVehicles: {"SUV":37.93, "Sports Car":31.03, "Car":13.79, "ORV":6.9} }, "afternoon_Expressway-end": { HL: 52, LL: 26, NC: 21, topVehicles: {"Supercar":31.25, "ORV":15.62, "Car":15.62, "Monster Truck":12.5} }, "afternoon_Expressway-mid": { HL: 43, LL: 41, NC: 16, topVehicles: {"Supercar":23.81, "Car":23.81, "Sports Car":19.05, "ORV":19.05} }, "afternoon_Expressway-start": { HL: 47, LL: 24, NC: 29, topVehicles: {"SUV":21.74, "Sports Car":21.74, "ORV":17.39, "Supercar":13.04} }, "afternoon_Highway-end": { HL: 49, LL: 26, NC: 26, topVehicles: {"Supercar":30.43, "Sports Car":21.74, "ORV":17.39, "Car":13.04} }, "afternoon_Highway-mid": { HL: 54, LL: 20, NC: 26, topVehicles: {"Sports Car":29.63, "Car":22.22, "SUV":14.81, "Supercar":14.81} }, "afternoon_Highway-start": { HL: 33, LL: 31, NC: 36, topVehicles: {"ORV":28.57, "SUV":21.43, "Sports Car":14.29, "Car":14.29} }, "afternoon_Potholes-end": { HL: 38, LL: 41, NC: 21, topVehicles: {"ATV":23.81, "Motorcycle":19.05, "SUV":19.05, "Monster Truck":19.05} }, "afternoon_Potholes-mid": { HL: 41, LL: 25, NC: 33, topVehicles: {"Monster Truck":46.15, "ATV":26.92, "Motorcycle":15.38, "Stock Car":7.69} }, "afternoon_Potholes-start": { HL: 57, LL: 24, NC: 19, topVehicles: {"Monster Truck":41.67, "ATV":25.0, "Motorcycle":16.67, "Stock Car":8.33} }, // Evening patterns "evening_Bumpy-end": { HL: 32, LL: 44, NC: 24, topVehicles: {"Monster Truck":25.0, "Stock Car":25.0, "ORV":25.0, "Car":12.5} }, "evening_Bumpy-mid": { HL: 41, LL: 28, NC: 31, topVehicles: {"Monster Truck":50.0, "Motorcycle":25.0, "Car":8.33, "Stock Car":8.33} }, "evening_Bumpy-start": { HL: 53, LL: 31, NC: 16, topVehicles: {"Monster Truck":52.94, "ORV":29.41, "Stock Car":17.65} }, "evening_Desert-end": { HL: 43, LL: 34, NC: 23, topVehicles: {"ATV":33.33, "Stock Car":26.67, "ORV":26.67, "Motorcycle":13.33} }, "evening_Desert-mid": { HL: 54, LL: 24, NC: 22, topVehicles: {"ORV":40.0, "Stock Car":30.0, "SUV":10.0, "Motorcycle":10.0} }, "evening_Desert-start": { HL: 41, LL: 31, NC: 28, topVehicles: {"Stock Car":33.33, "Motorcycle":33.33, "ORV":25.0, "SUV":8.33} }, "evening_Dirt-end": { HL: 53, LL: 19, NC: 28, topVehicles: {"SUV":35.29, "Car":29.41, "ORV":17.65, "Sports Car":11.76} }, "evening_Dirt-mid": { HL: 48, LL: 24, NC: 29, topVehicles: {"Car":50.0, "SUV":20.0, "Sports Car":20.0, "Stock Car":10.0} }, "evening_Dirt-start": { HL: 45, LL: 9, NC: 45, topVehicles: {"SUV":40.0, "Sports Car":30.0, "Car":20.0, "ORV":10.0} }, "evening_Expressway-end": { HL: 36, LL: 32, NC: 32, topVehicles: {"SUV":25.0, "Supercar":25.0, "Sports Car":25.0, "Monster Truck":12.5} }, "evening_Expressway-mid": { HL: 45, LL: 36, NC: 18, topVehicles: {"Sports Car":20.0, "SUV":20.0, "Car":20.0, "Monster Truck":13.33} }, "evening_Expressway-start": { HL: 45, LL: 24, NC: 30, topVehicles: {"Supercar":33.33, "Sports Car":26.67, "ORV":26.67, "SUV":13.33} }, "evening_Highway-end": { HL: 49, LL: 24, NC: 27, topVehicles: {"Sports Car":27.78, "SUV":27.78, "ORV":16.67, "Car":11.11} }, "evening_Highway-mid": { HL: 50, LL: 23, NC: 27, topVehicles: {"ORV":30.77, "Supercar":23.08, "SUV":15.38, "Monster Truck":15.38} }, "evening_Highway-start": { HL: 38, LL: 26, NC: 36, topVehicles: {"Supercar":33.33, "ORV":33.33, "Sports Car":20.0, "Monster Truck":6.67} }, "evening_Potholes-end": { HL: 26, LL: 48, NC: 26, topVehicles: {"ATV":50.0, "Motorcycle":16.67, "Stock Car":16.67, "Monster Truck":16.67} }, "evening_Potholes-mid": { HL: 58, LL: 19, NC: 23, topVehicles: {"ATV":33.33, "Monster Truck":33.33, "Stock Car":16.67, "Motorcycle":11.11} }, "evening_Potholes-start": { HL: 57, LL: 21, NC: 21, topVehicles: {"Monster Truck":37.5, "ATV":25.0, "SUV":18.75, "Motorcycle":6.25} }, // Night patterns "night_Bumpy-end": { HL: 43, LL: 32, NC: 25, topVehicles: {"ORV":41.67, "Monster Truck":33.33, "Stock Car":16.67, "ATV":8.33} }, "night_Bumpy-mid": { HL: 61, LL: 29, NC: 11, topVehicles: {"ORV":35.29, "Stock Car":29.41, "Monster Truck":23.53, "Car":5.88} }, "night_Bumpy-start": { HL: 62, LL: 22, NC: 16, topVehicles: {"Monster Truck":40.0, "ORV":30.0, "Stock Car":20.0, "SUV":5.0} }, "night_Desert-end": { HL: 51, LL: 29, NC: 20, topVehicles: {"Stock Car":38.89, "ORV":22.22, "SUV":16.67, "ATV":16.67} }, "night_Desert-mid": { HL: 39, LL: 32, NC: 29, topVehicles: {"Stock Car":43.75, "ORV":31.25, "ATV":18.75, "Motorcycle":6.25} }, "night_Desert-start": { HL: 48, LL: 24, NC: 28, topVehicles: {"ATV":28.57, "SUV":21.43, "Motorcycle":14.29, "ORV":14.29} }, "night_Dirt-end": { HL: 39, LL: 16, NC: 45, topVehicles: {"Car":26.67, "SUV":26.67, "Stock Car":20.0, "ORV":13.33} }, "night_Dirt-mid": { HL: 52, LL: 28, NC: 21, topVehicles: {"Car":33.33, "SUV":33.33, "Sports Car":13.33, "Supercar":13.33} }, "night_Dirt-start": { HL: 67, LL: 15, NC: 19, topVehicles: {"SUV":50.0, "Car":33.33, "Sports Car":5.56, "ATV":5.56} }, "night_Expressway-end": { HL: 38, LL: 31, NC: 31, topVehicles: {"Supercar":60.0, "Car":30.0, "SUV":10.0} }, "night_Expressway-mid": { HL: 44, LL: 25, NC: 31, topVehicles: {"Supercar":28.57, "Car":28.57, "Sports Car":21.43, "ORV":21.43} }, "night_Expressway-start": { HL: 40, LL: 30, NC: 30, topVehicles: {"Supercar":37.5, "Sports Car":25.0, "Car":12.5, "SUV":6.25} }, "night_Highway-end": { HL: 46, LL: 36, NC: 18, topVehicles: {"SUV":38.46, "Supercar":23.08, "Sports Car":23.08, "Car":15.38} }, "night_Highway-mid": { HL: 60, LL: 18, NC: 22, topVehicles: {"SUV":29.17, "Supercar":20.83, "Sports Car":20.83, "Car":16.67} }, "night_Highway-start": { HL: 37, LL: 46, NC: 17, topVehicles: {"Supercar":38.46, "Sports Car":15.38, "SUV":15.38, "Car":15.38} }, "night_Potholes-end": { HL: 42, LL: 35, NC: 23, topVehicles: {"ATV":38.46, "Monster Truck":30.77, "Stock Car":7.69, "SUV":7.69} }, "night_Potholes-mid": { HL: 41, LL: 36, NC: 23, topVehicles: {"Monster Truck":44.44, "SUV":22.22, "Stock Car":11.11, "Motorcycle":11.11} }, "night_Potholes-start": { HL: 41, LL: 44, NC: 16, topVehicles: {"ATV":30.77, "Monster Truck":23.08, "SUV":15.38, "Stock Car":15.38} } }; // ===== CSV Data Helper Functions ===== // === POSITION + KEY HELPERS (ADD THIS) === const POS_LABELS = { 0: 'start', 1: 'mid', 2: 'end' }; function getKnownSegmentAndPos(trackArr) { for (let i = 0; i < 3; i++) { const seg = trackArr[i]; if (seg && seg !== 'Unknown') return { segment: seg, pos: POS_LABELS[i] }; } return { segment: null, pos: null }; } function normalizeTimeSegment(ts) { if (!ts) return null; const t = ts.toString().toLowerCase(); if (t.startsWith('morn')) return 'morning'; if (t.startsWith('after')) return 'afternoon'; if (t.startsWith('even')) return 'evening'; if (t.startsWith('night')) return 'night'; return null; } function buildCsvKey(timeSegment, trackArr) { const ts = normalizeTimeSegment(timeSegment); if (!ts) return null; const { segment, pos } = getKnownSegmentAndPos(trackArr); if (!segment || !pos) return null; return `${ts}_${segment}-${pos}`; } function getCurrentTimeSegment() { const hour = new Date().getHours(); if (hour >= 0 && hour < 6) return 'night'; if (hour >= 6 && hour < 12) return 'morning'; if (hour >= 12 && hour < 18) return 'afternoon'; return 'evening'; } function getTrackPosition(track) { // Convert track array to position string const left = track[0] || 'Unknown'; const middle = track[1] || 'Unknown'; const right = track[2] || 'Unknown'; // Determine position based on known segments if (left !== 'Unknown' && middle === 'Unknown' && right === 'Unknown') { return `${left}-start`; } else if (left === 'Unknown' && middle !== 'Unknown' && right === 'Unknown') { return `${middle}-mid`; } else if (left === 'Unknown' && middle === 'Unknown' && right !== 'Unknown') { return `${right}-end`; } else { // Fallback: use the first known segment const knownSegment = track.find(seg => seg !== 'Unknown'); return knownSegment ? `${knownSegment}-mid` : 'Unknown'; } } // === CONTEXT-AWARE CSV LOOKUP (REPLACE THE OLD FUNCTION) === function getPatternDataFromCSV(timeOverride = null, trackOverride = null) { const timeSegment = timeOverride || ( document.getElementById('timeSlotFilter')?.value === 'All' ? getCurrentTimeSegment() : document.getElementById('timeSlotFilter')?.value || getCurrentTimeSegment() ); const track = trackOverride || [ document.getElementById('leftRoad').value || 'Unknown', document.getElementById('middleRoad').value || 'Unknown', document.getElementById('rightRoad').value || 'Unknown' ]; const key = buildCsvKey(timeSegment, track); return key ? patternData[key] || null : null; } function getPatternBoost(vehicleName, patternType) { const csvData = getPatternDataFromCSV(); if (!csvData) return 1.0; // No boost if no data const vehiclePercentage = csvData.topVehicles[vehicleName] || 0; const basePercentage = 100 / Object.keys(csvData.topVehicles).length; // Average % // Calculate boost: if vehicle has 30% and average is 25%, boost = 1.2 return vehiclePercentage > 0 ? (vehiclePercentage / basePercentage) : 1.0; } const roads = ["Expressway", "Highway", "Dirt", "Bumpy", "Potholes", "Desert"]; // ===== Global Aggregation Cache ===== const agg = { vehicleWins: {}, vehicleSecond: {}, vehicleOnSegmentWins: {}, setupWins: {}, synergyWins: {} }; function keyVS(vehicle, segment) { return `${vehicle}|${segment}`; } function keySynergy(vehicles) { return [...vehicles].sort().join('|'); } function updateAggFromRecord(r) { // Vehicle wins agg.vehicleWins[r.actual] = (agg.vehicleWins[r.actual] || 0) + 1; agg.vehicleSecond[r.second] = (agg.vehicleSecond[r.second] || 0) + 1; // Vehicle wins on specific segments const segKnown = r.track.find(s => s && s !== 'Unknown'); if (segKnown) { const k = keyVS(r.actual, segKnown); agg.vehicleOnSegmentWins[k] = (agg.vehicleOnSegmentWins[k] || 0) + 1; } // Setup wins agg.setupWins[r.setupKey] = agg.setupWins[r.setupKey] || { winsBy: {}, total: 0 }; agg.setupWins[r.setupKey].total++; agg.setupWins[r.setupKey].winsBy[r.actual] = (agg.setupWins[r.setupKey].winsBy[r.actual] || 0) + 1; // Synergy wins const synKey = keySynergy(r.vehicles); agg.synergyWins[synKey] = agg.synergyWins[synKey] || { wins: 0, total: 0 }; agg.synergyWins[synKey].total++; if (r.rankingsIdx && r.rankingsIdx[r.actual] === 0) { agg.synergyWins[synKey].wins++; } } function rebuildAggFromHistory() { // Reset aggregates Object.keys(agg).forEach(k => agg[k] = {}); // Rebuild from history raceHistory.forEach(updateAggFromRecord); } // ===== State & Persistence ===== let selectedVehicles = []; let raceHistory = []; let currentPrediction = null; let currentWinner = null; const STORAGE_KEY = 'eliteRacePredictorData_v2'; const DOTS = { HL:'๐ŸŸข', LL:'๐ŸŸก', NC:'๐Ÿ”ด', Random:'โšช' }; function saveToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ raceHistory })); } catch(e) { console.warn('Storage save failed', e); } } function loadFromStorage() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = JSON.parse(raw); if (Array.isArray(data?.raceHistory)) { raceHistory = data.raceHistory; // Backfill classification/time segments if missing raceHistory.forEach(r => { if (!r.isoTimestamp) r.isoTimestamp = new Date(r.timestamp || Date.now()).toISOString(); if (!r.timeSegment) r.timeSegment = getTimeSegment(new Date(r.isoTimestamp)); if (!r.pattern) classifyPattern(r); // Backfill new schema fields if (!r.id) r.id = `${Date.now()}-${Math.random()}`; if (!r.dateKey) r.dateKey = r.isoTimestamp.slice(0, 10); if (!r.hour) r.hour = new Date(r.isoTimestamp).getHours(); if (!r.setupKey) r.setupKey = r.track.join(" | "); if (!r.vehiclesSorted) r.vehiclesSorted = [...r.vehicles].sort(); if (!r.rankingsIdx) { r.rankingsIdx = { [r.actual]: 0, [r.second]: 1, [r.third]: 2 }; } }); // Rebuild aggregates rebuildAggFromHistory(); analyzeTrends(); updateHistoryDisplay(); renderPatternTimeline(); } } catch(e) { console.warn('Storage load failed', e); } } // ===== UI Bootstrap ===== function initializeUI() { function addValidationPanel() { // Check if panel already exists if (document.getElementById('validationPanel')) return; const panel = document.createElement('div'); panel.id = 'validationPanel'; panel.className = 'mt-4 p-3 bg-gray-800 rounded-lg'; panel.innerHTML = ` <h3 class="text-md font-semibold text-gray-300 mb-2">Model Validation</h3> <div id="validationResults" class="text-sm text-gray-400">Run evaluation to see accuracy</div> <button id="runEvaluation" class="mt-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm"> Evaluate Model </button> `; // Insert after results container const resultsContainer = document.getElementById('resultsContainer'); resultsContainer.parentNode.insertBefore(panel, resultsContainer.nextSibling); // Add click event document.getElementById('runEvaluation').addEventListener('click', evaluateCurrentFormula); } function evaluateCurrentFormula() { if (raceHistory.length < 30) { document.getElementById('validationResults').textContent = 'Need at least 30 races for evaluation'; return null; } const split = Math.floor(raceHistory.length * 0.7); const train = raceHistory.slice(0, split); const test = raceHistory.slice(split); // Save current aggregates const savedAgg = JSON.parse(JSON.stringify(agg)); // Rebuild from training data only Object.keys(agg).forEach(k => agg[k] = {}); train.forEach(updateAggFromRecord); // Test predictions let correct = 0, total = 0; test.forEach(r => { const originalSelected = [...selectedVehicles]; selectedVehicles = [...r.vehicles]; const preds = calculateAdvancedPredictionForSim(r.track); const predicted = preds[0].vehicle; if (predicted === r.actual) correct++; total++; selectedVehicles = originalSelected; }); // Restore aggregates Object.keys(savedAgg).forEach(k => agg[k] = savedAgg[k]); const accuracy = (correct / total * 100).toFixed(1); document.getElementById('validationResults').innerHTML = ` Accuracy: <span class="font-bold ${accuracy > 50 ? 'text-green-400' : 'text-yellow-400'}">${accuracy}%</span> (${total} test races) `; return { acc: accuracy, total }; } function calculateAdvancedPredictionForSim(track) { // Simplified version for simulation - uses same logic as calculateAdvancedPrediction const setupKey = track.join(' | '); const knownIdx = track.findIndex(s => s && s !== 'Unknown'); const knownSeg = knownIdx >= 0 ? track[knownIdx] : null; const dynRates = calculateDynamicVictoryRates(); const posW = [0.33, 0.28, 0.39]; const predictions = selectedVehicles.map(candidate => { const vehicle = vehicles[candidate]; let trackScore = 0, knownCount = 0; track.forEach((seg, i) => { if (seg && seg !== 'Unknown') { trackScore += vehicle[seg] * posW[i]; knownCount++; } }); if (knownCount < 3) { const avg = roads.reduce((s, seg) => s + vehicle[seg], 0) / roads.length; const missingW = posW.reduce((s, w, idx) => (track[idx] === 'Unknown' || !track[idx]) ? s + w : s, 0); trackScore += avg * missingW; } const dyn = (dynRates[candidate] || 0) / 100; const consistency = vehicle.consistency; const synB = synergyBonus(selectedVehicles); const setupB = setupPrior(setupKey, candidate); const segLift = segmentWinLift(candidate, knownSeg); let patBias = 0; if (knownSeg) { const label = labelForVehicleOnKnownSegment(candidate, knownSeg, selectedVehicles); const patDist = nextPatternDistribution(); const prob = patDist[label] || 0.33; patBias = (prob - 0.33) * 140; } const hour = new Date().getHours(); const hourW = hourWeight(hour); const timeB = (hourW - 1.0) * 120; const w = { track: 0.70, history: 0.18, consistency: 0.06, extras: 0.06 }; const base = (trackScore * w.track) + (dyn * 600 * w.history) + (consistency * 100 * w.consistency); const extras = (synB + setupB + segLift + patBias + timeB) * w.extras; const implied = 1 / vehicle.odds; const edge = implied > 0.45 ? 0.96 : (implied > 0.35 ? 1.0 : 1.04); const finalScore = (base + extras) * edge; return { vehicle: candidate, finalScore, odds: vehicle.odds }; }); const totalScore = predictions.reduce((s, p) => s + p.finalScore, 0) || 1; predictions.forEach(p => p.probability = (p.finalScore / totalScore) * 100); predictions.sort((a, b) => b.finalScore - a.finalScore); return predictions; } createVehicleButtons(); updateCalculateButton(); loadFromStorage(); addValidationPanel(); // Add this line // โœ… Make the calculate button actually work document.getElementById("calculateBtn").addEventListener("click", showPredictionResults); } function addValidationPanel() { const panel = document.createElement('div'); panel.id = 'validationPanel'; panel.className = 'mt-4 p-3 bg-gray-800 rounded-lg'; panel.innerHTML = ` <h3 class="text-md font-semibold text-gray-300 mb-2">Model Validation</h3> <div id="validationResults" class="text-sm text-gray-400">Run evaluation to see accuracy</div> <button id="runEvaluation" class="mt-2 px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm"> Evaluate Model </button> `; document.getElementById('resultsContainer').parentNode.insertBefore(panel, document.getElementById('resultsContainer').nextSibling); document.getElementById('runEvaluation').addEventListener('click', () => { const results = evaluateCurrentFormula(); if (results) { document.getElementById('validationResults').innerHTML = ` Accuracy: <span class="font-bold ${results.acc > 50 ? 'text-green-400' : 'text-yellow-400'}">${results.acc}%</span> (${results.total} test races) `; } else { document.getElementById('validationResults').textContent = 'Need at least 30 races for evaluation'; } }); } // Add backtest button const backtestBtn = document.createElement('button'); backtestBtn.id = 'runBacktest'; backtestBtn.className = 'mt-2 ml-2 px-3 py-1 bg-purple-600 hover:bg-purple-700 rounded text-sm'; backtestBtn.textContent = 'Run Backtest (70/30)'; backtestBtn.addEventListener('click', () => { const res = runBacktest(0.7); if (res) { const validationResults = document.getElementById('validationResults'); validationResults.innerHTML += `<div class="mt-1 text-xs text-gray-300">Backtest: ${res.acc}% on ${res.total} test races</div>`; } }); panel.appendChild(backtestBtn); function evaluateCurrentFormula() { if (raceHistory.length < 30) return null; const split = Math.floor(raceHistory.length * 0.7); const train = raceHistory.slice(0, split); const test = raceHistory.slice(split); // Save current aggregates const savedAgg = JSON.parse(JSON.stringify(agg)); // Rebuild from training data only Object.keys(agg).forEach(k => agg[k] = {}); train.forEach(updateAggFromRecord); // Simulate predictions on test data let correct = 0, total = 0; test.forEach(r => { // Temporarily set selected vehicles const originalSelected = [...selectedVehicles]; selectedVehicles = [...r.vehicles]; // Make prediction using the test track const preds = calculateAdvancedPredictionForSim(r.track); const predicted = preds[0].vehicle; if (predicted === r.actual) correct++; total++; // Restore selection selectedVehicles = originalSelected; }); // Restore aggregates Object.keys(savedAgg).forEach(k => agg[k] = savedAgg[k]); return { acc: (correct / total * 100).toFixed(1), total }; } function calculateAdvancedPredictionForSim(track) { // This is a simplified version for simulation - same logic as calculateAdvancedPrediction // but using the passed track instead of reading from DOM const setupKey = track.join(' | '); const knownIdx = track.findIndex(s => s && s !== 'Unknown'); const knownSeg = knownIdx >= 0 ? track[knownIdx] : null; const dynRates = calculateDynamicVictoryRates(); const posW = [0.33, 0.28, 0.39]; const patDist = nextPatternDistribution(); const hour = new Date().getHours(); const hourW = hourWeight(hour); const predictions = selectedVehicles.map(candidate => { const vehicle = vehicles[candidate]; // Track score calculation let trackScore = 0, knownCount = 0; track.forEach((seg, i) => { if (seg && seg !== 'Unknown') { trackScore += vehicle[seg] * posW[i]; knownCount++; } }); if (knownCount < 3) { const avg = roads.reduce((s, seg) => s + vehicle[seg], 0) / roads.length; const missingW = posW.reduce((s, w, idx) => (track[idx] === 'Unknown' || !track[idx]) ? s + w : s, 0); trackScore += avg * missingW; } // Other factors (simplified) const dyn = (dynRates[candidate] || 0) / 100; const consistency = vehicle.consistency; const synB = synergyBonus(selectedVehicles); const setupB = setupPrior(setupKey, candidate); const segLift = segmentWinLift(candidate, knownSeg); let patBias = 0; if (knownSeg) { const label = labelForVehicleOnKnownSegment(candidate, knownSeg, selectedVehicles); const prob = patDist[label] || 0.33; patBias = (prob - 0.33) * 140; } const timeB = (hourW - 1.0) * 120; const w = { track: 0.70, history: 0.18, consistency: 0.06, extras: 0.06 }; const base = (trackScore * w.track) + (dyn * 600 * w.history) + (consistency * 100 * w.consistency); const extras = (synB + setupB + segLift + patBias + timeB) * w.extras; const implied = 1 / vehicle.odds; const edge = implied > 0.45 ? 0.96 : (implied > 0.35 ? 1.0 : 1.04); const finalScore = (base + extras) * edge; return { vehicle: candidate, finalScore, odds: vehicle.odds }; }); const totalScore = predictions.reduce((s, p) => s + p.finalScore, 0) || 1; predictions.forEach(p => p.probability = (p.finalScore / totalScore) * 100); predictions.sort((a, b) => b.finalScore - a.finalScore); return predictions; } // === EVAL-SAFE PREDICTOR (ADD THIS) === function calculateAdvancedPredictionForEval(track, timeSegment, patDistOverride) { const setupKey = track.join(' | '); const knownIdx = track.findIndex(s => s && s !== 'Unknown'); const knownSeg = knownIdx >= 0 ? track[knownIdx] : null; const dynRates = calculateDynamicVictoryRates(); const posW = [0.35, 0.30, 0.35]; const predictions = selectedVehicles.map(candidate => { const vehicle = vehicles[candidate]; // Track score with known-segment bonus let trackScore = 0, knownCount = 0; track.forEach((seg, i) => { if (seg && seg !== 'Unknown') { trackScore += vehicle[seg] * (posW[i] * 1.2); knownCount++; } }); if (knownCount < 3) { const avg = roads.reduce((s, seg) => s + vehicle[seg], 0) / roads.length; const missingW = posW.reduce((s, w, idx) => (track[idx] === 'Unknown' || !track[idx]) ? s + w : s, 0); trackScore += avg * missingW; } // History/consistency/extras const dyn = (dynRates[candidate] || 0) / 100; const recentRaces = raceHistory.filter(r => r.vehicles.includes(candidate)).slice(-5); const recentWins = recentRaces.filter(r => r.actual === candidate).length; const recentPerformance = recentRaces.length > 0 ? (recentWins / recentRaces.length) * 0.1 : 0; const consistency = vehicle.consistency; const synB = synergyBonus(selectedVehicles); const setupB = setupPrior(setupKey, candidate); const segLift = segmentWinLift(candidate, knownSeg); // Pattern bias from TRAIN-only dist let patBias = 0; if (knownSeg) { const label = labelForVehicleOnKnownSegment(candidate, knownSeg, selectedVehicles); const dist = patDistOverride || { HL: 0.34, LL: 0.33, NC: 0.33 }; const prob = dist[label] || 0.33; const confidence = Math.abs(prob - 0.33) * 3; patBias = (prob - 0.33) * 180 * confidence; } // No device-hour bias in eval const timeB = 0; const w = { track: 0.65, history: 0.20, consistency: 0.08, extras: 0.07 }; const base = (trackScore * w.track) + (dyn * 600 * w.history) + (consistency * 100 * w.consistency) + (recentPerformance * 200); const extras = (synB + setupB + segLift + patBias + timeB) * w.extras; // Odds control const implied = 1 / vehicle.odds; let edge = 1.0; if (implied > 0.45) edge = 0.94; else if (implied > 0.35) edge = 1.0; else if (implied > 0.25) edge = 1.06; else edge = 1.10; let finalScore = (base + extras) * edge; // CSV boost with TEST race's timeSegment + track const csvData = getPatternDataFromCSV(timeSegment, track); if (csvData && knownSeg) { const pattern = getVehiclePatternForSegment(candidate, knownSeg, selectedVehicles); const { boost } = getSmoothedBoosts(csvData, pattern, candidate); finalScore *= boost; } return { vehicle: candidate, finalScore, rawTrack: trackScore, odds: vehicle.odds, consistency: vehicle.consistency }; }); const total = predictions.reduce((s, p) => s + p.finalScore, 0) || 1; predictions.forEach(p => p.probability = (p.finalScore / total) * 100); predictions.sort((a, b) => b.finalScore - a.finalScore); return predictions; } function createVehicleButtons() { const grid = document.getElementById('vehicleGrid'); Object.keys(vehicles).forEach(vehicleName => { const btn = document.createElement('button'); btn.className = 'p-3 bg-gray-800 hover:bg-gray-700 border border-gray-600 rounded-lg transition-all duration-200 text-sm font-medium'; btn.textContent = vehicleName; btn.onclick = () => toggleVehicle(vehicleName, btn); grid.appendChild(btn); }); } function toggleVehicle(vehicleName, btnElement) { if (selectedVehicles.includes(vehicleName)) { selectedVehicles = selectedVehicles.filter(v => v !== vehicleName); btnElement.classList.remove('bg-blue-600', 'border-blue-400'); btnElement.classList.add('bg-gray-800', 'border-gray-600'); } else if (selectedVehicles.length < 3) { selectedVehicles.push(vehicleName); btnElement.classList.remove('bg-gray-800', 'border-gray-600'); btnElement.classList.add('bg-blue-600', 'border-blue-400'); } updateSelectedVehiclesDisplay(); updateCalculateButton(); } function updateSelectedVehiclesDisplay() { document.getElementById('vehicleCount').textContent = `${selectedVehicles.length}/3`; const listEl = document.getElementById('vehicleList'); listEl.innerHTML = selectedVehicles.map(v => `<span class="bg-blue-600 text-white px-2 py-1 rounded text-xs">${v}</span>` ).join(''); } function updateCalculateButton() { const btn = document.getElementById('calculateBtn'); const hasTrack = document.getElementById('leftRoad').value || document.getElementById('middleRoad').value || document.getElementById('rightRoad').value; btn.disabled = selectedVehicles.length !== 3 || !hasTrack; btn.textContent = btn.disabled ? (selectedVehicles.length !== 3 ? `๐Ÿš— Select ${3 - selectedVehicles.length} more vehicle(s)` : '๐Ÿ›ฃ๏ธ Configure track segments') : '๐Ÿงฎ Calculate Winner Probability'; } // ===== Feature Engineering Functions ===== const hourBuckets = { Night: [0, 6], Morning: [6, 12], Afternoon: [12, 18], Evening: [18, 24] }; function hourWeight(hour) { const boosts = { 0: 1.06, 6: 1.04, 10: 1.05, 13: 1.04 }; return boosts[hour] || 1.0; } // ===== Pattern Helper Function ===== // ===== Pattern Helper Function ===== function getVehiclePatternForSegment(vehicleName, segment, allVehicles) { if (!segment || segment === 'Unknown') return null; // Get all vehicles with their stats for this segment const vehicleStats = allVehicles.map(v => ({ name: v, stat: vehicles[v][segment] || 0 })); // Sort by performance on this segment (highest first) vehicleStats.sort((a, b) => b.stat - a.stat); // Find the position of our target vehicle const position = vehicleStats.findIndex(v => v.name === vehicleName); if (position === 0) return 'HL'; if (position === 1) return 'LL'; if (position === 2) return 'NC'; return null; } function synergyBonus(vehicles) { const synKey = [...vehicles].sort().join('|'); const s = agg.synergyWins[synKey]; if (!s || s.total < 6) return 0; const wr = s.wins / s.total; return (wr - 0.33) * 120; } function setupPrior(setupKey, candidate) { const s = agg.setupWins[setupKey]; if (!s || s.total < 6) return 0; const wr = (s.winsBy?.[candidate] || 0) / s.total; return (wr - 0.33) * 150; } function segmentWinLift(vehicle, knownSegment) { if (!knownSegment || knownSegment === 'Unknown') return 0; const k = `${vehicle}|${knownSegment}`; const wins = agg.vehicleOnSegmentWins[k] || 0; const total = (raceHistory.filter(r => r.vehicles.includes(vehicle) && r.track.includes(knownSegment))).length || 1; const wr = wins / total; return (wr - 0.33) * 100; } function nextPatternDistribution() { const seq = raceHistory.map(r => r.pattern).filter(p => p === 'HL' || p === 'LL' || p === 'NC'); if (seq.length < 12) return { HL: 0.34, LL: 0.33, NC: 0.33 }; const k = 3; const map = {}; for (let i = 0; i + k < seq.length; i++) { const ctx = seq.slice(i, i + k).join('|'); const nxt = seq[i + k]; map[ctx] = map[ctx] || { HL: 0, LL: 0, NC: 0 }; map[ctx][nxt]++; } const ctxKey = seq.slice(-k).join('|'); const row = map[ctxKey] || null; if (!row) return { HL: 0.34, LL: 0.33, NC: 0.33 }; const tot = row.HL + row.LL + row.NC || 1; return { HL: row.HL / tot, LL: row.LL / tot, NC: row.NC / tot }; } // === TRAIN-ONLY PATTERN DIST HELPERS (ADD THIS) === function buildPatternSeqFrom(races) { return races.map(r => r?.pattern).filter(p => p === 'HL' || p === 'LL' || p === 'NC'); } function nextPatternDistributionFromSeq(seq, k = 3) { if (!seq || seq.length < k + 1) return { HL: 0.34, LL: 0.33, NC: 0.33 }; const map = {}; for (let i = 0; i + k < seq.length; i++) { const ctx = seq.slice(i, i + k).join('|'); const nxt = seq[i + k]; if (!map[ctx]) map[ctx] = { HL: 0, LL: 0, NC: 0 }; map[ctx][nxt]++; } const ctxKey = seq.slice(-k).join('|'); const row = map[ctxKey] || null; if (!row) return { HL: 0.34, LL: 0.33, NC: 0.33 }; const tot = row.HL + row.LL + row.NC || 1; return { HL: row.HL / tot, LL: row.LL / tot, NC: row.NC / tot }; } function labelForVehicleOnKnownSegment(candidate, knownSegment, trio) { const sorted = trio .map(v => ({ v, stat: vehicles[v][knownSegment] })) .sort((a, b) => b.stat - a.stat) .map(o => o.v); if (candidate === sorted[0]) return 'HL'; if (candidate === sorted[1]) return 'LL'; if (candidate === sorted[2]) return 'NC'; return 'HL'; } // === SMOOTHED CSV BOOSTS (ADD THIS) === const GLOBAL_PATTERN_PRIOR = { HL: 0.38, LL: 0.34, NC: 0.28 }; // adjust to your global mix function getSmoothedBoosts(csvData, patternLabel, vehicleName, alpha = 10) { if (!csvData || !patternLabel) return { boost: 1.0 }; // Pattern smoothing const nPattern = Math.max(csvData.total || 20, 1); const rawPatP = (csvData[patternLabel] ?? 33.3) / 100; const priorPatP = GLOBAL_PATTERN_PRIOR[patternLabel] ?? (1/3); const patCount = rawPatP * nPattern; const smoothedPatP = (patCount + alpha * priorPatP) / (nPattern + alpha); // Vehicle smoothing within this context const tv = csvData.topVehicles || {}; const k = Object.keys(tv).length || 4; const avg = 1 / k; const nVeh = Math.max(csvData.topVehiclesTotal || nPattern, 1); const rawVehP = ((tv[vehicleName] ?? 0) / 100); const vehCount = rawVehP * nVeh; const smoothedVehP = (vehCount + alpha * avg) / (nVeh + alpha); // Relative boosts const patternBoost = smoothedPatP / (Object.values(GLOBAL_PATTERN_PRIOR).reduce((a,b)=>a+b,0)/3 || 0.3333); const vehicleBoost = smoothedVehP / avg; // Soft caps const totalBoost = Math.max(0.85, Math.min(patternBoost * vehicleBoost, 1.25)); return { boost: totalBoost }; } // ===== Advanced Prediction (existing) ===== // Enhanced prediction algorithm with better tie-breaking function calculateAdvancedPrediction() { const track = [ document.getElementById('leftRoad').value || 'Unknown', document.getElementById('middleRoad').value || 'Unknown', document.getElementById('rightRoad').value || 'Unknown' ]; const setupKey = track.join(' | '); const knownIdx = track.findIndex(s => s && s !== 'Unknown'); const knownSeg = knownIdx >= 0 ? track[knownIdx] : null; const dynRates = calculateDynamicVictoryRates(); // Adjusted position weights with more emphasis on known segments const posW = [0.35, 0.30, 0.35]; const patDist = nextPatternDistribution(); const hour = new Date().getHours(); const hourW = hourWeight(hour); const predictions = selectedVehicles.map(candidate => { const vehicle = vehicles[candidate]; // 1) Track score with unknown imputation - ENHANCED let trackScore = 0, knownCount = 0; track.forEach((seg, i) => { if (seg && seg !== 'Unknown') { // Apply bonus for known segments const segmentWeight = posW[i] * 1.2; // 20% bonus for known segments trackScore += vehicle[seg] * segmentWeight; knownCount++; } }); if (knownCount < 3) { const avg = roads.reduce((s, seg) => s + vehicle[seg], 0) / roads.length; const missingW = posW.reduce((s, w, idx) => (track[idx] === 'Unknown' || !track[idx]) ? s + w : s, 0); trackScore += avg * missingW; } // 2) Dynamic history factor with recency bias const dyn = (dynRates[candidate] || 0) / 100; // Recent performance bonus (last 5 races) const recentRaces = raceHistory.filter(r => r.vehicles.includes(candidate)).slice(-5); const recentWins = recentRaces.filter(r => r.actual === candidate).length; const recentPerformance = recentRaces.length > 0 ? (recentWins / recentRaces.length) * 0.1 : 0; // 3) Consistency with pattern consideration const consistency = vehicle.consistency; // 4) Synergy bonus with recent performance consideration const synB = synergyBonus(selectedVehicles); // 5) Setup prior with recency adjustment const setupB = setupPrior(setupKey, candidate); // 6) Segment-specific win lift with pattern consideration const segLift = segmentWinLift(candidate, knownSeg); // 7) Pattern prior bias - ENHANCED with confidence weighting let patBias = 0; if (knownSeg) { const label = labelForVehicleOnKnownSegment(candidate, knownSeg, selectedVehicles); const prob = patDist[label] || 0.33; // Apply stronger bias for higher confidence predictions const confidence = Math.abs(prob - 0.33) * 3; // 0 to 1 multiplier patBias = (prob - 0.33) * 180 * confidence; } // 8) Time-of-day impact const timeB = (hourW - 1.0) * 120; // 9) Combine with weights - ADJUSTED for better balance const w = { track: 0.65, // Slightly reduced to give more weight to other factors history: 0.20, // Increased for better historical consideration consistency: 0.08, extras: 0.07 }; const base = (trackScore * w.track) + (dyn * 600 * w.history) + (consistency * 100 * w.consistency) + (recentPerformance * 200); // Added recent performance const extras = (synB + setupB + segLift + patBias + timeB) * w.extras; // 10) Odds control with pattern consideration const implied = 1 / vehicle.odds; // More nuanced edge calculation based on pattern let edge = 1.0; if (implied > 0.45) edge = 0.94; else if (implied > 0.35) edge = 1.0; else if (implied > 0.25) edge = 1.06; else edge = 1.10; const finalScore = (base + extras) * edge; // ===== CSV DATA BOOST ===== // Get current time and track data from CSV // ===== CSV DATA BOOST (REPLACE OLD BLOCK) ===== const csvData = getPatternDataFromCSV(); // honors UI timeSlot or current time if (csvData && knownSeg) { const pattern = getVehiclePatternForSegment(candidate, knownSeg, selectedVehicles); const { boost } = getSmoothedBoosts(csvData, pattern, candidate); finalScore *= boost; } return { vehicle: candidate, rawTrack: trackScore, finalScore, components: { base, extras, dyn, synB, setupB, segLift, patBias, timeB, recentPerformance }, odds: vehicle.odds, consistency: vehicle.consistency }; }); // Compute probabilities const total = predictions.reduce((s, p) => s + p.finalScore, 0) || 1; predictions.forEach(p => p.probability = (p.finalScore / total) * 100); // Sort by final score predictions.sort((a, b) => b.finalScore - a.finalScore); // ENHANCED TIE-BREAKER with multiple factors if (predictions.length >= 2) { const first = predictions[0]; const second = predictions[1]; // Consider it a close race if within 5% of each other const closeThreshold = 0.95; // 95% of first place score if (second.finalScore > first.finalScore * closeThreshold) { console.log('Close race detected between', first.vehicle, 'and', second.vehicle); // Create a tie-breaker score based on multiple factors const firstTieBreaker = calculateTieBreakerScore(first, track, knownSeg); const secondTieBreaker = calculateTieBreakerScore(second, track, knownSeg); console.log('Tie-breaker scores:', first.vehicle, firstTieBreaker, second.vehicle, secondTieBreaker); // If second has a significantly better tie-breaker score, swap them if (secondTieBreaker > firstTieBreaker * 1.1) { // 10% threshold console.log('Swapping positions based on tie-breaker'); [predictions[0], predictions[1]] = [predictions[1], predictions[0]]; } } } return predictions; } // New function to calculate tie-breaker score function calculateTieBreakerScore(prediction, track, knownSegment) { const vehicle = vehicles[prediction.vehicle]; let score = 0; // 1. Performance on known segments (weighted higher) track.forEach((seg, idx) => { if (seg && seg !== 'Unknown') { // Higher weight for known segments in tie-breaking const positionWeight = [0.4, 0.35, 0.25][idx] || 0.33; score += vehicle[seg] * positionWeight * 1.5; } }); // 2. Consistency bonus score += vehicle.consistency * 50; // 3. Recent performance bonus (last 3 races) const recentRaces = raceHistory.filter(r => r.vehicles.includes(prediction.vehicle)).slice(-3); if (recentRaces.length > 0) { const recentWins = recentRaces.filter(r => r.actual === prediction.vehicle).length; score += (recentWins / recentRaces.length) * 80; } // 4. Known segment specialization if (knownSegment) { const segmentRaces = raceHistory.filter(r => r.vehicles.includes(prediction.vehicle) && r.track.includes(knownSegment) ); if (segmentRaces.length > 0) { const segmentWins = segmentRaces.filter(r => r.actual === prediction.vehicle).length; score += (segmentWins / segmentRaces.length) * 60; } } return score; } // Enhanced function to update victory rates with recency bias function calculateDynamicVictoryRates() { const dynamicRates = {}; Object.keys(vehicles).forEach(vehicleName => { const vehicleRaces = raceHistory.filter(race => race.vehicles.includes(vehicleName)); if (vehicleRaces.length === 0) { dynamicRates[vehicleName] = vehicles[vehicleName].baseVictory * 0.3; } else { // Calculate with recency bias (more recent races weighted higher) let totalWeight = 0; let weightedWins = 0; let weightedSeconds = 0; vehicleRaces.forEach((race, index) => { // Recent races get higher weight (linear decay) const weight = 1 + (index / vehicleRaces.length); // 1 to 2 weight range totalWeight += weight; if (race.actual === vehicleName) { weightedWins += weight; } else if (race.second === vehicleName) { weightedSeconds += weight * 0.7; // Second place gets 70% value } }); const actualWinRate = (weightedWins / totalWeight) * 100; const actualSecondRate = (weightedSeconds / totalWeight) * 100; // Dynamic weighting based on number of races const dataWeight = Math.min(vehicleRaces.length / 15, 0.8); // Cap at 80% const baseWeight = 1 - dataWeight; dynamicRates[vehicleName] = (actualWinRate * dataWeight) + (vehicles[vehicleName].baseVictory * baseWeight * 0.3) + (actualSecondRate * 0.25); } }); return dynamicRates; } // Keep the old version as backup function calculateAdvancedPrediction_legacy() { const track = [ document.getElementById('leftRoad').value, document.getElementById('middleRoad').value, document.getElementById('rightRoad').value ]; const dynamicVictoryRates = calculateDynamicVictoryRates(); const positionWeights = [0.35, 0.25, 0.40]; // left, middle, right const predictions = selectedVehicles.map(vehicleName => { const vehicle = vehicles[vehicleName]; let totalScore = 0; let knownSegments = 0; track.forEach((roadType, index) => { if (roadType) { totalScore += vehicle[roadType] * positionWeights[index]; knownSegments++; } }); const unknownSegments = 3 - knownSegments; if (unknownSegments > 0) { const avgRoadScore = roads.reduce((sum, road) => sum + vehicle[road], 0) / roads.length; const unknownWeight = positionWeights.slice().reduce((sum, weight, index) => track[index] ? sum : sum + weight, 0); totalScore += avgRoadScore * unknownWeight; } const dynamicHistoricalFactor = dynamicVictoryRates[vehicleName] / 100; const consistencyBonus = vehicle.consistency; let trackSpecificBonus = 0; const trackSignature = track.map(t => t || 'Unknown').join('-'); const specificTrackRaces = raceHistory.filter(r => r.vehicles.includes(vehicleName) && r.track.join('-') === trackSignature ); if (specificTrackRaces.length > 0) { const trackWins = specificTrackRaces.filter(r => r.actual === vehicleName).length; const trackSeconds = specificTrackRaces.filter(r => r.second === vehicleName).length; const trackWinRate = trackWins / specificTrackRaces.length; const trackSecondRate = trackSeconds / specificTrackRaces.length; trackSpecificBonus = (trackWinRate * 200) + (trackSecondRate * 75); } const trackWeight = 0.75; const historyWeight = Math.max(0.15, 0.4 - (raceHistory.length * 0.01)); const consistencyWeight = 0.1; const combinedScore = (totalScore * trackWeight) + (dynamicHistoricalFactor * 600 * historyWeight) + (consistencyBonus * 100 * consistencyWeight) + (trackSpecificBonus * 0.1); const impliedProb = 1 / vehicle.odds; const edgeValue = impliedProb > 0.45 ? 0.95 : (impliedProb > 0.35 ? 1.0 : 1.05); return { vehicle: vehicleName, rawScore: totalScore, finalScore: combinedScore * edgeValue, odds: vehicle.odds, consistency: vehicle.consistency, dynamicWinRate: dynamicVictoryRates[vehicleName], trackSpecificBonus: trackSpecificBonus }; }); const totalFinalScore = predictions.reduce((sum, p) => sum + p.finalScore, 0); predictions.forEach(p => { p.probability = (p.finalScore / totalFinalScore) * 100; }); predictions.sort((a, b) => b.probability - a.probability); return predictions; } // Replace the displayResults function with this corrected version function displayResults(predictions) { const container = document.getElementById('resultsContainer'); currentPrediction = predictions; const winner = predictions[0]; const isCloseRace = predictions.length > 1 && predictions[1].probability > winner.probability * 0.8; let html = ` <div class="winner-glow p-4 rounded-lg bg-gradient-to-r from-green-600/20 to-blue-600/20 border border-green-500/30"> <h3 class="text-lg font-bold text-green-400 mb-2">๐Ÿ† AI Prediction</h3> <div class="text-xl font-bold text-white mb-1">${winner.vehicle}</div> <div class="text-green-300">${winner.probability.toFixed(1)}% win probability</div> <div class="text-sm text-gray-300 mt-1">Odds: ${winner.odds}:1 | Confidence: ${winner.consistency > 0.8 ? 'High' : 'Medium'}</div> <!-- Explainability chips --> <div class="mt-2 text-xs flex flex-wrap gap-1"> ${winner.rawTrack ? ` <span class="bg-blue-600/30 px-1.5 py-0.5 rounded">Track:${winner.rawTrack.toFixed(1)}</span> ` : ''} ${winner.dynamicWinRate ? ` <span class="bg-purple-600/30 px-1.5 py-0.5 rounded">History:${winner.dynamicWinRate.toFixed(1)}%</span> ` : ''} ${winner.consistency ? ` <span class="bg-indigo-600/30 px-1.5 py-0.5 rounded">Consistency:${(winner.consistency * 100).toFixed(0)}%</span> ` : ''} </div> ${isCloseRace ? ` <div class="mt-2 p-2 bg-yellow-600/20 border border-yellow-500/30 rounded text-yellow-300 text-sm"> โš ๏ธ Close race - consider hedging bets </div> ` : ''} </div> <div class="mt-4"> <h4 class="text-md font-semibold text-gray-300 mb-2">Full Prediction</h4> `; predictions.forEach((p, i) => { const oddsImplied = (1 / p.odds * 100).toFixed(1); // Determine pattern for this vehicle based on known segment let patternBadge = ''; const track = [ document.getElementById('leftRoad').value || 'Unknown', document.getElementById('middleRoad').value || 'Unknown', document.getElementById('rightRoad').value || 'Unknown' ]; const knownSegment = track.find(s => s && s !== 'Unknown'); if (knownSegment) { const pattern = getVehiclePatternForSegment(p.vehicle, knownSegment, selectedVehicles); if (pattern) { const patternEmoji = pattern === 'HL' ? '๐ŸŸข' : pattern === 'LL' ? '๐ŸŸก' : '๐Ÿ”ด'; patternBadge = `<span class="ml-2 ${pattern === 'HL' ? 'text-green-400' : pattern === 'LL' ? 'text-yellow-400' : 'text-red-400'}">${patternEmoji} ${pattern}</span>`; } } html += ` <div class="result-row flex justify-between items-center p-2 ${i === 0 ? 'bg-green-900/20' : 'bg-gray-800/30'} rounded mb-1"> <div class="flex items-center"> <span class="text-sm font-medium text-gray-200">${i + 1}. ${p.vehicle}</span> ${patternBadge} </div> <div class="text-right"> <span class="text-sm font-bold ${i === 0 ? 'text-green-300' : 'text-gray-300'}">${p.probability.toFixed(1)}%</span> <div class="text-xs text-gray-400">${p.odds}:1 (${oddsImplied}% implied)</div> </div> </div> `; }); html += `</div>`; container.innerHTML = html; document.getElementById('advancedToggle').classList.remove('hidden'); showWinnerSelection(); } // Also update the calculateAdvancedPrediction function to ensure it returns the right properties function calculateAdvancedPrediction() { const track = [ document.getElementById('leftRoad').value || 'Unknown', document.getElementById('middleRoad').value || 'Unknown', document.getElementById('rightRoad').value || 'Unknown' ]; const setupKey = track.join(' | '); const knownIdx = track.findIndex(s => s && s !== 'Unknown'); const knownSeg = knownIdx >= 0 ? track[knownIdx] : null; const dynRates = calculateDynamicVictoryRates(); // Adjusted position weights with more emphasis on known segments const posW = [0.35, 0.30, 0.35]; const patDist = nextPatternDistribution(); const hour = new Date().getHours(); const hourW = hourWeight(hour); const predictions = selectedVehicles.map(candidate => { const vehicle = vehicles[candidate]; // 1) Track score with unknown imputation - ENHANCED let trackScore = 0, knownCount = 0; track.forEach((seg, i) => { if (seg && seg !== 'Unknown') { // Apply bonus for known segments const segmentWeight = posW[i] * 1.2; // 20% bonus for known segments trackScore += vehicle[seg] * segmentWeight; knownCount++; } }); if (knownCount < 3) { const avg = roads.reduce((s, seg) => s + vehicle[seg], 0) / roads.length; const missingW = posW.reduce((s, w, idx) => (track[idx] === 'Unknown' || !track[idx]) ? s + w : s, 0); trackScore += avg * missingW; } // 2) Dynamic history factor with recency bias const dyn = (dynRates[candidate] || 0) / 100; // Recent performance bonus (last 5 races) const recentRaces = raceHistory.filter(r => r.vehicles.includes(candidate)).slice(-5); const recentWins = recentRaces.filter(r => r.actual === candidate).length; const recentPerformance = recentRaces.length > 0 ? (recentWins / recentRaces.length) * 0.1 : 0; // 3) Consistency with pattern consideration const consistency = vehicle.consistency; // 4) Synergy bonus with recent performance consideration const synB = synergyBonus(selectedVehicles); // 5) Setup prior with recency adjustment const setupB = setupPrior(setupKey, candidate); // 6) Segment-specific win lift with pattern consideration const segLift = segmentWinLift(candidate, knownSeg); // 7) Pattern prior bias - ENHANCED with confidence weighting let patBias = 0; if (knownSeg) { const label = labelForVehicleOnKnownSegment(candidate, knownSeg, selectedVehicles); const prob = patDist[label] || 0.33; // Apply stronger bias for higher confidence predictions const confidence = Math.abs(prob - 0.33) * 3; // 0 to 1 multiplier patBias = (prob - 0.33) * 180 * confidence; } // 8) Time-of-day impact const timeB = (hourW - 1.0) * 120; // 9) Combine with weights - ADJUSTED for better balance const w = { track: 0.65, // Slightly reduced to give more weight to other factors history: 0.20, // Increased for better historical consideration consistency: 0.08, extras: 0.07 }; const base = (trackScore * w.track) + (dyn * 600 * w.history) + (consistency * 100 * w.consistency) + (recentPerformance * 200); // Added recent performance const extras = (synB + setupB + segLift + patBias + timeB) * w.extras; // 10) Odds control with pattern consideration const implied = 1 / vehicle.odds; // More nuanced edge calculation based on pattern let edge = 1.0; if (implied > 0.45) edge = 0.94; else if (implied > 0.35) edge = 1.0; else if (implied > 0.25) edge = 1.06; else edge = 1.10; let finalScore = (base + extras) * edge; // ===== CSV DATA BOOST ===== // Get current time and track data from CSV const csvData = getPatternDataFromCSV(); if (csvData) { // Check what pattern this vehicle would have (HL, LL, or NC) const pattern = getVehiclePatternForSegment(candidate, knownSeg, selectedVehicles); if (pattern) { // How often this pattern happens (e.g., HL happens 39% of the time) const patternProb = csvData[pattern] || 33.3; // Calculate boost (39% รท 33.3% = 1.17x boost) const patternBoost = patternProb / 33.3; // Check if this vehicle is good for this pattern const vehiclePercentage = csvData.topVehicles[candidate] || 0; const averagePercentage = 100 / Object.keys(csvData.topVehicles).length; const vehicleBoost = vehiclePercentage > 0 ? (vehiclePercentage / averagePercentage) : 1.0; // Combine both boosts const totalBoost = patternBoost * vehicleBoost; // Apply the boost to the final score! finalScore *= totalBoost; } } return { vehicle: candidate, rawTrack: trackScore, // Changed from rawScore to rawTrack for consistency finalScore, components: { base, extras, dyn, synB, setupB, segLift, patBias, timeB, recentPerformance }, odds: vehicle.odds, consistency: vehicle.consistency, dynamicWinRate: dynRates[candidate] // Add this for display }; }); // Compute probabilities const total = predictions.reduce((s, p) => s + p.finalScore, 0) || 1; predictions.forEach(p => p.probability = (p.finalScore / total) * 100); // Sort by final score predictions.sort((a, b) => b.finalScore - a.finalScore); // ENHANCED TIE-BREAKER with multiple factors if (predictions.length >= 2) { const first = predictions[0]; const second = predictions[1]; // Consider it a close race if within 5% of each other const closeThreshold = 0.95; // 95% of first place score if (second.finalScore > first.finalScore * closeThreshold) { console.log('Close race detected between', first.vehicle, 'and', second.vehicle); // Create a tie-breaker score based on multiple factors const firstTieBreaker = calculateTieBreakerScore(first, track, knownSeg); const secondTieBreaker = calculateTieBreakerScore(second, track, knownSeg); console.log('Tie-breaker scores:', first.vehicle, firstTieBreaker, second.vehicle, secondTieBreaker); // If second has a significantly better tie-breaker score, swap them if (secondTieBreaker > firstTieBreaker * 1.1) { // 10% threshold console.log('Swapping positions based on tie-breaker'); [predictions[0], predictions[1]] = [predictions[1], predictions[0]]; } } } return predictions; } function showAdvancedResults() { if (!currentPrediction) return; const content = document.getElementById('advancedContent'); let html = `<div class="space-y-3 text-sm"> <h4 class="font-semibold text-purple-300">Adaptive AI Analysis - Learning from YOUR races:</h4>`; currentPrediction.forEach((p, index) => { const expectedPosition = index + 1; const positionEmoji = expectedPosition === 1 ? '๐Ÿฅ‡' : expectedPosition === 2 ? '๐Ÿฅˆ' : '๐Ÿฅ‰'; const raceCount = raceHistory.filter(r => r.vehicles.includes(p.vehicle)).length; const learningStatus = raceCount < 5 ? '๐Ÿ”„ Learning' : raceCount < 15 ? '๐Ÿ“ˆ Adapting' : '๐ŸŽฏ Optimized'; html += ` <div class="bg-gray-700 rounded p-3"> <div class="font-medium text-white mb-1">${positionEmoji} ${p.vehicle} - Expected ${expectedPosition}${expectedPosition === 1 ? 'st' : expectedPosition === 2 ? 'nd' : 'rd'} ${learningStatus}</div> <div class="grid grid-cols-2 gap-2 text-xs"> <div>Track Score: ${p.rawTrack?.toFixed(1) ?? 'โ€”'}</div> <div>Final Score: ${p.finalScore.toFixed(1)}</div> <div>Base Win Rate: ${vehicles[p.vehicle].baseVictory}%</div> <div class="text-green-400">Your Data: ${p.dynamicWinRate.toFixed(1)}%</div> ${p.trackSpecificBonus > 0 ? `<div class="col-span-2 text-blue-400">Track Specialist: +${p.trackSpecificBonus.toFixed(1)} (proven on this setup)</div>` : ''} <div class="col-span-2 text-yellow-400">Races Analyzed: ${raceCount}</div> </div> <div class="mt-2"> <div class="text-purple-300">Strategy: ${p.probability > 40 ? 'Strong Pick' : p.probability > 25 ? 'Value Bet' : 'Dark Horse'}</div> </div> </div>`; }); const totalRaces = raceHistory.length; const learningPhase = totalRaces < 10 ? 'Initial Learning' : totalRaces < 30 ? 'Pattern Recognition' : 'Expert Mode'; html += ` <div class="bg-green-900/30 rounded p-3 mt-3"> <h5 class="font-semibold text-green-300 mb-2">๐Ÿง  AI Learning Status: ${learningPhase}</h5> <div class="text-xs space-y-1"> <div>Total Races Analyzed: <span class="font-medium">${totalRaces}</span></div> <div>Historical Data Weight: <span class="font-medium">${Math.max(15, 40 - totalRaces).toFixed(0)}%</span> (decreasing as AI learns)</div> <div>Track Performance Weight: <span class="font-medium">75%</span></div> <div class="mt-2 text-green-300"> ${totalRaces < 10 ? '๐Ÿ“š AI is learning your game patterns...' : totalRaces < 30 ? '๐ŸŽฏ AI is adapting to your specific racing dynamics' : '๐Ÿ† AI has expert-level knowledge of your races'} </div> </div> </div>`; html += `</div>`; content.innerHTML = html; } // ===== Winner & Result Recording ===== function showWinnerSelection() { const winnerDiv = document.getElementById('winnerSelection'); const buttonsDiv = document.getElementById('winnerButtons'); buttonsDiv.innerHTML = selectedVehicles.map(vehicle => `<button class="bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded transition-colors text-sm" onclick="selectWinner('${vehicle}')">${vehicle}</button>` ).join(''); winnerDiv.classList.remove('hidden'); document.getElementById('secondPlaceSelection').classList.add('hidden'); } function selectWinner(winner) { currentWinner = winner; document.getElementById('winnerButtons').style.opacity = '0.5'; const winnerButtons = document.querySelectorAll('#winnerButtons button'); winnerButtons.forEach(btn => btn.disabled = true); const secondPlaceDiv = document.getElementById('secondPlaceSelection'); const secondButtonsDiv = document.getElementById('secondPlaceButtons'); const remainingVehicles = selectedVehicles.filter(v => v !== winner); secondButtonsDiv.innerHTML = remainingVehicles.map(vehicle => `<button class="bg-orange-600 hover:bg-orange-700 text-white px-3 py-1 rounded transition-colors text-sm" onclick="selectSecondPlace('${vehicle}')">${vehicle}</button>` ).join(''); secondPlaceDiv.classList.remove('hidden'); } function selectSecondPlace(second) { const third = selectedVehicles.find(v => v !== currentWinner && v !== second); recordCompleteRace(currentWinner, second, third); } function recordCompleteRace(winner, second, third) { const now = new Date(); const record = { timestamp: now.toLocaleString(), isoTimestamp: now.toISOString(), vehicles: [...selectedVehicles], track: [ document.getElementById('leftRoad').value || 'Unknown', document.getElementById('middleRoad').value || 'Unknown', document.getElementById('rightRoad').value || 'Unknown' ], predicted: currentPrediction[0].vehicle, actual: winner, second: second, third: third, correct: currentPrediction[0].vehicle === winner, secondCorrect: currentPrediction[1].vehicle === second, rankings: [winner, second, third], pattern: null, streakCount: 0, timeSegment: getTimeSegment(now) }; // Schema hardening record.id = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`; record.dateKey = record.isoTimestamp.slice(0, 10); record.hour = new Date(record.isoTimestamp).getHours(); record.setupKey = record.track.join(" | "); record.vehiclesSorted = [...record.vehicles].sort(); record.rankingsIdx = { [record.actual]: 0, [record.second]: 1, [record.third]: 2 }; classifyPattern(record); raceHistory.push(record); // Update aggregates updateAggFromRecord(record); analyzeTrends(); updateHistoryDisplay(); renderPatternTimeline(); saveToStorage(); // Reset selection UI document.getElementById('winnerSelection').classList.add('hidden'); document.getElementById('winnerButtons').style.opacity = '1'; document.querySelectorAll('#winnerButtons button').forEach(btn => btn.disabled = false); currentWinner = null; // Feedback message let message = ''; if (record.correct && record.secondCorrect) { message = `๐ŸŽ‰ Perfect! AI predicted ${winner} to win and ${second} for second place!`; } else if (record.correct) { message = `๐ŸŽฏ Winner correct! AI predicted ${winner} would win. Learning from second place: ${second}`; } else if (record.secondCorrect) { message = `๐Ÿ“Š Interesting! AI missed winner but correctly predicted ${second} for second place.`; } else { message = `๐Ÿ“ˆ Learning mode: Predicted ${record.predicted}. Actual: ${winner} (2nd: ${second}).`; } showNotification(message, record.correct ? 'success' : 'info'); // Try a trend prediction toast const alertMsg = predictNextTrend(); if (alertMsg) showNotification(alertMsg, 'warning'); } // ===== Pattern Classification ===== function classifyPattern(r) { // Determine the revealed (known) segment: use the first non-Unknown slot const idx = r.track.findIndex(seg => seg && seg !== 'Unknown'); if (idx === -1) { r.pattern = 'Random'; return r.pattern; } const segment = r.track[idx]; // Build stats for only the 3 vehicles in this race const trio = r.vehicles.map(v => ({ name: v, stat: vehicles[v][segment] })); trio.sort((a,b) => b.stat - a.stat); // highest first const highest = trio[0].name; const second = trio[1].name; const lowest = trio[2].name; if (r.actual === highest) r.pattern = 'HL'; else if (r.actual === second) r.pattern = 'LL'; else if (r.actual === lowest) r.pattern = 'NC'; else r.pattern = 'Random'; // safety fallback return r.pattern; } // ===== Trend Analysis ===== function analyzeTrends() { // Compute streakCount ignoring 'Random' as members; Random ends a streak (gap) let lastPattern = null; let count = 0; // Ensure chronological order raceHistory.sort((a,b) => new Date(a.isoTimestamp) - new Date(b.isoTimestamp)); raceHistory.forEach(r => { if (!r.pattern) classifyPattern(r); if (r.pattern === 'Random') { r.streakCount = 0; lastPattern = null; // gap ends streak count = 0; } else { if (lastPattern === r.pattern) { count += 1; } else { lastPattern = r.pattern; count = 1; } r.streakCount = count; } }); saveToStorage(); updatePatternSummary(); // keeps header stats in sync } // ===== Predictor ===== function predictNextTrend() { if (raceHistory.length < 3) return null; const last = raceHistory[raceHistory.length - 1]; if (last.pattern === 'Random') return null; if (last.streakCount !== 1) return null; // Only when a new streak just started const target = last.pattern; // HL / LL / NC // Count historical situations where a streak started with single target, and whether it extended to >=2 next (next non-random same pattern) let total = 0; let favorable = 0; for (let i=0; i<raceHistory.length-1; i++) { const r = raceHistory[i]; if (r.pattern !== target || r.streakCount !== 1) continue; // Find next non-random after i let j = i+1; while (j < raceHistory.length && raceHistory[j].pattern === 'Random') j++; if (j >= raceHistory.length) continue; total++; if (raceHistory[j].pattern === target) favorable++; } if (total === 0) return null; const prob = Math.round((favorable / total) * 100); if (prob >= 60) { return `Pattern Alert: A new ${target} trend is likely to begin. Probability: ${prob}%`; } return null; } // ===== Time Segment Helper ===== function getTimeSegment(dateObj) { const h = dateObj.getHours(); if (h >= 0 && h < 6) return 'Night'; if (h >= 6 && h < 12) return 'Morning'; if (h >= 12 && h < 18) return 'Afternoon'; return 'Evening'; // 18:00 - 24:00 } // ===== Pattern Timeline Rendering & Filters ===== function renderPatternTimeline() { const timeline = document.getElementById('patternTimeline'); if (!timeline) return; const { filtered, label } = getFilteredRecords(); timeline.innerHTML = ''; if (filtered.length === 0) { timeline.innerHTML = `<div class="text-gray-400 text-sm p-3">No races found for ${label}.</div>`; updatePatternSummary(); // updates summary to reflect zero result return; } // Chronological filtered.sort((a,b) => new Date(a.isoTimestamp) - new Date(b.isoTimestamp)); filtered.forEach((r, idx) => { const span = document.createElement('span'); span.className = 'dot'; const emoji = DOTS[r.pattern] || 'โšช'; span.textContent = emoji; const dateLocal = new Date(r.isoTimestamp).toLocaleString(); const streakText = r.pattern === 'Random' ? 'โ€”' : r.streakCount; span.title = `${dateLocal} | Winner: ${r.actual} | Pattern: ${r.pattern} | Streak: ${streakText}`; timeline.appendChild(span); }); updatePatternSummary(); } function getFilteredRecords() { const dateVal = document.getElementById('dateFilter').value; // YYYY-MM-DD or '' const slotVal = document.getElementById('timeSlotFilter').value; // All | Night | Morning | Afternoon | Evening const labelParts = []; let records = [...raceHistory]; if (dateVal) { labelParts.push(new Date(dateVal).toDateString()); records = records.filter(r => { const d = new Date(r.isoTimestamp); // format yyyy-mm-dd with leading zeros const yyyy = d.getFullYear(); const mm = String(d.getMonth()+1).padStart(2,'0'); const dd = String(d.getDate()).padStart(2,'0'); const key = `${yyyy}-${mm}-${dd}`; return key === dateVal; }); } if (slotVal && slotVal !== 'All') { labelParts.push(`(${slotVal})`); records = records.filter(r => r.timeSegment === slotVal); } return { filtered: records, label: labelParts.join(' ') || 'All time' }; } function updatePatternSummary() { const summary = document.getElementById('patternSummary'); const { filtered, label } = getFilteredRecords(); if (filtered.length === 0) { summary.textContent = `On ${label}: 0 races.`; return; } const counts = { HL:0, LL:0, NC:0, Random:0 }; filtered.forEach(r => counts[r.pattern] = (counts[r.pattern] || 0) + 1); // Current streak in filtered view: compute as in analyzeTrends but only within filtered let currentStreak = { pattern: 'โ€”', count: 0 }; // Sort chronological filtered.sort((a,b) => new Date(a.isoTimestamp) - new Date(b.isoTimestamp)); let lastPattern = null, count = 0; filtered.forEach(r => { if (r.pattern === 'Random') { lastPattern = null; count = 0; return; } if (lastPattern === r.pattern) count++; else { lastPattern = r.pattern; count = 1; } currentStreak = { pattern: r.pattern, count }; }); summary.textContent = `On ${label}: ${filtered.length} Races | HL: ${counts.HL} | LL: ${counts.LL} | NC: ${counts.NC} | Current Streak: ${currentStreak.pattern} x${currentStreak.count}`; } // ===== History Panel ===== function updateHistoryDisplay() { const container = document.getElementById('historyContainer'); if (raceHistory.length === 0) { container.innerHTML = '<p class="text-gray-400">No races completed yet. Results will appear here to improve future predictions.</p>'; return; } const winnerAccuracy = (raceHistory.filter(r => r.correct).length / raceHistory.length * 100).toFixed(1); const secondPlaceAccuracy = raceHistory.filter(r => r.secondCorrect).length > 0 ? (raceHistory.filter(r => r.secondCorrect).length / raceHistory.length * 100).toFixed(1) : 'N/A'; let html = ` <div class="mb-4 p-3 bg-gray-800 rounded-lg"> <h3 class="font-bold text-green-400">๐ŸŽฏ AI Performance</h3> <div class="grid grid-cols-2 gap-4 text-sm mt-2"> <div><span class="text-gray-300">Winner Accuracy:</span><span class="text-green-400 font-bold ml-2">${winnerAccuracy}%</span></div> <div><span class="text-gray-300">Second Place:</span><span class="text-blue-400 font-bold ml-2">${secondPlaceAccuracy}%</span></div> </div> <p class="text-sm text-gray-300 mt-1">${raceHistory.length} races analyzed โ€ข Enhanced learning with ranking & pattern data</p> </div> <div class="space-y-2 max-h-48 overflow-y-auto">`; raceHistory.slice(-10).reverse().forEach(record => { const winnerIcon = record.correct ? 'โœ…' : 'โŒ'; const secondIcon = record.secondCorrect ? '๐Ÿฅˆ' : 'โšช'; const dot = DOTS[record.pattern] || 'โšช'; html += ` <div class="bg-gray-800 rounded p-2 text-xs"> <div class="flex justify-between items-start gap-3"> <div> <span class="font-medium">${record.timestamp}</span> <div class="text-gray-400">Track: ${record.track.join(' โ†’ ')}</div> <div class="text-gray-300">Vehicles: ${record.vehicles.join(', ')}</div> <div class="text-gray-300">Pattern: <span title="${record.pattern} trend position">${dot} ${record.pattern}${record.streakCount?` x${record.streakCount}`:''}</span> | Time: ${record.timeSegment}</div> </div> <div class="text-right"> <div class="flex gap-1">${winnerIcon} ${secondIcon}</div> <div class="text-gray-400">P: ${record.predicted}</div> <div class="text-white">W: ${record.actual}</div> ${record.second ? `<div class="text-orange-300">2nd: ${record.second}</div>` : ''} </div> </div> </div>`; }); html += '</div>'; container.innerHTML = html; } // ===== Toasts ===== function showNotification(message, type) { const notification = document.createElement('div'); const bgColor = { 'success': 'bg-green-600', 'error': 'bg-red-600', 'warning': 'bg-yellow-600', 'info': 'bg-blue-600' }[type] || 'bg-blue-600'; notification.className = `fixed top-4 right-4 p-4 rounded-lg text-white z-50 ${bgColor} transform translate-x-full transition-transform duration-300`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); setTimeout(() => { notification.style.transform = 'translateX(full)'; setTimeout(() => notification.remove(), 300); }, 5000); } // ===== Data Management (Export/Import/Clear) ===== function exportRaceHistory() { if (raceHistory.length === 0) { showNotification('No race history to export. Complete some races first!', 'warning'); return; } const exportData = { version: '2.0-patterns', exportDate: new Date().toISOString(), totalRaces: raceHistory.length, accuracy: (raceHistory.filter(r => r.correct).length / raceHistory.length * 100).toFixed(1), raceHistory: raceHistory }; const dataStr = JSON.stringify(exportData, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(dataBlob); link.download = `race-history-${new Date().toISOString().split('T')[0]}.json`; link.click(); showNotification(`๐Ÿ“ Exported ${raceHistory.length} races successfully!`, 'success'); } function importRaceHistory(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const importData = JSON.parse(e.target.result); if (!importData.raceHistory || !Array.isArray(importData.raceHistory)) { throw new Error('Invalid file format'); } const validEntries = importData.raceHistory.filter(record => { return record.timestamp && record.vehicles && record.predicted && record.actual; }); if (validEntries.length === 0) throw new Error('No valid race records found'); const existingKeys = new Set(raceHistory.map(r => r.isoTimestamp || r.timestamp)); const newEntries = validEntries .filter(r => !existingKeys.has(r.isoTimestamp || r.timestamp)) .map(r => { // Normalize/backfill if (!r.isoTimestamp) r.isoTimestamp = new Date(r.timestamp).toISOString(); if (!r.timeSegment) r.timeSegment = getTimeSegment(new Date(r.isoTimestamp)); if (!r.pattern) classifyPattern(r); return r; }); raceHistory.push(...newEntries); rebuildAggFromHistory(); analyzeTrends(); updateHistoryDisplay(); renderPatternTimeline(); saveToStorage(); const importMessage = document.getElementById('importMessage'); importMessage.className = 'mt-3 p-2 rounded text-sm bg-green-600/20 border border-green-500/30 text-green-300'; importMessage.textContent = `โœ… Successfully imported ${newEntries.length} new races! Total: ${raceHistory.length} races`; importMessage.classList.remove('hidden'); setTimeout(() => { importMessage.classList.add('hidden'); }, 5000); showNotification(`๐Ÿ“ฅ Imported ${newEntries.length} races. Oracle updated.`, 'success'); } catch (error) { const importMessage = document.getElementById('importMessage'); importMessage.className = 'mt-3 p-2 rounded text-sm bg-red-600/20 border border-red-500/30 text-red-300'; importMessage.textContent = `โŒ Import failed: ${error.message}`; importMessage.classList.remove('hidden'); setTimeout(() => { importMessage.classList.add('hidden'); }, 5000); showNotification('Import failed. Please check your file format.', 'error'); } }; reader.readAsText(file); event.target.value = ''; } function clearRaceHistory() { if (raceHistory.length === 0) { showNotification('No data to clear.', 'warning'); return; } const confirmed = confirm(`Are you sure you want to clear all ${raceHistory.length} race records? This cannot be undone.`); if (confirmed) { raceHistory = []; analyzeTrends(); updateHistoryDisplay(); renderPatternTimeline(); localStorage.removeItem(STORAGE_KEY); showNotification('๐Ÿ—‘๏ธ All race history cleared.', 'success'); } } // === Export only pattern data === function exportPatternData() { if (!raceHistory.length) { showNotification('No patterns to export yet.', 'warning'); return; } // Pick only pattern-related info from raceHistory const patternRecords = raceHistory.map(r => ({ timestamp: r.timestamp, ts: r.ts, winner: r.actual, pattern: r.pattern, streakCount: r.streakCount, timeSegment: r.timeSegment })); // Turn into JSON text const blob = new Blob([JSON.stringify(patternRecords, null, 2)], { type: 'application/json' }); // Create invisible download link const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `pattern-history-${new Date().toISOString().split('T')[0]}.json`; link.click(); // Show a little notification showNotification(`๐Ÿ“ค Exported ${patternRecords.length} pattern records.`, 'success'); } // ===== Event Listeners ===== document.getElementById('calculateBtn').onclick = () => { const predictions = calculateAdvancedPrediction(); displayResults(predictions); }; document.getElementById('showAdvanced').onclick = () => { const advancedDiv = document.getElementById('advancedResults'); if (advancedDiv.classList.contains('hidden')) { showAdvancedResults(); advancedDiv.classList.remove('hidden'); document.getElementById('showAdvanced').textContent = '๐Ÿ“Š Hide Advanced Analytics'; } else { advancedDiv.classList.add('hidden'); document.getElementById('showAdvanced').textContent = '๐Ÿ“Š Show Advanced Analytics'; } }; document.getElementById('exportBtn').onclick = exportRaceHistory; document.getElementById('importFile').onchange = importRaceHistory; document.getElementById('clearBtn').onclick = clearRaceHistory; ['leftRoad', 'middleRoad', 'rightRoad'].forEach(id => { document.getElementById(id).onchange = updateCalculateButton; }); // Filters document.getElementById('dateFilter').addEventListener('change', renderPatternTimeline); document.getElementById('timeSlotFilter').addEventListener('change', renderPatternTimeline); document.getElementById('clearFilters').addEventListener('click', () => { document.getElementById('dateFilter').value = ''; document.getElementById('timeSlotFilter').value = 'All'; renderPatternTimeline(); }); // ===== Kickoff ===== initializeUI(); // Pattern Oracle export button const exportPatternBtn = document.getElementById('exportPatternBtn'); if (exportPatternBtn) exportPatternBtn.onclick = exportPatternData; </script> <script> // ===== Strict Next Pattern (100% historical certainty) ===== (() => { const PANEL_ID = "strictNextPatternPanel"; const OUT_ID = "strictNextPatternOutput"; const CTX_ID = "strictNextPatternContext"; // Reuse DOTS mapping if present; otherwise define a minimal fallback. const DOTS = (typeof window.DOTS !== "undefined") ? window.DOTS : { HL: "๐ŸŸข", LL: "๐ŸŸก", NC: "๐Ÿ”ด", Random: "โšช" }; // Storage key fallback if the global constant exists. const STORAGE_KEY = (typeof window.STORAGE_KEY !== "undefined") ? window.STORAGE_KEY : "eliteRacePredictorData_v2"; function getStoredHistory() { try { // Prefer the in-memory history if available and non-empty. if (Array.isArray(window.raceHistory) && window.raceHistory.length > 0) { return window.raceHistory.slice(); } // Otherwise read from localStorage. const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const obj = JSON.parse(raw); if (obj && Array.isArray(obj.raceHistory)) return obj.raceHistory.slice(); // Some apps store directly as an array; handle that too. if (Array.isArray(obj)) return obj.slice(); } catch(e) { console.warn("Strict predictor: failed to read stored history", e); } return []; } function getPatternSequence() { const hist = getStoredHistory(); // use only HL, LL, NC (ignore 'Random' or missing) return hist.map(r => r && r.pattern).filter(p => p === "HL" || p === "LL" || p === "NC"); } // Build deterministic mapping for contexts of length k function computeDeterministicNext(seq, k) { const map = new Map(); // ctxKey -> Set(nextPatterns) for (let i = 0; i + k < seq.length; i++) { const ctx = seq.slice(i, i + k).join("|"); const nxt = seq[i + k]; if (!map.has(ctx)) map.set(ctx, new Set()); map.get(ctx).add(nxt); } const ctxKey = seq.slice(-k).join("|"); const s = map.get(ctxKey); if (s && s.size === 1) { return { next: [...s][0], ctxKey }; } return null; } // We try longer contexts first to be maximally strict, then fall back. function computeDeterministicNextWithSupport(seq, k, minSupport = 4) { const map = new Map(), freq = new Map(); for (let i = 0; i + k < seq.length; i++) { const ctx = seq.slice(i, i + k).join('|'); const nxt = seq[i + k]; if (!map.has(ctx)) map.set(ctx, new Set()); map.get(ctx).add(nxt); freq.set(ctx, (freq.get(ctx) || 0) + 1); } const ctxKey = seq.slice(-k).join('|'); if (freq.get(ctxKey) >= minSupport) { const s = map.get(ctxKey); if (s && s.size === 1) return { next: [...s][0], ctxKey }; } return null; } function strictNextPattern(seq, maxK = 6, minK = 3) { if (!seq || seq.length < minK) return null; for (let k = Math.min(maxK, seq.length - 1); k >= minK; k--) { const hit = computeDeterministicNextWithSupport(seq, k, 4); // minSupport = 4 if (hit) { return { pattern: hit.next, ctxLen: k }; } } return null; } function renderStrictPanel() { const panel = document.getElementById(PANEL_ID); const out = document.getElementById(OUT_ID); const ctx = document.getElementById(CTX_ID); if (!panel || !out || !ctx) return; const seq = getPatternSequence(); const res = strictNextPattern(seq, 6, 3); if (!res) { out.textContent = "I can't predict"; ctx.innerHTML = ""; return; } const k = res.ctxLen; const pred = res.pattern; const lastK = seq.slice(-k); const dotsCtx = lastK.map(p => `<span class="inline-block text-base leading-none">${DOTS[p] || "โ€ข"}</span>`).join("<span class='mx-1 text-cyan-300/70'>ยท</span>"); const nextDot = DOTS[pred] || "โ€ข"; out.innerHTML = `<span class="inline-flex items-center gap-1">${nextDot} <span>${pred}</span> <span class="ml-2 text-[11px] text-green-300/90 font-normal">(100% historical)</span></span>`; ctx.innerHTML = `<div class="flex items-center gap-2"> <span class="text-[11px] text-cyan-100/80">Context (last ${k}):</span> <div class="flex items-center gap-1">${dotsCtx}</div> <span class="mx-1 text-cyan-200/80">โ†’</span> <span class="inline-block text-base leading-none">${nextDot}</span> </div>`; } // Expose a hook so the rest of the app can trigger an update. window.renderStrictNextPattern = renderStrictPanel; // Update on initial load if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", renderStrictPanel); } else { renderStrictPanel(); } // Try to hook into the Calculate button if present const calcBtn = document.getElementById("calculateBtn"); if (calcBtn) { calcBtn.addEventListener("click", () => { // Allow the main predictor to finish in the same tick, then update. setTimeout(renderStrictPanel, 0); }); } // Also update after a race is recorded, if the app exposes a hook/event. // If there's a global function that runs after saving to storage, you can call window.renderStrictNextPattern() there as well. })(); </script> <script> // ===== HL Trend Oracle ===== (() => { const OUT = document.getElementById("hlTrendOutput"); if (!OUT) return; const STORAGE_KEY = (typeof window.STORAGE_KEY !== "undefined") ? window.STORAGE_KEY : "eliteRacePredictorData_v2"; function getPatternSeq() { try { if (Array.isArray(window.raceHistory) && window.raceHistory.length > 0) { return window.raceHistory.map(r => r && r.pattern); } const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const obj = JSON.parse(raw); if (obj && Array.isArray(obj.raceHistory)) return obj.raceHistory.map(r => r.pattern); if (Array.isArray(obj)) return obj.map(r => r.pattern); } catch(e) { console.warn("HL Trend Oracle: failed to read", e); } return []; } function detectHLTrend(seq) { if (seq.length < 3) return null; let streak = 0; for (let i = seq.length - 1; i >= 0; i--) { if (seq[i] === "HL") streak++; else break; } if (streak >= 3) { return { type: "streak", streak }; } return null; } function renderHLTrend() { const seq = getPatternSeq().filter(p => p === "HL" || p === "LL" || p === "NC"); const res = detectHLTrend(seq); if (!res) { OUT.textContent = "No HL trend signal"; } else { OUT.textContent = `HL trend likely: last ${res.streak} races HL`; } } // Hook into Calculate button const calcBtn = document.getElementById("calculateBtn"); if (calcBtn) { calcBtn.addEventListener("click", () => setTimeout(renderHLTrend, 0)); } // Initial render if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", renderHLTrend); } else { renderHLTrend(); } })(); </script> <script> // ===== Global Oracle (Weighted) ===== (() => { const OUT = document.getElementById("globalOracleOutput"); const CTX = document.getElementById("globalOracleContext"); if (!OUT || !CTX) return; const DOTS = (typeof window.DOTS !== "undefined") ? window.DOTS : { HL: "๐ŸŸข", LL: "๐ŸŸก", NC: "๐Ÿ”ด", Random: "โšช" }; const STORAGE_KEY = (typeof window.STORAGE_KEY !== "undefined") ? window.STORAGE_KEY : "eliteRacePredictorData_v2"; function getPatternSeq() { try { if (Array.isArray(window.raceHistory) && window.raceHistory.length > 0) { return window.raceHistory.map(r => r && r.pattern).filter(p => p === "HL" || p === "LL" || p === "NC"); } const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const obj = JSON.parse(raw); if (obj && Array.isArray(obj.raceHistory)) return obj.raceHistory.map(r => r.pattern).filter(p => p === "HL" || p === "LL" || p === "NC"); if (Array.isArray(obj)) return obj.map(r => r.pattern).filter(p => p === "HL" || p === "LL" || p === "NC"); } catch(e) { console.warn("Global Oracle: failed to read", e); } return []; } function buildTransitionMap(seq, k) { const map = {}; for (let i = 0; i + k < seq.length; i++) { const ctx = seq.slice(i, i + k).join("|"); const nxt = seq[i + k]; // Laplace smoothing - start with 1 each if (!map[ctx]) map[ctx] = { HL: 1, LL: 1, NC: 1 }; map[ctx][nxt]++; } return map; } function globalPredict(seq, maxK = 4) { if (!seq.length) return null; for (let k = Math.min(maxK, seq.length - 1); k >= 1; k--) { const map = buildTransitionMap(seq, k); const ctxKey = seq.slice(-k).join("|"); const row = map[ctxKey]; if (!row) continue; const total = row.HL + row.LL + row.NC; if (total === 0) continue; const best = Object.entries(row).sort((a,b)=>b[1]-a[1])[0]; const perc = (best[1]/total)*100; return {pattern: best[0], perc: perc.toFixed(1), k, ctxKey, dist: row}; } return null; } function renderGlobalOracle() { const seq = getPatternSeq(); const res = globalPredict(seq, 7); if (!res) { OUT.textContent = "No global signal"; CTX.innerHTML = ""; return; } const dot = DOTS[res.pattern] || "โ€ข"; OUT.innerHTML = `${dot} ${res.pattern} (${res.perc}% of ${res.k}-context cases)`; const ctxDots = res.ctxKey.split("|").map(p => `<span class="inline-block text-base leading-none">${DOTS[p]||"โ€ข"}</span>`).join("<span class='mx-1 text-purple-300/70'>ยท</span>"); CTX.innerHTML = `<div class="flex items-center gap-2"> <span class="text-[11px] text-purple-100/80">Context (last ${res.k}):</span> <div class="flex items-center gap-1">${ctxDots}</div> <span class="mx-1 text-purple-200/80">โ†’</span> <span class="inline-block text-base leading-none">${dot}</span> </div>`; } // Hook into Calculate button const calcBtn = document.getElementById("calculateBtn"); if (calcBtn) calcBtn.addEventListener("click", () => setTimeout(renderGlobalOracle, 0)); // Initial if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", renderGlobalOracle); } else { renderGlobalOracle(); } window.renderGlobalOracle = renderGlobalOracle; })(); </script> <!-- Replace the oracle accuracy panel with this version that includes a clear button --> <div id="oracleAccuracyPanel" class="fixed bottom-4 right-4 w-auto max-w-sm bg-gray-800 border border-gray-600 rounded-lg shadow-lg z-50"> <div id="oracleAccuracyHeader" class="flex justify-between items-center p-2 bg-gray-900 cursor-move"> <h3 class="font-bold text-purple-400">๐Ÿ”ฎ Pattern Oracle Accuracy</h3> <div id="oracleAccuracyContent" class="p-3 overflow-y-auto"> <div> <button id="refreshOraclePanel" class="text-blue-400 hover:text-blue-300 mr-2" title="Refresh data">โ†ป</button> <button id="clearOracleData" class="text-red-400 hover:text-red-300 mr-2" title="Clear all data">โŒซ</button> <button id="toggleOraclePanel" class="text-gray-400 hover:text-white">โˆ’</button> </div> </div> </div> <div id="oracleAccuracyContent" class="p-3 max-h-96 overflow-y-auto"> <div class="mb-3 flex space-x-1 border-b border-gray-700 pb-2"> <button class="time-slot-btn active" data-slot="all">All Time</button> <button class="time-slot-btn" data-slot="Morning">Morning</button> <button class="time-slot-btn" data-slot="Afternoon">Afternoon</button> <button class="time-slot-btn" data-slot="Evening">Evening</button> <button class="time-slot-btn" data-slot="Night">Night</button> </div> <div id="oracleAccuracyStats" class="text-sm mb-3"> <div class="text-gray-300">No prediction data yet.</div> </div> <div class="mt-3 pt-3 border-t border-gray-700"> <div class="text-xs text-gray-400 mb-1">Last Global Oracle prediction:</div> <div id="lastPredictionDisplay" class="text-sm font-mono bg-gray-900 p-2 rounded mb-2">No prediction recorded</div> <div class="text-xs text-gray-400 mb-1">Prediction result:</div> <div id="predictionResult" class="text-sm bg-gray-900 p-2 rounded">--</div> </div> </div> </div> <style> #oracleAccuracyPanel { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); } #oracleAccuracyHeader { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; user-select: none; } #oracleAccuracyContent.collapsed { display: none; } .accuracy-high { color: #34d399; /* green-400 */ } .accuracy-medium { color: #fbbf24; /* yellow-400 */ } .accuracy-low { color: #f87171; /* red-400 */ } .time-slot-btn { padding: 2px 6px; font-size: 11px; border-radius: 4px; background: #374151; color: #d1d5db; border: 1px solid #4b5563; cursor: pointer; } .time-slot-btn.active, .time-slot-btn:hover { background: #4f46e5; color: white; } .correct-prediction { background: rgba(34, 197, 94, 0.15); color: #34d399; border: 1px solid rgba(34, 197, 94, 0.3); padding: 4px 8px; border-radius: 4px; } .incorrect-prediction { background: rgba(239, 68, 68, 0.15); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.3); padding: 4px 8px; border-radius: 4px; } </style> <script> // Oracle Accuracy Panel functionality with Clear Data Button (function() { const STORAGE_KEY = 'oracleAccuracyData'; const panel = document.getElementById('oracleAccuracyPanel'); const header = document.getElementById('oracleAccuracyHeader'); const content = document.getElementById('oracleAccuracyContent'); const toggleBtn = document.getElementById('toggleOraclePanel'); const refreshBtn = document.getElementById('refreshOraclePanel'); const clearBtn = document.getElementById('clearOracleData'); const statsEl = document.getElementById('oracleAccuracyStats'); const lastPredictionEl = document.getElementById('lastPredictionDisplay'); const predictionResultEl = document.getElementById('predictionResult'); const timeSlotBtns = document.querySelectorAll('.time-slot-btn'); let isDragging = false; let dragOffset = { x: 0, y: 0 }; let currentTimeSlot = 'all'; let accuracyData = { all: { total: 0, correct: 0 }, Morning: { total: 0, correct: 0 }, Afternoon: { total: 0, correct: 0 }, Evening: { total: 0, correct: 0 }, Night: { total: 0, correct: 0 } }; // Track the last Global Oracle prediction let lastOraclePrediction = null; // Load accuracy data from storage function loadAccuracyData() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const parsed = JSON.parse(stored); // Merge with existing structure to ensure all time slots exist accuracyData = { ...accuracyData, ...parsed }; } } catch (e) { console.error('Failed to load oracle accuracy data', e); } } // Save accuracy data to storage function saveAccuracyData() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(accuracyData)); } catch (e) { console.error('Failed to save oracle accuracy data', e); } } // Clear all accuracy data function clearAccuracyData() { if (confirm("Are you sure you want to clear ALL oracle accuracy data? This cannot be undone.")) { accuracyData = { all: { total: 0, correct: 0 }, Morning: { total: 0, correct: 0 }, Afternoon: { total: 0, correct: 0 }, Evening: { total: 0, correct: 0 }, Night: { total: 0, correct: 0 } }; lastOraclePrediction = null; // Save cleared data saveAccuracyData(); // Update display updateOracleAccuracy(); // Show confirmation message showNotification("Oracle accuracy data cleared", "success"); } } // Show notification function showNotification(message, type) { // Create a simple notification (you can replace this with your existing notification system) const notification = document.createElement('div'); notification.textContent = message; notification.className = `fixed top-4 right-4 p-3 rounded-lg z-50 ${ type === 'success' ? 'bg-green-600' : 'bg-blue-600' } text-white`; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } // Make panel draggable header.addEventListener('mousedown', (e) => { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; panel.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; // Keep within viewport bounds const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; panel.style.left = `${Math.max(0, Math.min(x, maxX))}px`; panel.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.cursor = 'grab'; } }); // Toggle panel collapse toggleBtn.addEventListener('click', () => { content.classList.toggle('collapsed'); toggleBtn.textContent = content.classList.contains('collapsed') ? '+' : 'โˆ’'; }); // Refresh button refreshBtn.addEventListener('click', () => { updateOracleAccuracy(); }); // Clear data button clearBtn.addEventListener('click', () => { clearAccuracyData(); }); // Time slot filtering timeSlotBtns.forEach(btn => { btn.addEventListener('click', () => { timeSlotBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentTimeSlot = btn.dataset.slot; updateOracleAccuracy(); }); }); // Store the last Global Oracle prediction function captureOraclePrediction() { const globalOutput = document.getElementById('globalOracleOutput'); if (!globalOutput) return null; const predictionText = globalOutput.textContent || globalOutput.innerText; if (!predictionText || predictionText === 'โ€“' || predictionText.includes("can't predict")) { return null; } // Extract the pattern from the prediction text // Example: "๐ŸŸก LL (62.5% of 4-context cases)" const patternMatch = predictionText.match(/(๐ŸŸข|๐ŸŸก|๐Ÿ”ด)/); if (!patternMatch) return null; const patternEmoji = patternMatch[1]; let pattern = ''; if (patternEmoji === '๐ŸŸข') pattern = 'HL'; else if (patternEmoji === '๐ŸŸก') pattern = 'LL'; else if (patternEmoji === '๐Ÿ”ด') pattern = 'NC'; return pattern ? { pattern, timestamp: new Date().toISOString() } : null; } // Check if the Oracle's prediction was correct function checkPredictionAccuracy(prediction, actualRace) { if (!prediction || !actualRace || !actualRace.pattern) return false; // The prediction is correct if it matches the actual pattern of the race return prediction.pattern === actualRace.pattern; } // Calculate Oracle accuracy function calculateOracleAccuracy() { // Get the most recent race const latestRace = raceHistory.length > 0 ? raceHistory[raceHistory.length - 1] : null; if (!latestRace || !latestRace.pattern || latestRace.pattern === 'Random') { statsEl.innerHTML = `<div class="text-gray-300">No completed races with patterns yet.</div>`; lastPredictionEl.textContent = 'No prediction recorded'; predictionResultEl.innerHTML = '--'; return; } const timeSegment = latestRace.timeSegment || 'all'; // Calculate accuracy percentages const total = currentTimeSlot === 'all' ? accuracyData.all.total : accuracyData[currentTimeSlot].total; const correct = currentTimeSlot === 'all' ? accuracyData.all.correct : accuracyData[currentTimeSlot].correct; const accuracy = total > 0 ? (correct / total) * 100 : 0; // Determine accuracy class for coloring let accuracyClass = 'accuracy-low'; if (accuracy >= 70) accuracyClass = 'accuracy-high'; else if (accuracy >= 50) accuracyClass = 'accuracy-medium'; // Update the display statsEl.innerHTML = ` <div class="grid grid-cols-2 gap-2"> <div class="text-gray-300">Time slot:</div> <div class="font-medium">${currentTimeSlot}</div> <div class="text-gray-300">Total predictions:</div> <div class="font-medium">${total}</div> <div class="text-gray-300">Correct predictions:</div> <div class="font-medium">${correct}</div> <div class="text-gray-300">Accuracy:</div> <div class="font-bold ${accuracyClass}">${accuracy.toFixed(1)}%</div> </div> `; // Display the last prediction and its result if (lastOraclePrediction) { const predictionDot = DOTS[lastOraclePrediction.pattern] || 'โšช'; const actualDot = DOTS[latestRace.pattern] || 'โšช'; const wasCorrect = checkPredictionAccuracy(lastOraclePrediction, latestRace); lastPredictionEl.innerHTML = ` <div class="flex items-center justify-center"> <span class="text-lg mr-2">${predictionDot} ${lastOraclePrediction.pattern}</span> <span class="text-gray-400 mx-2">โ†’</span> <span class="text-lg">${actualDot} ${latestRace.pattern}</span> </div> `; predictionResultEl.innerHTML = wasCorrect ? `<span class="correct-prediction">โœ“ Correct prediction</span>` : `<span class="incorrect-prediction">โœ— Incorrect prediction</span>`; } else { lastPredictionEl.textContent = 'No prediction recorded for last race'; predictionResultEl.textContent = '--'; } } // Update the panel when relevant events occur function updateOracleAccuracy() { calculateOracleAccuracy(); } // Capture Oracle prediction when Calculate is clicked document.getElementById('calculateBtn').addEventListener('click', () => { // Capture the prediction after a short delay to allow the UI to update setTimeout(() => { lastOraclePrediction = captureOraclePrediction(); }, 300); }); // Override the recordCompleteRace function to check prediction accuracy const originalRecordCompleteRace = window.recordCompleteRace; if (originalRecordCompleteRace) { window.recordCompleteRace = function(winner, second, third) { const result = originalRecordCompleteRace.apply(this, arguments); // Check if the last prediction was correct if (lastOraclePrediction) { const latestRace = raceHistory[raceHistory.length - 1]; const wasCorrect = checkPredictionAccuracy(lastOraclePrediction, latestRace); const timeSegment = latestRace.timeSegment || 'all'; // Update accuracy counts accuracyData.all.total++; accuracyData[timeSegment].total++; if (wasCorrect) { accuracyData.all.correct++; accuracyData[timeSegment].correct++; } // Save updated data saveAccuracyData(); } setTimeout(updateOracleAccuracy, 100); return result; }; } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { loadAccuracyData(); setTimeout(updateOracleAccuracy, 500); }); // Expose update function globally window.updateOracleAccuracy = updateOracleAccuracy; })(); </script> <!-- Replace the pattern credibility panel with this corrected version --> <div id="patternCredibilityPanel" class="fixed bottom-4 left-4 w-auto max-w-sm bg-gray-800 border border-gray-600 rounded-lg shadow-lg z-50"> <div id="patternCredibilityHeader" class="flex justify-between items-center p-2 bg-gray-900 cursor-move"> <h3 class="font-bold text-green-400">๐Ÿ“Š Pattern Credibility</h3> <div id="patternCredibilityContent" class="p-3 overflow-y-auto"> <div> <button id="togglePatternPanel" class="text-gray-400 hover:text-white">โˆ’</button> </div> </div> </div> <div id="patternCredibilityContent" class="p-3 max-h-96 overflow-y-auto"> <div class="mb-3 flex space-x-1 border-b border-gray-700 pb-2"> <button class="cred-slot-btn active" data-slot="all">All Time</button> <button class="cred-slot-btn" data-slot="Morning">Morning</button> <button class="cred-slot-btn" data-slot="Afternoon">Afternoon</button> <button class="cred-slot-btn" data-slot="Evening">Evening</button> <button class="cred-slot-btn" data-slot="Night">Night</button> </div> <div id="currentPrediction" class="mb-4 p-3 bg-gray-900 rounded-lg"> <div class="text-xs text-gray-400 mb-1">Current Prediction:</div> <div id="currentPredictionDisplay" class="text-lg font-mono text-center">--</div> </div> <div id="patternCredibilityStats" class="text-sm mb-3"> <div class="text-gray-300 text-center">No historical data for this pattern yet.</div> </div> <div class="mt-3 pt-3 border-t border-gray-700"> <div class="text-xs text-gray-400 mb-1">Pattern History:</div> <div id="patternHistory" class="text-sm bg-gray-900 p-2 rounded max-h-32 overflow-y-auto"> <div class="text-gray-400 text-center py-4">No history recorded yet</div> </div> </div> </div> </div> <style> #patternCredibilityPanel { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); } #patternCredibilityHeader { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; user-select: none; } #patternCredibilityContent.collapsed { display: none; } .credibility-high { color: #34d399; } .credibility-medium { color: #fbbf24; } .credibility-low { color: #f87171; } .pattern-history-item { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid rgba(75, 85, 99, 0.3); } .pattern-history-item:last-child { border-bottom: none; } .history-correct { color: #34d399; } .history-incorrect { color: #f87171; } .cred-slot-btn { padding: 2px 6px; font-size: 11px; border-radius: 4px; background: #374151; color: #d1d5db; border: 1px solid #4b5563; cursor: pointer; } .cred-slot-btn.active, .cred-slot-btn:hover { background: #4f46e5; color: white; } .outcome-row { display: flex; justify-content: space-between; margin-bottom: 4px; padding: 2px 4px; border-radius: 3px; } .outcome-hl { background-color: rgba(34, 197, 94, 0.1); } .outcome-ll { background-color: rgba(250, 204, 21, 0.1); } .outcome-nc { background-color: rgba(239, 68, 68, 0.1); } </style> <script> // Pattern Credibility Panel functionality with Time Slots + Outcome Breakdown (function() { const STORAGE_KEY = 'patternCredibilityData_v3'; const panel = document.getElementById('patternCredibilityPanel'); const header = document.getElementById('patternCredibilityHeader'); const content = document.getElementById('patternCredibilityContent'); const toggleBtn = document.getElementById('togglePatternPanel'); const currentPredictionEl = document.getElementById('currentPredictionDisplay'); const statsEl = document.getElementById('patternCredibilityStats'); const historyEl = document.getElementById('patternHistory'); const slotBtns = document.querySelectorAll('.cred-slot-btn'); let isDragging = false; let dragOffset = { x: 0, y: 0 }; let patternHistoryData = {}; let lastPrediction = null; let currentSlot = 'all'; const DOTS = { HL:'๐ŸŸข', LL:'๐ŸŸก', NC:'๐Ÿ”ด', Random:'โšช' }; // Time segmentation function getTimeSegment(dateObj) { const h = dateObj.getHours(); if (h >= 0 && h < 6) return 'Night'; if (h >= 6 && h < 12) return 'Morning'; if (h >= 12 && h < 18) return 'Afternoon'; return 'Evening'; } // Load pattern history data function loadPatternHistory() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) patternHistoryData = JSON.parse(stored); } catch(e) { console.error('Failed to load pattern history', e); patternHistoryData = {}; } } function savePatternHistory() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(patternHistoryData)); } catch(e) { console.error('Failed to save pattern history', e); } } // Make panel draggable header.addEventListener('mousedown', (e) => { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; panel.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; panel.style.left = `${Math.max(0, Math.min(x, maxX))}px`; panel.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.cursor = 'grab'; } }); // Toggle panel collapse toggleBtn.addEventListener('click', () => { content.classList.toggle('collapsed'); toggleBtn.textContent = content.classList.contains('collapsed') ? '+' : 'โˆ’'; }); // Handle time slot switching slotBtns.forEach(btn => { btn.addEventListener('click', () => { slotBtns.forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentSlot = btn.dataset.slot; displayPatternCredibility(); }); }); // Record outcomes - FIXED VERSION function updatePatternHistory(raceRecord) { if (!lastPrediction || !raceRecord.pattern || raceRecord.pattern === 'Random') return; const slot = raceRecord.timeSegment || getTimeSegment(new Date(raceRecord.isoTimestamp || Date.now())); const actualPattern = raceRecord.pattern; // Ensure structure exists if (!patternHistoryData[lastPrediction]) { patternHistoryData[lastPrediction] = {}; } // Initialize for all time slots ['all','Morning','Afternoon','Evening','Night'].forEach(s => { if (!patternHistoryData[lastPrediction][s]) { patternHistoryData[lastPrediction][s] = { total: 0, outcomes: {HL: 0, LL: 0, NC: 0, Random: 0}, history: [] }; } }); // Update both "all" and specific slot ['all', slot].forEach(seg => { const data = patternHistoryData[lastPrediction][seg]; data.total++; // Count what actually happened when this pattern was predicted if (actualPattern in data.outcomes) { data.outcomes[actualPattern]++; } else { data.outcomes[actualPattern] = 1; } // Add to history (limit to 20 entries) data.history.unshift({ timestamp: new Date().toISOString(), predicted: lastPrediction, actual: actualPattern, correct: lastPrediction === actualPattern }); if (data.history.length > 20) { data.history.pop(); } }); savePatternHistory(); lastPrediction = null; } // Get current prediction from Global Oracle function getCurrentPrediction() { const globalOutput = document.getElementById('globalOracleOutput'); if (!globalOutput) return null; const txt = globalOutput.textContent || globalOutput.innerText; if (!txt || txt === 'โ€“' || txt.includes("can't predict") || txt.includes("No global signal")) return null; const match = txt.match(/(๐ŸŸข|๐ŸŸก|๐Ÿ”ด)/); if (!match) return null; return match[1] === '๐ŸŸข' ? 'HL' : match[1] === '๐ŸŸก' ? 'LL' : 'NC'; } // Display pattern credibility with outcome breakdown function displayPatternCredibility() { const currentPattern = getCurrentPrediction(); if (!currentPattern) { currentPredictionEl.textContent = 'No active prediction'; statsEl.innerHTML = '<div class="text-gray-300 text-center">Calculate winner probability first</div>'; historyEl.innerHTML = '<div class="text-gray-400 text-center py-4">No prediction to analyze</div>'; return; } lastPrediction = currentPattern; const patternDot = DOTS[currentPattern] || 'โšช'; currentPredictionEl.innerHTML = `<span class="text-xl">${patternDot} ${currentPattern}</span>`; const data = patternHistoryData[currentPattern]?.[currentSlot] || { total: 0, outcomes: {HL: 0, LL: 0, NC: 0, Random: 0}, history: [] }; if (data.total === 0) { statsEl.innerHTML = ` <div class="text-center"> <div class="text-gray-300 mb-2">No historical data (${currentSlot})</div> <div class="text-xs text-gray-400">This will be recorded after the race</div> </div> `; historyEl.innerHTML = '<div class="text-gray-400 text-center py-4">No history recorded yet</div>'; return; } // Calculate accuracy const correctPredictions = data.outcomes[currentPattern] || 0; const accuracy = (correctPredictions / data.total) * 100; let credClass = accuracy >= 70 ? 'credibility-high' : accuracy >= 50 ? 'credibility-medium' : 'credibility-low'; // Create outcome breakdown let outcomeHTML = ''; const patterns = ['HL', 'LL', 'NC', 'Random']; patterns.forEach(pattern => { const count = data.outcomes[pattern] || 0; if (count > 0) { const percentage = ((count / data.total) * 100).toFixed(1); const dot = DOTS[pattern] || 'โšช'; const isCurrent = pattern === currentPattern; outcomeHTML += ` <div class="outcome-row outcome-${pattern.toLowerCase()}"> <span>${dot} ${pattern}:</span> <span>${percentage}% (${count}/${data.total})</span> </div> `; } }); statsEl.innerHTML = ` <div class="grid grid-cols-2 gap-2 mb-2"> <div class="text-gray-300">Times predicted:</div> <div class="font-medium">${data.total}</div> <div class="text-gray-300">Correct predictions:</div> <div class="font-medium">${correctPredictions}</div> <div class="text-gray-300">Accuracy:</div> <div class="font-bold ${credClass}">${accuracy.toFixed(1)}%</div> </div> <div class="mt-3 text-xs text-gray-400">When ${patternDot} ${currentPattern} was predicted:</div> <div class="mt-1 text-sm"> ${outcomeHTML} </div> <div class="text-xs text-gray-400 text-center mt-2">(${currentSlot})</div> `; // Display pattern history if (!data.history.length) { historyEl.innerHTML = '<div class="text-gray-400 text-center py-4">No history recorded yet</div>'; } else { historyEl.innerHTML = data.history.map(entry => { const d = new Date(entry.timestamp); const t = d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'}); const actualDot = DOTS[entry.actual] || 'โšช'; const predictedDot = DOTS[entry.predicted] || 'โšช'; return ` <div class="pattern-history-item"> <div>${t}</div> <div class="${entry.correct ? 'history-correct' : 'history-incorrect'}"> ${predictedDot}โ†’${actualDot} ${entry.correct ? 'โœ“' : 'โœ—'} </div> </div> `; }).join(''); } } // Hook into the race recording process const originalRecordCompleteRace = window.recordCompleteRace; if (originalRecordCompleteRace) { window.recordCompleteRace = function(winner, second, third) { const result = originalRecordCompleteRace.apply(this, arguments); // Update pattern history with the latest race if (raceHistory && raceHistory.length > 0) { const latestRace = raceHistory[raceHistory.length - 1]; updatePatternHistory(latestRace); } // Update display setTimeout(displayPatternCredibility, 100); return result; }; } // Update when Calculate is clicked document.getElementById('calculateBtn').addEventListener('click', () => { // Update after a short delay to allow the UI to update setTimeout(() => { const prediction = getCurrentPrediction(); if (prediction) { lastPrediction = prediction; displayPatternCredibility(); } }, 300); }); // Initialize on page load document.addEventListener('DOMContentLoaded', () => { loadPatternHistory(); setTimeout(displayPatternCredibility, 500); }); // Expose function globally window.displayPatternCredibility = displayPatternCredibility; })(); </script> <script> function makePanelDraggable(panelId, headerId) { const panel = document.getElementById(panelId); const header = document.getElementById(headerId); let isDragging = false, offsetX = 0, offsetY = 0; header.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; document.body.style.userSelect = "none"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; panel.style.left = (e.clientX - offsetX) + "px"; panel.style.top = (e.clientY - offsetY) + "px"; panel.style.right = "auto"; panel.style.bottom = "auto"; panel.style.position = "absolute"; }); document.addEventListener("mouseup", () => { isDragging = false; document.body.style.userSelect = "auto"; }); } makePanelDraggable("patternCredibilityPanel", "patternCredibilityHeader"); makePanelDraggable("oracleAccuracyPanel", "oracleAccuracyHeader"); </script> <!-- Replace your existing strategy panel code with this fixed version --> <div id="strategyPanel" class="fixed bottom-4 right-4 w-auto max-w-sm bg-gray-800 border border-blue-500 rounded-lg shadow-xl z-50"> <div id="strategyHeader" class="flex justify-between items-center p-3 bg-gradient-to-r from-blue-800 to-purple-800 cursor-move rounded-t-lg"> <h3 class="font-bold text-blue-300 flex items-center"> <span class="text-xl mr-2">๐ŸŽฏ</span> <span>Elite Betting Strategy</span> </h3> <div> <button id="refreshStrategy" class="text-blue-300 hover:text-blue-100 mr-2" title="Refresh">โ†ป</button> <button id="minimizeStrategy" class="text-gray-300 hover:text-white">โˆ’</button> </div> </div> <div id="strategyContent" class="p-4 overflow-y-auto max-h-72"> <div class="text-center text-gray-400 py-4"> <p>Configure the track and calculate probabilities to see strategy</p> </div> </div> </div> <script> // Fixed Strategy Panel with Working Historical Pattern Analysis (function() { // DOM elements const panel = document.getElementById('strategyPanel'); const header = document.getElementById('strategyHeader'); const content = document.getElementById('strategyContent'); const refreshBtn = document.getElementById('refreshStrategy'); const minimizeBtn = document.getElementById('minimizeStrategy'); let isDragging = false; let dragOffset = { x: 0, y: 0 }; // Make panel draggable header.addEventListener('mousedown', (e) => { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; // IMPORTANT: lock current position using left/top and clear right/bottom // so CSS `right/bottom` do not conflict with left/top while dragging. panel.style.left = `${rect.left}px`; panel.style.top = `${rect.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.style.cursor = 'grabbing'; header.classList.add('bg-gradient-to-r', 'from-blue-900', 'to-purple-900'); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; // Keep within viewport bounds const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; panel.style.left = `${Math.max(0, Math.min(x, maxX))}px`; panel.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.cursor = 'grab'; header.classList.remove('bg-gradient-to-r', 'from-blue-900', 'to-purple-900'); } }); // Minimize button minimizeBtn.addEventListener('click', () => { content.classList.toggle('hidden'); minimizeBtn.textContent = content.classList.contains('hidden') ? '+' : 'โˆ’'; }); // Refresh button refreshBtn.addEventListener('click', () => { generateStrategy(); }); // Function to get race history from localStorage function getRaceHistory() { try { // Try to access the global raceHistory variable first if (typeof window.raceHistory !== 'undefined' && Array.isArray(window.raceHistory)) { return window.raceHistory; } // If not available, try to get from localStorage const storedData = localStorage.getItem('eliteRacePredictorData_v2'); if (storedData) { const parsedData = JSON.parse(storedData); if (parsedData && Array.isArray(parsedData.raceHistory)) { return parsedData.raceHistory; } } // If all else fails, return empty array return []; } catch (e) { console.error('Error accessing race history:', e); return []; } } // Function to analyze historical patterns function analyzeHistoricalPatterns(trackPattern, timeSegment) { const raceHistory = getRaceHistory(); if (!raceHistory || raceHistory.length === 0) { return null; } // Filter races with similar track pattern and time segment const similarRaces = raceHistory.filter(race => { if (!race.track || !Array.isArray(race.track)) return false; const racePattern = race.track.join(' | '); let raceTimeSegment; // Get time segment from the race record if (race.timeSegment) { raceTimeSegment = race.timeSegment; } else if (race.isoTimestamp) { raceTimeSegment = getTimeSegmentFromTimestamp(race.isoTimestamp); } else if (race.timestamp) { raceTimeSegment = getTimeSegmentFromTimestamp(race.timestamp); } else { return false; } return racePattern === trackPattern && raceTimeSegment === timeSegment; }); if (similarRaces.length === 0) { return null; } // Count pattern occurrences const patternCounts = { HL: 0, LL: 0, NC: 0, Random: 0 }; similarRaces.forEach(race => { // Get pattern from race record let pattern; if (race.pattern) { pattern = race.pattern; } else { // If pattern doesn't exist, try to classify it pattern = classifyPatternFromRace(race); } if (pattern && patternCounts.hasOwnProperty(pattern)) { patternCounts[pattern]++; } }); // Calculate percentages const total = similarRaces.length; const patternPercentages = { HL: total > 0 ? (patternCounts.HL / total) * 100 : 0, LL: total > 0 ? (patternCounts.LL / total) * 100 : 0, NC: total > 0 ? (patternCounts.NC / total) * 100 : 0, Random: total > 0 ? (patternCounts.Random / total) * 100 : 0 }; return { count: total, percentages: patternPercentages, patternCounts: patternCounts }; } // Helper function to classify pattern from race data (if not already classified) function classifyPatternFromRace(race) { if (!race.track || !race.actual || !race.vehicles) return 'Random'; // Find the first non-unknown segment const knownSegmentIndex = race.track.findIndex(seg => seg && seg !== 'Unknown'); if (knownSegmentIndex === -1) return 'Random'; const segmentType = race.track[knownSegmentIndex]; // Get stats for all vehicles in this race on this segment const vehicleStats = race.vehicles.map(vehicle => { return { name: vehicle, stat: window.vehicles[vehicle]?.[segmentType] || 0 }; }); // Sort by performance on this segment (highest first) vehicleStats.sort((a, b) => b.stat - a.stat); // Determine pattern based on which vehicle won if (race.actual === vehicleStats[0].name) return 'HL'; if (race.actual === vehicleStats[1].name) return 'LL'; if (race.actual === vehicleStats[2].name) return 'NC'; return 'Random'; } // Helper function to extract time segment from timestamp function getTimeSegmentFromTimestamp(timestamp) { try { const date = new Date(timestamp); const h = date.getHours(); if (h >= 0 && h < 6) return 'Night'; if (h >= 6 && h < 12) return 'Morning'; if (h >= 12 && h < 18) return 'Afternoon'; return 'Evening'; } catch (e) { return 'Unknown'; } } // Function to generate strategy based on current state function generateStrategy() { const leftRoad = document.getElementById('leftRoad').value || 'Unknown'; const middleRoad = document.getElementById('middleRoad').value || 'Unknown'; const rightRoad = document.getElementById('rightRoad').value || 'Unknown'; const timeSlot = document.getElementById('timeSlotFilter').value; const trackPattern = `${leftRoad} | ${middleRoad} | ${rightRoad}`; const timeSegment = timeSlot === 'All' ? getCurrentTimeSegment() : timeSlot; // Get current time if not filtered function getCurrentTimeSegment() { const now = new Date(); const h = now.getHours(); if (h >= 0 && h < 6) return 'Night'; if (h >= 6 && h < 12) return 'Morning'; if (h >= 12 && h < 18) return 'Afternoon'; return 'Evening'; } // Get historical pattern analysis const historicalData = analyzeHistoricalPatterns(trackPattern, timeSegment); // Calculate pattern probabilities from CSV data (existing functionality) const hlPatternProb = window.patternData?.hlTrackPatterns[trackPattern] || 0; const llPatternProb = window.patternData?.llTrackPatterns[trackPattern] || 0; const ncPatternProb = window.patternData?.ncTrackPatterns[trackPattern] || 0; const hlTimeProb = window.patternData?.hlTimeSegments[timeSegment] || 0; const llTimeProb = window.patternData?.llTimeSegments[timeSegment] || 0; const ncTimeProb = window.patternData?.ncTimeSegments[timeSegment] || 0; // Combined probability (weighted average: 70% pattern, 30% time) const hlCombined = (hlPatternProb * 0.7) + (hlTimeProb * 0.3); const llCombined = (llPatternProb * 0.7) + (llTimeProb * 0.3); const ncCombined = (ncPatternProb * 0.7) + (ncTimeProb * 0.3); // Determine recommended pattern let recommendedPattern = 'HL'; if (llCombined > hlCombined && llCombined > ncCombined) recommendedPattern = 'LL'; if (ncCombined > hlCombined && ncCombined > llCombined) recommendedPattern = 'NC'; // Generate strategy HTML let html = ` <div class="mb-4 p-3 bg-gray-700 rounded-lg"> <div class="flex justify-between items-center mb-2"> <span class="font-semibold">Track Pattern:</span> <span class="text-blue-300">${trackPattern}</span> </div> <div class="flex justify-between items-center"> <span class="font-semibold">Time Segment:</span> <span class="text-blue-300">${timeSegment}</span> </div> </div> <div class="grid grid-cols-3 gap-2 mb-4"> <div class="text-center p-2 rounded-lg ${recommendedPattern === 'HL' ? 'bg-green-900/30 border border-green-500' : 'bg-gray-700'}"> <div class="text-lg">๐ŸŸข HL</div> <div class="text-sm ${hlCombined > 60 ? 'text-green-400' : hlCombined > 40 ? 'text-yellow-400' : 'text-red-400'}">${hlCombined.toFixed(1)}%</div> </div> <div class="text-center p-2 rounded-lg ${recommendedPattern === 'LL' ? 'bg-yellow-900/30 border border-yellow-500' : 'bg-gray-700'}"> <div class="text-lg">๐ŸŸก LL</div> <div class="text-sm ${llCombined > 60 ? 'text-green-400' : llCombined > 40 ? 'text-yellow-400' : 'text-red-400'}">${llCombined.toFixed(1)}%</div> </div> <div class="text-center p-2 rounded-lg ${recommendedPattern === 'NC' ? 'bg-red-900/30 border border-red-500' : 'bg-gray-700'}"> <div class="text-lg">๐Ÿ”ด NC</div> <div class="text-sm ${ncCombined > 60 ? 'text-green-400' : ncCombined > 40 ? 'text-yellow-400' : 'text-red-400'}">${ncCombined.toFixed(1)}%</div> </div> </div> `; // Add historical analysis if available if (historicalData) { html += ` <div class="mb-4 p-3 bg-gray-900 rounded-lg"> <h4 class="font-bold text-blue-300 mb-2 flex items-center"> <span class="mr-2">๐Ÿ“Š</span> Historical Analysis </h4> <p class="text-sm text-gray-300 mb-2">Found ${historicalData.count} similar races in ${timeSegment}</p> <div class="space-y-2"> <div class="flex justify-between items-center"> <span class="text-green-400">๐ŸŸข HL:</span> <span>${historicalData.patternCounts.HL} races (${historicalData.percentages.HL.toFixed(1)}%)</span> </div> <div class="flex justify-between items-center"> <span class="text-yellow-400">๐ŸŸก LL:</span> <span>${historicalData.patternCounts.LL} races (${historicalData.percentages.LL.toFixed(1)}%)</span> </div> <div class="flex justify-between items-center"> <span class="text-red-400">๐Ÿ”ด NC:</span> <span>${historicalData.patternCounts.NC} races (${historicalData.percentages.NC.toFixed(1)}%)</span> </div> ${historicalData.patternCounts.Random > 0 ? ` <div class="flex justify-between items-center"> <span class="text-gray-400">โšช Random:</span> <span>${historicalData.patternCounts.Random} races (${historicalData.percentages.Random.toFixed(1)}%)</span> </div> ` : ''} </div> </div> `; } else { html += ` <div class="mb-4 p-3 bg-gray-900 rounded-lg"> <h4 class="font-bold text-blue-300 mb-2 flex items-center"> <span class="mr-2">๐Ÿ“Š</span> Historical Analysis </h4> <p class="text-sm text-gray-300">No similar races found in history for "${trackPattern}" during ${timeSegment}</p> <p class="text-xs text-gray-400 mt-1">Complete more races with this pattern to build historical data</p> </div> `; } // Add recommendation html += ` <div class="p-3 rounded-lg ${recommendedPattern === 'HL' ? 'bg-green-900/20 border border-green-700' : recommendedPattern === 'LL' ? 'bg-yellow-900/20 border border-yellow-700' : 'bg-red-900/20 border border-red-700'}"> <h4 class="font-bold mb-2 ${recommendedPattern === 'HL' ? 'text-green-400' : recommendedPattern === 'LL' ? 'text-yellow-400' : 'text-red-400'}">๐ŸŽฏ Recommended Strategy</h4> <p class="text-sm mb-2">Bet on <strong>${recommendedPattern}</strong> pattern vehicle</p> <p class="text-xs text-gray-400">Based on pattern strength + time boost algorithm</p> </div> <div class="mt-4 text-xs text-gray-400"> <p>Pattern strength: 70% weight | Time segment: 30% weight</p> </div> `; content.innerHTML = html; } // Make sure patternData is available globally if (typeof window.patternData === 'undefined') { window.patternData = { hlTrackPatterns: { "Unknown | Dirt | Unknown": 92.16, "Highway | Unknown | Unknown": 88.64, "Unknown | Unknown | Dirt": 87.5, "Unknown | Unknown | Highway": 87.5, "Dirt | Unknown | Unknown": 86.79, "Expressway | Unknown | Unknown": 86.67, "Unknown | Highway | Unknown": 82.35, "Unknown | Expressway | Unknown": 76.09, "Unknown | Unknown | Bumpy": 73.91, "Unknown | Unknown | Expressway": 72.5, "Bumpy | Unknown | Unknown": 67.21, "Unknown | Unknown | Desert": 66.0, "Unknown | Bumpy | Unknown": 64.71, "Unknown | Desert | Unknown": 53.23, "Potholes | Unknown | Unknown": 43.18, "Desert | Unknown | Unknown": 41.03, "Unknown | Potholes | Unknown": 36.54, "Unknown | Unknown | Potholes": 30.23 }, llTrackPatterns: { "Unknown | Potholes | Unknown": 64.52, "Unknown | Bumpy | Unknown": 55.17, "Potholes | Unknown | Unknown": 51.85, "Unknown | Unknown | Potholes": 51.35, "Desert | Unknown | Unknown": 42.86, "Unknown | Desert | Unknown": 39.02, "Unknown | Unknown | Bumpy": 36.36, "Bumpy | Unknown | Unknown": 36.36, "Unknown | Unknown | Desert": 30.77, "Unknown | Expressway | Unknown": 30.77, "Unknown | Highway | Unknown": 28.57, "Highway | Unknown | Unknown": 27.27, "Unknown | Unknown | Dirt": 23.81, "Unknown | Unknown | Expressway": 18.75, "Unknown | Dirt | Unknown": 17.39, "Dirt | Unknown | Unknown": 16.67, "Unknown | Unknown | Highway": 16.13, "Expressway | Unknown | Unknown": 3.70 }, ncTrackPatterns: { "Potholes | Unknown | Unknown": 52.94, "Unknown | Potholes | Unknown": 50.0, "Unknown | Unknown | Potholes": 28.57, "Desert | Unknown | Unknown": 26.09, "Dirt | Unknown | Unknown": 20.0, "Unknown | Unknown | Desert": 18.18, "Unknown | Expressway | Unknown": 17.14, "Unknown | Desert | Unknown": 16.13, "Unknown | Highway | Unknown": 13.33, "Unknown | Dirt | Unknown": 9.52, "Unknown | Unknown | Dirt": 8.0, "Bumpy | Unknown | Unknown": 7.41, "Unknown | Unknown | Expressway": 5.88, "Unknown | Unknown | Bumpy": 4.17, "Expressway | Unknown | Unknown": 3.13, "Expressway | Dirt | Unknown": 0.0, "Highway | Unknown | Unknown": 0.0, "Unknown | Bumpy | Unknown": 0.0, "Unknown | Unknown | Highway": 0.0 }, hlTimeSegments: { "Morning": 71.70, "Night": 69.90, "Afternoon": 66.95, "Evening": 66.44 }, llTimeSegments: { "Evening": 37.5, "Afternoon": 34.71, "Morning": 32.47, "Night": 31.50 }, ncTimeSegments: { "Evening": 16.88, "Morning": 15.30, "Afternoon": 14.29, "Night": 13.13 } }; } // Hook into the Calculate button const calculateBtn = document.getElementById('calculateBtn'); if (calculateBtn) { calculateBtn.addEventListener('click', () => { // Wait a bit for the prediction to complete setTimeout(generateStrategy, 300); }); } // Initialize - keep fixed so it floats like the other panels panel.style.position = 'fixed'; panel.style.right = '1rem'; panel.style.bottom = '1rem'; panel.style.left = 'auto'; panel.style.top = 'auto'; panel.style.cursor = 'grab'; })(); </script> <style> #strategyPanel { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); transition: all 0.3s ease; } #strategyHeader { border-top-left-radius: 0.5rem; border-top-right-radius: 0.5rem; user-select: none; transition: background 0.3s ease; } #strategyContent { scrollbar-width: thin; scrollbar-color: rgba(59, 130, 246, 0.5) rgba(31, 41, 55, 0.5); } #strategyContent::-webkit-scrollbar { width: 6px; } #strategyContent::-webkit-scrollbar-track { background: rgba(31, 41, 55, 0.5); border-radius: 3px; } #strategyContent::-webkit-scrollbar-thumb { background-color: rgba(59, 130, 246, 0.5); border-radius: 3px; } </style> <!-- Daily Accuracy Panel --> <div id="dailyAccuracyPanel" class="fixed top-4 left-4 w-auto bg-gray-800 border border-purple-500 rounded-lg shadow-lg z-50"> <div id="dailyAccuracyHeader" class="flex justify-between items-center p-2 bg-gray-900 cursor-move rounded-t-lg"> <h3 class="font-bold text-purple-400 text-sm">๐ŸŽฏ Daily AI Accuracy</h3> <button id="toggleDailyPanel" class="text-gray-400 hover:text-white text-xs">โˆ’</button> </div> <div id="dailyAccuracyContent" class="p-2"> <div class="text-center"> <div class="text-xs text-gray-400 mb-1">Today's Performance</div> <div id="dailyAccuracyValue" class="text-lg font-bold text-green-400">0%</div> <div class="text-xs text-gray-400 mt-1"><span id="dailyRacesCount">0</span> races</div> </div> </div> </div> <style> #dailyAccuracyPanel { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); min-width: 120px; } #dailyAccuracyHeader { user-select: none; } #dailyAccuracyContent.collapsed { display: none; } </style> <script> // Daily Accuracy Panel (function() { const STORAGE_KEY = 'dailyAccuracyData'; const panel = document.getElementById('dailyAccuracyPanel'); const header = document.getElementById('dailyAccuracyHeader'); const content = document.getElementById('dailyAccuracyContent'); const toggleBtn = document.getElementById('toggleDailyPanel'); const accuracyEl = document.getElementById('dailyAccuracyValue'); const racesCountEl = document.getElementById('dailyRacesCount'); let isDragging = false; let dragOffset = { x: 0, y: 0 }; let dailyData = { date: new Date().toDateString(), total: 0, correct: 0 }; // Load daily data from storage function loadDailyData() { try { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { const parsed = JSON.parse(stored); // Only use if it's from today if (parsed.date === new Date().toDateString()) { dailyData = parsed; } } } catch (e) { console.error('Failed to load daily accuracy data', e); } updateDailyDisplay(); } // Save daily data to storage function saveDailyData() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(dailyData)); } catch (e) { console.error('Failed to save daily accuracy data', e); } } // Update the display function updateDailyDisplay() { const accuracy = dailyData.total > 0 ? Math.round((dailyData.correct / dailyData.total) * 100) : 0; accuracyEl.textContent = `${accuracy}%`; racesCountEl.textContent = dailyData.total; // Color code based on accuracy if (accuracy >= 70) accuracyEl.className = 'text-lg font-bold text-green-400'; else if (accuracy >= 50) accuracyEl.className = 'text-lg font-bold text-yellow-400'; else accuracyEl.className = 'text-lg font-bold text-red-400'; } // Check if we need to reset for a new day function checkDateReset() { const today = new Date().toDateString(); if (dailyData.date !== today) { dailyData = { date: today, total: 0, correct: 0 }; saveDailyData(); updateDailyDisplay(); } } // Make panel draggable header.addEventListener('mousedown', (e) => { isDragging = true; const rect = panel.getBoundingClientRect(); dragOffset = { x: e.clientX - rect.left, y: e.clientY - rect.top }; panel.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const x = e.clientX - dragOffset.x; const y = e.clientY - dragOffset.y; // Keep within viewport bounds const maxX = window.innerWidth - panel.offsetWidth; const maxY = window.innerHeight - panel.offsetHeight; panel.style.left = `${Math.max(0, Math.min(x, maxX))}px`; panel.style.top = `${Math.max(0, Math.min(y, maxY))}px`; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; panel.style.cursor = 'grab'; } }); // Toggle panel collapse toggleBtn.addEventListener('click', () => { content.classList.toggle('collapsed'); toggleBtn.textContent = content.classList.contains('collapsed') ? '+' : 'โˆ’'; }); // Record a race result function recordDailyRace(correct) { checkDateReset(); // Ensure we're tracking today's data dailyData.total++; if (correct) dailyData.correct++; saveDailyData(); updateDailyDisplay(); } // Hook into the race recording process const originalRecordCompleteRace = window.recordCompleteRace; if (originalRecordCompleteRace) { window.recordCompleteRace = function(winner, second, third) { const result = originalRecordCompleteRace.apply(this, arguments); // Update daily accuracy with the latest race if (raceHistory && raceHistory.length > 0) { const latestRace = raceHistory[raceHistory.length - 1]; recordDailyRace(latestRace.correct); } return result; }; } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { // Set initial position panel.style.position = 'fixed'; panel.style.top = '1rem'; panel.style.left = '1rem'; panel.style.cursor = 'grab'; loadDailyData(); // Check every minute if we need to reset for a new day setInterval(checkDateReset, 60000); }); })(); // ===== Initialize Pattern Data ===== (function initializePatternData() { if (typeof window.patternData === 'undefined') { window.patternData = { hlTrackPatterns: { "Unknown | Dirt | Unknown": 92.16, "Highway | Unknown | Unknown": 88.64, "Unknown | Unknown | Dirt": 87.5, "Unknown | Unknown | Highway": 87.5, "Dirt | Unknown | Unknown": 86.79, "Expressway | Unknown | Unknown": 86.67, "Unknown | Highway | Unknown": 82.35, "Unknown | Expressway | Unknown": 76.09, "Unknown | Unknown | Bumpy": 73.91, "Unknown | Unknown | Expressway": 72.5, "Bumpy | Unknown | Unknown": 67.21, "Unknown | Unknown | Desert": 66.0, "Unknown | Bumpy | Unknown": 64.71, "Unknown | Desert | Unknown": 53.23, "Potholes | Unknown | Unknown": 43.18, "Desert | Unknown | Unknown": 41.03, "Unknown | Potholes | Unknown": 36.54, "Unknown | Unknown | Potholes": 30.23 }, llTrackPatterns: { "Unknown | Potholes | Unknown": 64.52, "Unknown | Bumpy | Unknown": 55.17, "Potholes | Unknown | Unknown": 51.85, "Unknown | Unknown | Potholes": 51.35, "Desert | Unknown | Unknown": 42.86, "Unknown | Desert | Unknown": 39.02, "Unknown | Unknown | Bumpy": 36.36, "Bumpy | Unknown | Unknown": 36.36, "Unknown | Unknown | Desert": 30.77, "Unknown | Expressway | Unknown": 30.77, "Unknown | Highway | Unknown": 28.57, "Highway | Unknown | Unknown": 27.27, "Unknown | Unknown | Dirt": 23.81, "Unknown | Unknown | Expressway": 18.75, "Unknown | Dirt | Unknown": 17.39, "Dirt | Unknown | Unknown": 16.67, "Unknown | Unknown | Highway": 16.13, "Expressway | Unknown | Unknown": 3.70 }, ncTrackPatterns: { "Potholes | Unknown | Unknown": 52.94, "Unknown | Potholes | Unknown": 50.0, "Unknown | Unknown | Potholes": 28.57, "Desert | Unknown | Unknown": 26.09, "Dirt | Unknown | Unknown": 20.0, "Unknown | Unknown | Desert": 18.18, "Unknown | Expressway | Unknown": 17.14, "Unknown | Desert | Unknown": 16.13, "Unknown | Highway | Unknown": 13.33, "Unknown | Dirt | Unknown": 9.52, "Unknown | Unknown | Dirt": 8.0, "Bumpy | Unknown | Unknown": 7.41, "Unknown | Unknown | Expressway": 5.88, "Unknown | Unknown | Bumpy": 4.17, "Expressway | Unknown | Unknown": 3.13, "Highway | Unknown | Unknown": 0.0, "Unknown | Bumpy | Unknown": 0.0, "Unknown | Unknown | Highway": 0.0 }, hlTimeSegments: { "Morning": 71.70, "Night": 69.90, "Afternoon": 66.95, "Evening": 66.44 }, llTimeSegments: { "Evening": 37.5, "Afternoon": 34.71, "Morning": 32.47, "Night": 31.50 }, ncTimeSegments: { "Evening": 16.88, "Morning": 15.30, "Afternoon": 14.29, "Night": 13.13 } }; } })(); // ===== Ensure Proper Initialization ===== // This makes sure all panels wait until everything is loaded document.addEventListener('DOMContentLoaded', function() { console.log("Page loaded, waiting for panels to initialize..."); // Wait 1 second to make sure everything is ready setTimeout(function() { console.log("Initializing all panels..."); // Start each panel one by one if (typeof window.renderStrictNextPattern === 'function') { console.log("Starting Strict Pattern panel..."); window.renderStrictNextPattern(); } if (typeof window.renderGlobalOracle === 'function') { console.log("Starting Global Oracle panel..."); window.renderGlobalOracle(); } if (typeof window.displayPatternCredibility === 'function') { console.log("Starting Pattern Credibility panel..."); window.displayPatternCredibility(); } if (typeof window.updateOracleAccuracy === 'function') { console.log("Starting Oracle Accuracy panel..."); window.updateOracleAccuracy(); } console.log("All panels initialized successfully!"); }, 1000); // Wait 1000 milliseconds (1 second) }); // === LEAKAGE-FREE BACKTEST (ADD THIS) === function runBacktest(splitRatio = 0.7) { if (raceHistory.length < 30) { console.warn('Need at least 30 races for evaluation'); return null; } const split = Math.floor(raceHistory.length * splitRatio); const train = raceHistory.slice(0, split); const test = raceHistory.slice(split); // Build TRAIN-only pattern distribution const trainSeq = buildPatternSeqFrom(train); const trainPatDist = nextPatternDistributionFromSeq(trainSeq, 3); let correct = 0, total = 0; test.forEach(r => { const originalSelected = [...selectedVehicles]; selectedVehicles = [...r.vehicles]; // Use TEST race's track and timeSegment const preds = calculateAdvancedPredictionForEval(r.track, r.timeSegment, trainPatDist); if (preds[0].vehicle === r.actual) correct++; total++; selectedVehicles = originalSelected; }); const acc = (correct / total * 100).toFixed(1); console.log(`Backtest accuracy: ${acc}% on ${total} test races`); return { acc: Number(acc), total }; } // โœ… Run setup when the page is loaded window.addEventListener("DOMContentLoaded", () => { initializeUI(); }); // === Show Prediction Results === function showPredictionResults() { const resultsContainer = document.getElementById("resultsContainer"); resultsContainer.innerHTML = ""; // clear old results const predictions = calculateAdvancedPrediction(); if (!predictions || predictions.length === 0) { resultsContainer.innerHTML = `<p class="text-gray-400">โš ๏ธ No prediction available. Make sure you selected 3 vehicles and a track.</p>`; return; } // Take only top 3 predictions const top3 = predictions.slice(0, 3); top3.forEach((p, index) => { const div = document.createElement("div"); div.className = "p-3 bg-gray-800 rounded-lg border border-gray-600"; div.innerHTML = ` <p class="font-bold text-lg ${index === 0 ? 'text-green-400' : (index === 1 ? 'text-blue-400' : 'text-yellow-400')}"> ${index + 1}. ${p.vehicle} </p> <p class="text-sm text-gray-300">Probability: ${p.probability.toFixed(1)}%</p> <p class="text-sm text-gray-400">Odds: ${p.odds}</p> `; resultsContainer.appendChild(div); }); } window.addEventListener("DOMContentLoaded", () => { initializeUI(); }); </script> </body> </html>