<!DOCTYPE html>
<html lang="it" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>Cerchio di Spilimbergo — Suite Completa v2.2</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600&display=swap');
:root[data-theme="dark"]{
--frame-dark:#241a10; --frame-dark2:#1a130b;
--brass:#b4842c; --brass-hi:#e0b04a;
--paper-1:#f4ecd8; --ink:#2c1e12; --ink-soft:#5a4630;
--garnet:#8c2a2a; --accent:#0f6e6e; --gold-fill:#e0b04a;
--panel-bg:linear-gradient(180deg,#2b1f12,#201709);
--body-bg:radial-gradient(ellipse at 50% -10%, #2c2013 0%, #1a130b 55%);
--input-bg:#171009;
}
:root[data-theme="light"]{
--frame-dark:#e8dcc4; --frame-dark2:#d4c5a0;
--brass:#8b6914; --brass-hi:#a67c1a;
--paper-1:#2c1e12; --ink:#f4ecd8; --ink-soft:#8a7a5c;
--garnet:#b23a3a; --accent:#1a8f8f; --gold-fill:#d4a017;
--panel-bg:linear-gradient(180deg,#f5edd8,#e8dcc4);
--body-bg:radial-gradient(ellipse at 50% -10%, #f5edd8 0%, #d4c5a0 55%);
--input-bg:#faf6eb;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;min-height:100%;background:var(--body-bg);color:var(--paper-1);
font-family:'Inter',sans-serif;transition:background .4s,color .4s;}
h1{font-family:'Cormorant Garamond',serif;font-weight:700;font-size:clamp(26px,4vw,40px);margin:0 0 2px;color:var(--brass-hi);}
.subtitle{font-family:'Cormorant Garamond',serif;font-style:italic;color:#c9b98f;font-size:16px;margin:0 0 20px;}
.wrap{max-width:1320px;margin:0 auto;padding:28px 16px 56px;}
.toolbar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;justify-content:center;}
.toolbar button{background:var(--panel-bg);border:1px solid var(--brass);color:var(--paper-1);
font-family:'Inter',sans-serif;font-size:13px;padding:8px 14px;border-radius:8px;cursor:pointer;transition:all .2s;}
.toolbar button:hover{border-color:var(--brass-hi);transform:translateY(-1px);}
.layout{display:grid;grid-template-columns:minmax(300px,560px) 360px;gap:24px;align-items:start;}
@media (max-width:980px){.layout{grid-template-columns:1fr;}}
.wheel-card{position:relative;background:var(--panel-bg);border:1px solid #4a3a20;border-radius:16px;
padding:10px;box-shadow:0 30px 60px -20px rgba(0,0,0,.6),inset 0 0 30px rgba(0,0,0,.35);}
svg{width:100%;height:auto;display:block;user-select:none;}
#lines-0,#lines-1,#lines-2,#text-0,#text-1,#text-2{transition:transform .5s cubic-bezier(0.34,1.56,0.64,1);}
.num{font-family:'Cormorant Garamond',serif;font-weight:500;text-anchor:middle;dominant-baseline:middle;
pointer-events:none;cursor:pointer;fill:#1a1206;transition:fill .3s,font-weight .2s;}
.num.highlighted{fill:var(--ink);font-weight:800;}
.num.draw-hit{fill:#1a6b3c;font-weight:800;}
.highlight-dot{fill:var(--gold-fill);opacity:0;pointer-events:none;transition:opacity .3s;}
.draw-dot{fill:#4fae7a;opacity:0;pointer-events:none;transition:opacity .3s;}
.compass-label{font-family:'Cormorant Garamond',serif;font-weight:700;font-size:26px;fill:var(--ink);text-anchor:middle;dominant-baseline:middle;}
.ring-track{fill:transparent;cursor:grab;touch-action:none;}
.ring-track.dragging{cursor:grabbing;}
.divider{stroke:var(--ink-soft);stroke-width:1;opacity:.35;}
.band{fill:none;stroke:var(--ink-soft);stroke-width:1.2;opacity:.55;}
.gap-mark{fill:var(--ink);opacity:.14;}
.opposite-line{stroke:var(--gold-fill);stroke-width:2;stroke-dasharray:4 4;opacity:.6;}
.panel{display:flex;flex-direction:column;gap:14px;max-height:88vh;overflow-y:auto;padding-right:4px;}
.panel::-webkit-scrollbar{width:6px;}
.panel::-webkit-scrollbar-thumb{background:var(--brass);border-radius:3px;}
.card{background:var(--panel-bg);border:1px solid #4a3a20;border-radius:12px;padding:13px 15px;}
.card h3{margin:0 0 10px;font-family:'Cormorant Garamond',serif;font-weight:600;font-size:17px;
color:var(--brass-hi);display:flex;align-items:center;gap:8px;}
.swatch{width:11px;height:11px;border-radius:50%;display:inline-block;}
.btn-row{display:flex;gap:8px;align-items:center;}
button.step{flex:1;background:#332512;border:1px solid var(--brass);color:var(--paper-1);
font-family:'Inter',sans-serif;font-size:13px;padding:9px 6px;border-radius:8px;cursor:pointer;transition:all .15s;}
button.step:hover{background:#40300f;border-color:var(--brass-hi);}
button.step.primary{background:var(--brass);border-color:var(--brass-hi);color:#1a1206;font-weight:600;}
button.step.primary:hover{background:var(--brass-hi);}
button.step.danger{border-color:var(--garnet);color:#e0a0a0;}
button.step.danger:hover{background:rgba(140,42,42,.2);}
button.step:disabled{opacity:.4;cursor:not-allowed;}
table{width:100%;border-collapse:collapse;}
table th{font-weight:500;font-size:12px;color:#c9b98f;text-align:center;padding-bottom:6px;border-bottom:1px solid #4a3a20;}
table td{text-align:center;padding:6px 4px;font-family:'Cormorant Garamond',serif;font-weight:600;
font-size:18px;color:var(--paper-1);border-bottom:1px solid #332512;}
table td.blank{color:#8a7a5c;opacity:.7;font-style:italic;font-size:13px;}
.ring-name-cell{text-align:left;font-family:'Inter',sans-serif;font-weight:500;font-size:12px;color:#c9b98f;}
.hint{font-size:12px;line-height:1.55;color:#b3a37e;padding:10px 4px 0;}
textarea,input[type=number],input[type=text],select,input[type=file]{background:var(--input-bg);border:1px solid var(--brass);
color:var(--paper-1);font-family:'Cormorant Garamond',serif;font-size:16px;border-radius:8px;padding:8px;width:100%;}
select{font-family:'Inter',sans-serif;font-size:12px;}
input[type=file]{font-family:'Inter',sans-serif;font-size:12px;padding:6px;}
input[type=file]::file-selector-button{background:var(--brass);border:1px solid var(--brass-hi);color:#1a1206;
font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;margin-right:8px;}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;}
.chip{background:#332512;border:1px solid var(--gold-fill);color:var(--gold-fill);
font-family:'Cormorant Garamond',serif;font-weight:600;font-size:14px;padding:2px 9px;border-radius:12px;cursor:pointer;}
.chip.draw-chip{border-color:#4fae7a;color:#4fae7a;}
.archive-info{display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(0,0,0,.15);
border-radius:8px;margin-bottom:8px;font-size:12px;color:#c9b98f;}
.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
.status-ok{background:#4fae7a;} .status-no{background:#e0a0a0;}
.progress-bar-bg{height:6px;background:rgba(0,0,0,.3);border-radius:3px;overflow:hidden;margin-top:8px;display:none;}
.progress-bar-fill{height:100%;background:var(--brass-hi);border-radius:3px;transition:width .3s;width:0%;}
.progress-label{font-size:11px;color:#b3a37e;margin-top:4px;text-align:center;min-height:14px;}
.archive-log{max-height:110px;overflow-y:auto;margin-top:8px;padding:6px;background:rgba(0,0,0,.2);
border-radius:6px;font-family:'Courier New',monospace;font-size:10.5px;color:#b3a37e;line-height:1.5;display:none;}
.log-ok{color:#4fae7a;} .log-warn{color:#e0b04a;} .log-err{color:#e0a0a0;}
.archive-use{margin-top:10px;padding-top:10px;border-top:1px solid #4a3a20;}
.validation-report{margin-top:8px;font-size:11.5px;max-height:160px;overflow-y:auto;
padding:8px;background:rgba(0,0,0,.15);border-radius:8px;display:none;}
.validation-report table{margin-top:6px;}
.validation-report th{font-size:10px;padding:3px 4px;}
.validation-report td{font-size:10.5px;padding:3px 4px;}
.esiti-card{border:2px solid var(--brass-hi) !important;}
.esiti-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}
.esiti-subtitle{font-size:11px;color:#b3a37e;margin-bottom:10px;line-height:1.4;}
.esiti-grid{display:flex;flex-wrap:wrap;gap:12px;}
.esito-item{display:flex;flex-direction:column;align-items:center;gap:4px;}
.esito-num{position:relative;width:58px;height:58px;border-radius:50%;
display:flex;flex-direction:column;align-items:center;justify-content:center;
background:radial-gradient(circle at 30% 30%, #f7d774, var(--gold-fill));
color:#1a1206;font-weight:700;cursor:pointer;
box-shadow:0 3px 10px rgba(0,0,0,.4);transition:transform .15s;}
.esito-num:hover{transform:scale(1.1);}
.esito-num.top1{background:radial-gradient(circle at 30% 30%, #ff9d6c, #e0552c);color:#fff;
box-shadow:0 0 14px rgba(224,85,44,.7);}
.esito-num .n{font-size:21px;font-family:'Cormorant Garamond',serif;line-height:1;}
.esito-num .s{font-size:9px;font-weight:600;opacity:.85;}
.esito-fonti{display:flex;gap:3px;flex-wrap:wrap;justify-content:center;max-width:70px;}
.fonte-dot{width:8px;height:8px;border-radius:50%;}
.fd-motore{background:#e0b04a;} .fd-conv{background:#7fd4d4;} .fd-backtest{background:#4fae7a;}
.esiti-legenda{display:flex;gap:14px;margin-top:12px;font-size:10px;color:#b3a37e;flex-wrap:wrap;}
.esiti-legenda span{display:flex;align-items:center;gap:5px;}
.esiti-vuoto{text-align:center;color:#8a7a5c;font-size:12px;padding:18px 8px;line-height:1.6;}
.esiti-refresh{flex:0 0 auto !important;padding:7px 14px !important;}
.conv-archive-box{background:rgba(15,110,110,.12);border:1px solid var(--accent);border-radius:8px;padding:10px;margin-bottom:10px;}
.conv-archive-box .title{font-size:12px;font-weight:600;color:#7fd4d4;margin-bottom:6px;}
.pred-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px;}
.pred-box{background:rgba(0,0,0,.18);border-radius:8px;padding:8px;text-align:center;}
.pred-box .lbl{font-size:10.5px;letter-spacing:.08em;color:#c9b98f;text-transform:uppercase;}
.pred-box .val{font-family:'Cormorant Garamond',serif;font-size:20px;font-weight:700;color:var(--gold-fill);}
.cand-list{margin-top:10px;max-height:180px;overflow-y:auto;}
.cand-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:rgba(0,0,0,.15);
margin-bottom:4px;font-size:12.5px;cursor:pointer;transition:background .15s;}
.cand-item:hover{background:rgba(224,176,74,.18);}
.cand-item .cn{font-family:'Cormorant Garamond',serif;font-size:17px;font-weight:700;color:var(--gold-fill);min-width:28px;text-align:center;}
.cm{color:#b3a37e;flex:1;line-height:1.35;}
.cs{color:#4fae7a;font-weight:600;}
.fig-badge{display:inline-block;padding:2px 9px;border-radius:10px;font-size:11px;margin:2px 3px 2px 0;border:1px solid;}
.distance-list{max-height:160px;overflow-y:auto;padding:4px;}
.distance-item{display:grid;grid-template-columns:1fr auto auto;gap:8px;padding:5px 8px;
background:rgba(0,0,0,.15);border-radius:6px;margin-bottom:4px;font-family:'Cormorant Garamond',serif;font-size:14px;}
.figure-item{display:flex;justify-content:space-between;padding:5px 8px;background:rgba(140,42,42,.15);
border-radius:6px;margin-bottom:4px;font-size:13px;}
.figure-item .type{color:var(--gold-fill);font-weight:600;}
.alignment-result{padding:8px;background:rgba(15,110,110,.15);border-radius:6px;margin-top:8px;font-size:13px;
text-align:center;min-height:38px;display:flex;align-items:center;justify-content:center;}
.config-input-row{display:flex;gap:8px;margin-bottom:8px;}
.config-input-row input{flex:1;}
.config-input-row button{background:var(--brass);border:1px solid var(--brass-hi);color:#1a1206;
font-weight:600;padding:6px 12px;border-radius:6px;cursor:pointer;font-size:13px;}
.saved-list{max-height:140px;overflow-y:auto;}
.saved-item{display:flex;justify-content:space-between;align-items:center;padding:6px 8px;
background:rgba(0,0,0,.15);border-radius:6px;margin-bottom:4px;font-size:13px;}
.saved-item .name{color:var(--brass-hi);font-weight:500;cursor:pointer;flex:1;}
.saved-item .del{color:#e0a0a0;cursor:pointer;padding:2px 6px;}
.hist-list{margin-top:8px;max-height:120px;overflow-y:auto;}
.hist-item{display:flex;align-items:center;gap:6px;padding:5px 8px;background:rgba(0,0,0,.15);
border-radius:6px;margin-bottom:4px;font-size:12px;}
.hist-item .ruota{color:var(--brass-hi);font-weight:600;min-width:60px;}
.hist-item .nums{font-family:'Cormorant Garamond',serif;font-size:14px;flex:1;}
.hist-item .del{color:#e0a0a0;cursor:pointer;}
.conv-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;
background:rgba(15,110,110,.14);margin-bottom:4px;font-size:12.5px;cursor:pointer;}
.conv-item:hover{background:rgba(15,110,110,.3);}
.conv-item .cn{font-family:'Cormorant Garamond',serif;font-size:17px;font-weight:700;color:#7fd4d4;min-width:28px;text-align:center;}
.conv-badge{background:var(--accent);color:#f4ecd8;border-radius:10px;padding:1px 8px;font-size:11px;font-weight:600;}
.fig-shape{fill-opacity:.10;stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round;}
.fig-shape-glow{filter:drop-shadow(0 0 5px currentColor);}
.fig-ray{stroke-width:1.4;stroke-dasharray:7 5;opacity:.45;fill:none;}
.fig-vertex{stroke-width:2.5;fill-opacity:.95;}
.fig-vertex-ring{fill:none;stroke-width:1.5;opacity:.6;}
.fig-center{opacity:.9;}
.fig-arc{fill:none;stroke-width:2.2;opacity:.55;stroke-linecap:round;}
.fig-arc-inner{fill:none;stroke-width:1.5;opacity:.35;stroke-dasharray:4 3;}
.fig-diameter{stroke-width:2.5;opacity:.7;stroke-linecap:round;}
.fig-label-bg{fill:rgba(0,0,0,.55);rx:4;ry:4;}
.fig-label{font:700 11px Inter,sans-serif;fill:#fff;text-anchor:middle;dominant-baseline:middle;
paint-order:stroke;stroke:rgba(0,0,0,.6);stroke-width:3px;}
.fig-angle-label{font:600 9px Inter,sans-serif;fill-opacity:.85;text-anchor:middle;}
.fig-crosshair{stroke-width:1.5;opacity:.7;}
@keyframes figPulse{0%,100%{stroke-opacity:.95}50%{stroke-opacity:.45}}
.fig-pulse{animation:figPulse 2.6s ease-in-out infinite;}
.bt-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px;}
.bt-grid label{font-size:11px;color:#c9b98f;display:block;margin-bottom:3px;}
.bt-result{margin-top:10px;padding:12px;background:rgba(0,0,0,.18);border-radius:8px;font-size:12px;}
.bt-row{display:flex;align-items:center;gap:6px;margin:6px 0;flex-wrap:wrap;}
.bt-num{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;
border-radius:50%;font-weight:700;font-family:'Cormorant Garamond',serif;font-size:16px;
background:#332512;border:1px solid var(--gold-fill);color:var(--gold-fill);transition:transform .15s;}
.bt-num:hover{transform:scale(1.08);}
.bt-num.actual{background:#1a6b3c;border-color:#4fae7a;color:#fff;}
.bt-num.hit{background:var(--gold-fill);color:#1a1206;border:2px solid #4fae7a;box-shadow:0 0 8px rgba(79,174,122,.6);}
.bt-label{font-size:11px;color:#c9b98f;margin:8px 0 4px;}
input[type=date]{background:var(--input-bg);border:1px solid var(--brass);color:var(--paper-1);
border-radius:8px;padding:8px;width:100%;font-family:'Inter',sans-serif;font-size:13px;}
input[type=date]::-webkit-calendar-picker-indicator{filter:invert(.7);cursor:pointer;}
.bt-jump{cursor:pointer;text-decoration:underline;color:var(--brass-hi);}
.notification{position:fixed;top:20px;right:20px;background:var(--brass);color:#1a1206;padding:12px 20px;
border-radius:8px;font-weight:600;box-shadow:0 4px 12px rgba(0,0,0,.3);opacity:0;transform:translateY(-20px);
transition:all .3s;z-index:1000;pointer-events:none;}
.notification.show{opacity:1;transform:translateY(0);}
.guide-modal{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;align-items:center;justify-content:center;z-index:2000;padding:16px;}
.guide-modal.open{display:flex;}
.guide-box{background:var(--panel-bg);border:1px solid var(--brass);border-radius:14px;max-width:760px;width:100%;
max-height:85vh;overflow-y:auto;padding:24px 28px;}
.guide-box h2{font-family:'Cormorant Garamond',serif;color:var(--brass-hi);margin:0 0 12px;}
.guide-box h4{color:var(--brass-hi);margin:16px 0 6px;font-family:'Cormorant Garamond',serif;font-size:17px;}
.guide-box p,.guide-box li{font-size:13.5px;line-height:1.6;}
.guide-close{float:right;background:none;border:1px solid var(--garnet);color:#e0a0a0;border-radius:6px;padding:4px 10px;cursor:pointer;}
.stats-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-top:8px;}
.stat-item{text-align:center;padding:8px;background:rgba(0,0,0,.15);border-radius:6px;cursor:pointer;transition:background .15s;}
.stat-item:hover{background:rgba(224,176,74,.18);}
.stat-item .stat-num{font-size:18px;font-weight:700;color:var(--gold-fill);font-family:'Cormorant Garamond',serif;}
.stat-item .stat-freq{font-size:10px;color:#c9b98f;margin-top:2px;}
.coerenza-box{margin-top:10px;padding:10px;background:rgba(0,0,0,.18);border-radius:8px;font-size:12px;}
.coerenza-item{padding:4px 6px;margin:3px 0;background:rgba(79,174,122,.12);border-radius:4px;color:#4fae7a;}
.pattern-box{margin-top:8px;font-size:11.5px;}
.pattern-item{display:flex;justify-content:space-between;padding:5px 8px;background:rgba(0,0,0,.15);
border-radius:6px;margin-bottom:4px;}
.pattern-item .pair{color:var(--gold-fill);font-weight:600;font-family:'Cormorant Garamond',serif;font-size:14px;}
.pattern-item .freq{color:#c9b98f;}
.download-info{background:rgba(224,176,74,.15);border:1px solid var(--brass);border-radius:8px;padding:10px;margin-bottom:10px;font-size:11.5px;line-height:1.5;}
.download-info strong{color:var(--brass-hi);}
.download-info ol{margin:6px 0;padding-left:20px;}
.download-info li{margin:3px 0;}
.download-info a{color:var(--brass-hi);text-decoration:underline;}
</style>
</head>
<body>
<div class="wrap">
<h1>Cerchio di Spilimbergo — Suite Completa v2.2</h1>
<p class="subtitle">Archivio ZIP Brightstar · Cache IndexedDB · Ciclometria Avanzata · Motore Previsivo Potenziato</p>
<div class="toolbar">
<button id="themeToggle">🌙 Tema</button>
<button id="shareBtn">🔗 Condividi</button>
<button id="importDemoBtn">📥 Demo</button>
<button id="clearAllBtn">🗑️ Pulisci Tutto</button>
<button id="guideBtn">❓ Guida</button>
</div>
<div class="layout">
<div class="wheel-card"><svg id="wheel" viewBox="0 0 700 700"></svg></div>
<div class="panel">
<!-- Ghiere -->
<div class="card" data-ring="0"><h3><span class="swatch" style="background:#8c2a2a"></span>Anello interno</h3>
<div class="btn-row"><button class="step" data-ring="0" data-dir="-1">↺</button><button class="step" data-ring="0" data-dir="1">↻</button></div></div>
<div class="card" data-ring="1"><h3><span class="swatch" style="background:#8c2a2a"></span>Anello medio</h3>
<div class="btn-row"><button class="step" data-ring="1" data-dir="-1">↺</button><button class="step" data-ring="1" data-dir="1">↻</button></div></div>
<div class="card" data-ring="2"><h3><span class="swatch" style="background:#8c2a2a"></span>Anello esterno</h3>
<div class="btn-row"><button class="step" data-ring="2" data-dir="-1">↺</button><button class="step" data-ring="2" data-dir="1">↻</button></div></div>
<!-- Lettura cardinale -->
<div class="card">
<h3>Lettura ai 4 punti cardinali</h3>
<table><tr><th></th><th>N</th><th>S</th><th>E</th><th>O</th></tr>
<tr><td class="ring-name-cell">Interno</td><td id="r0-N"></td><td id="r0-S"></td><td id="r0-E"></td><td id="r0-O"></td></tr>
<tr><td class="ring-name-cell">Medio</td><td id="r1-N"></td><td id="r1-S"></td><td id="r1-E"></td><td id="r1-O"></td></tr>
<tr><td class="ring-name-cell">Esterno</td><td id="r2-N"></td><td id="r2-S"></td><td id="r2-E"></td><td id="r2-O"></td></tr>
</table>
<button class="step danger" id="resetBtn" style="margin-top:8px;width:100%;">Riporta le ghiere a riposo</button>
</div>
<!-- STATISTICHE RAPIDE -->
<div class="card">
<h3>📊 Statistiche & Pattern</h3>
<div class="btn-row">
<select id="statsRuota" style="flex:1;"></select>
<button class="step" id="statsRun" style="flex:0 0 auto;padding:8px 14px;">Analizza</button>
</div>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="statsTop10">Top 10 Frequenti</button>
<button class="step" id="statsPattern">Pattern Coppie</button>
</div>
<div id="statsResults"></div>
</div>
<!-- ARCHIVI ESTRAZIONI -->
<div class="card" id="archiveCard">
<h3>📚 Archivio Estrazioni</h3>
<div class="download-info">
<strong>📥 Come caricare l'archivio:</strong>
<ol>
<li>Scarica manualmente il file: <a href="https://www.brightstarlottery.it/STORICO_ESTRAZIONI_LOTTO/storico.zip" target="_blank">storico.zip</a></li>
<li>Clicca su <strong>"📂 Carica ZIP Locale"</strong> qui sotto</li>
<li>Seleziona il file ZIP appena scaricato</li>
<li>Il sistema lo decomprimerà e creerà la cache automaticamente</li>
</ol>
<em>💡 La cache resta salvata nel browser per usi futuri!</em>
</div>
<div class="archive-info"><span class="status-dot status-no" id="cacheStatusDot"></span><span id="cacheInfoText">Nessuna cache</span></div>
<div class="btn-row">
<button class="step primary" id="btnLoadLocal">📂 Carica ZIP Locale</button>
<input type="file" id="fileInput" accept=".zip" style="display:none;">
</div>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="btnLoadCache" disabled>💾 Carica Cache</button>
<button class="step" id="btnValida" disabled>🔍 Valida</button>
<button class="step danger" id="btnClearCache">🗑️ Cancella</button>
</div>
<div class="progress-bar-bg" id="progressWrap"><div class="progress-bar-fill" id="progressBar"></div></div>
<div class="progress-label" id="progressLabel"></div>
<div class="archive-log" id="archiveLog"></div>
<div class="validation-report" id="validationReport"></div>
<div class="archive-use">
<label style="font-size:11px;color:#c9b98f;display:block;margin-bottom:3px;">Epoca storica</label>
<select id="epocaSelect" style="margin-bottom:6px;">
<option value="completa">Archivio Completo</option>
<option value="venus">Venus (2005-oggi)</option>
<option value="quattro">4 Estrazioni (2023-oggi)</option>
</select>
<label style="font-size:11px;color:#c9b98f;display:block;margin-bottom:3px;">Usa archivio sul Cerchio</label>
<div class="btn-row"><select id="archivioRuotaSelect" style="flex:1;"></select></div>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="btnUseLast1" disabled>🎯 Ultima estrazione</button>
<button class="step" id="btnUseLast3" disabled>🎯 Ultime 3</button>
</div>
</div>
</div>
<!-- NUMERI PROMETTENTI (ESITI AGGREGATI) -->
<div class="card esiti-card">
<div class="esiti-header">
<h3 style="margin:0;">🎯 Numeri Promettenti</h3>
<button class="step primary esiti-refresh" id="esitiRefresh">↻ Aggiorna</button>
</div>
<div class="esiti-subtitle">
Aggrega gli esiti di <b>Motore Previsivo</b>, <b>Convergenze</b> e <b>Verifica a Ritroso</b> con scoring avanzato.
Arancio = top assoluto. Clicca un pallino per evidenziarlo sul Cerchio.
</div>
<div id="esitiContent">
<div class="esiti-vuoto">
Esegui almeno un'analisi (⚡ Motore, 🔀 Convergenze o 🕰️ Verifica a Ritroso)<br>
poi premi <b>↻ Aggiorna</b> per vedere i numeri promettenti.
</div>
</div>
</div>
<!-- VERIFICA A RITROSO -->
<div class="card" id="backtestCard">
<h3>🕰️ Verifica a Ritroso</h3>
<div class="bt-grid">
<div>
<label>Ruota</label>
<select id="btRuotaSelect"></select>
</div>
<div>
<label>Data (calendario)</label>
<input type="date" id="btDateInput">
</div>
<div>
<label>Estrazioni base</label>
<input type="number" id="btNumBase" value="60" min="10" max="500">
</div>
<div>
<label>Top candidati</label>
<input type="number" id="btTopN" value="10" min="5" max="45">
</div>
</div>
<div class="btn-row" style="margin-top:8px;">
<button class="step primary" id="btRun">🔍 Verifica a ritroso</button>
</div>
<div id="btResults"></div>
</div>
<!-- MOTORE PREVISIVO L1 -->
<div class="card">
<h3>🤖 Motore Previsivo <span style="font-size:11px;color:#8a7a5c;">L1 Potenziato</span></h3>
<div class="btn-row"><button class="step primary" id="engineRun">⚡ Genera figure e previsione</button></div>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="heatToggle">🔥 Calore</button>
<button class="step" id="tourBtn">🎬 Tour</button>
<button class="step" id="engineClear">🧹</button>
</div>
<div id="engineResults"></div>
<div style="font-size:10.5px;color:#8a7a5c;font-style:italic;margin-top:8px;text-align:center;">Metodo tradizionale: solo studio e intrattenimento. Gioca responsabilmente.</div>
</div>
<!-- STORICO L2 -->
<div class="card">
<h3>📜 Storico & Convergenze <span style="font-size:11px;color:#8a7a5c;">L2</span></h3>
<!-- CARICA RUOTE DA ARCHIVIO -->
<div class="conv-archive-box">
<div class="title">📥 Carica ruote dall'archivio</div>
<div class="btn-row">
<select id="convArchivioRuota" style="flex:1;"></select>
<input type="number" id="convArchivioN" value="10" min="2" max="100" style="width:70px;">
</div>
<button class="step" id="btnCaricaRuoteConv" style="margin-top:6px;width:100%;">📥 Carica estrazioni nelle Convergenze</button>
</div>
<textarea id="historyInput" rows="3" placeholder="Oppure inserisci a mano, una estrazione per riga:
Bari 7 23 45 68 81
Napoli 12 34 56 72 90"></textarea>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="histLoad">📥 Carica</button>
<button class="step" id="histDemo">Demo</button>
<button class="step" id="histClear">🧹</button>
</div>
<div class="hist-list" id="histList"></div>
<div class="btn-row" style="margin-top:6px;">
<select id="ruotaFilter"></select>
<button class="step primary" id="convRun">🔀 Convergenze</button>
</div>
<div id="convResults"></div>
</div>
<!-- Estrazione singola -->
<div class="card">
<h3>Estrazione da analizzare</h3>
<textarea id="drawInput" rows="2" placeholder="Incolla i numeri: 4 26 38 60 61"></textarea>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="drawGo">Evidenzia</button>
<button class="step" id="drawClear">Pulisci</button>
</div>
<div class="chips" id="drawChips"></div>
<div id="coerenzaBox"></div>
</div>
<!-- Distanze e figure -->
<div class="card">
<h3>📐 Distanze e Figure</h3>
<div class="distance-list" id="distanceList"><div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Evidenzia almeno 2 numeri</div></div>
<div id="figureList"></div>
</div>
<!-- Allineamenti potenziati -->
<div class="card">
<h3>🎯 Cerca Allineamenti <span style="font-size:11px;color:#8a7a5c;">potenziato</span></h3>
<div class="btn-row" style="margin-bottom:6px;">
<select id="alignMode" style="flex:1;">
<option value="entrambi">🟡 Oro + 🟢 Verde</option>
<option value="oro">🟡 Solo numeri evidenziati</option>
<option value="verde">🟢 Solo estrazione caricata</option>
</select>
</div>
<button class="step primary" id="findAlignmentBtn" style="width:100%;">🔍 Trova migliori configurazioni</button>
<div class="alignment-result" id="alignmentResult"><span style="color:#8a7a5c;font-size:12px;">Evidenzia numeri o carica una estrazione, poi cerca</span></div>
<div id="alignTopList"></div>
<div id="alignDetail"></div>
</div>
<!-- Configurazioni -->
<div class="card">
<h3>💾 Configurazioni</h3>
<div class="config-input-row"><input type="text" id="configNameInput" placeholder="Nome configurazione"><button id="saveConfigBtn">Salva</button></div>
<div class="saved-list" id="savedList"></div>
</div>
<!-- Scova numero -->
<div class="card">
<h3>Scova numero</h3>
<div class="btn-row"><input type="number" id="finderInput" min="1" max="90" placeholder="1-90"><button class="step" id="finderGo" style="flex:0 0 auto;padding:8px 14px;">Trova</button></div>
<div class="chips" id="chips"></div>
<button class="step danger" id="clearHighlights" style="width:100%;margin-top:8px;">Pulisci evidenziazioni</button>
</div>
<p class="hint">Trascina un anello per ruotarlo. 📚 Carica l'archivio ZIP locale (cache su IndexedDB). 🕰️ Verifica a Ritroso con calendario. 🎯 Numeri Promettenti aggrega tutti gli esiti con scoring avanzato. 📊 Statistiche frequenze e pattern.</p>
</div>
</div>
</div>
<!-- Modale guida -->
<div class="guide-modal" id="guideModal">
<div class="guide-box">
<button class="guide-close" id="guideClose">✕ Chiudi</button>
<h2>Guida alla Suite Completa v2.2</h2>
<h4>1. Archivio Estrazioni (ZIP Brightstar + Cache IndexedDB)</h4>
<p>📂 <strong>Carica ZIP Locale</strong>: Scarica manualmente il file <code>storico.zip</code> da Brightstar Lottery, poi caricalo nel browser. Il sistema lo decomprime automaticamente e salva su <b>IndexedDB</b>. La cache resta salvata per usi futuri.</p>
<h4>2. Statistiche & Pattern</h4>
<p>Analizza frequenze dei numeri e pattern di coppie per ogni ruota. Top 10 numeri più frequenti e coppie più uscite.</p>
<h4>3. Numeri Promettenti (Scoring Avanzato)</h4>
<p>Aggrega automaticamente i risultati di <b>Motore Previsivo</b>, <b>Convergenze</b> e <b>Verifica a Ritroso</b> con sistema di pesatura intelligente che premia la coerenza tra fonti.</p>
<h4>4. Verifica a Ritroso (Backtest)</h4>
<p>Scegli ruota + data dal calendario. Il sistema ricostruisce le estrazioni fino a quella data, genera i numeri "dovuti" e li confronta con l'estrazione reale successiva.</p>
<h4>5. Analisi Coerenza Geometrica</h4>
<p>Quando carichi una estrazione, il sistema valuta automaticamente la coerenza geometrica (figure ciclometriche presenti).</p>
<h4>6. Storico & Convergenze (con archivio)</h4>
<p>Nel box "Carica ruote dall'archivio" puoi scegliere una ruota o tutte, impostare N estrazioni recenti, e caricarle automaticamente nelle Convergenze.</p>
<h4>7. Cerchio di Spilimbergo</h4>
<p>Tre ghiere da 30 numeri (passo 23). Trascina o usa ↺/↻ per ruotare. La tabella mostra i numeri sotto N/S/E/O.</p>
<h4>⚠️ Nota importante</h4>
<p>Metodi della tradizione lottologica, privi di validità scientifica. Il Lotto è casuale. Gioca responsabilmente.</p>
</div>
</div>
<div class="notification" id="notification"></div>
<script>
// ============================================================
// PARAMETRI E COSTANTI - AGGIORNATI CON NUOVI CODICI RUOTA
// ============================================================
const RUOTE_NOME = {
'BA':'Bari','CA':'Cagliari','FI':'Firenze','GE':'Genova','MI':'Milano',
'NA':'Napoli','PA':'Palermo','RM':'Roma','RN':'Nazionale','TO':'Torino','VE':'Venezia'
};
// Mappatura codici dal file storico.txt
const CODICE_TO_NOME = {
'BA':'Bari','CA':'Cagliari','FI':'Firenze','GE':'Genova','MI':'Milano',
'NA':'Napoli','PA':'Palermo','RM':'Roma','RN':'Nazionale','TO':'Torino','VE':'Venezia'
};
const EPOCHE = {
'venus': { nome:'Venus (2005-oggi)', dataInizio:'2005-05-05' },
'quattro': { nome:'4 Estrazioni (2023-oggi)', dataInizio:'2023-07-07' },
'completa': { nome:'Archivio Completo', dataInizio:null }
};
// ============================================================
// STATO GLOBALE
// ============================================================
let archivio = null;
let LAST_BACKTEST = [];
let STATISTICHE_CACHE = {};
// ============================================================
// ARCHIVIO: CACHE SU INDEXEDDB
// ============================================================
const DB_NAME = 'icq_archivio_db';
const DB_STORE = 'estrazioni';
const CACHE_META_KEY = 'archivio_estrazioni_meta';
function apriDB(){
return new Promise((resolve, reject)=>{
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = e => {
const db = e.target.result;
if(!db.objectStoreNames.contains(DB_STORE)) db.createObjectStore(DB_STORE, { keyPath:'id' });
};
req.onsuccess = e => resolve(e.target.result);
req.onerror = () => reject(req.error);
});
}
function cacheEsiste(){ return localStorage.getItem(CACHE_META_KEY) !== null; }
function cacheUltimaData(){
const m = localStorage.getItem(CACHE_META_KEY);
return m ? JSON.parse(m).ultimaData : null;
}
async function salvaCache(righe, ultimaData){
try{
const db = await apriDB();
const compatte = righe.map(r => [r.data, r.ruota, r.n1, r.n2, r.n3, r.n4, r.n5]);
await new Promise((resolve, reject)=>{
const tx = db.transaction(DB_STORE, 'readwrite');
tx.objectStore(DB_STORE).put({ id:'archivio', righe:compatte, ultimaData, salvatoIl:new Date().toISOString() });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
db.close();
localStorage.setItem(CACHE_META_KEY, JSON.stringify({ultimaData, nRighe:righe.length}));
if(navigator.storage && navigator.storage.persist) navigator.storage.persist();
logArchivio(`✅ Cache salvata (IndexedDB): ${righe.length.toLocaleString('it-IT')} righe, ultima: ${ultimaData}`, 'ok');
return true;
}catch(e){
logArchivio(`❌ Errore salvataggio IndexedDB: ${e.message}`, 'err');
return false;
}
}
async function caricaCache(){
try{
const db = await apriDB();
const dati = await new Promise((resolve, reject)=>{
const tx = db.transaction(DB_STORE, 'readonly');
const req = tx.objectStore(DB_STORE).get('archivio');
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
db.close();
if(!dati) return null;
const righe = dati.righe.map(a => ({data:a[0], ruota:a[1], n1:a[2], n2:a[3], n3:a[4], n4:a[5], n5:a[6]}));
archivio = { righe, ultimaData: dati.ultimaData };
logArchivio(`📂 Cache caricata (IndexedDB): ${righe.length.toLocaleString('it-IT')} righe, ultima: ${dati.ultimaData}`, 'ok');
return { righe, ultimaData: dati.ultimaData };
}catch(e){
logArchivio(`❌ Errore lettura cache: ${e.message}`, 'err');
return null;
}
}
async function cancellaCache(){
try{
const db = await apriDB();
await new Promise(resolve=>{
const tx = db.transaction(DB_STORE, 'readwrite');
tx.objectStore(DB_STORE).delete('archivio');
tx.oncomplete = () => resolve();
tx.onerror = () => resolve();
});
db.close();
}catch(e){}
localStorage.removeItem(CACHE_META_KEY);
archivio = null;
document.getElementById('validationReport').style.display = 'none';
updateArchiveUI();
logArchivio('🗑️ Cache cancellata', 'warn');
showNotification('🗑️ Cache cancellata');
}
// ============================================================
// ARCHIVIO: UI, LOG, PROGRESS
// ============================================================
function logArchivio(msg, tipo=''){
const log = document.getElementById('archiveLog');
log.style.display = 'block';
const cls = tipo==='ok'?'log-ok':tipo==='warn'?'log-warn':tipo==='err'?'log-err':'';
log.innerHTML += `<div class="${cls}">[${new Date().toLocaleTimeString('it-IT')}] ${msg}</div>`;
log.scrollTop = log.scrollHeight;
}
function setProgress(pct, label){
document.getElementById('progressWrap').style.display = pct>0 ? 'block' : 'none';
document.getElementById('progressBar').style.width = pct+'%';
document.getElementById('progressLabel').textContent = label||'';
}
function updateArchiveUI(){
const esiste = cacheEsiste() && archivio;
const dot = document.getElementById('cacheStatusDot');
const info = document.getElementById('cacheInfoText');
if(esiste){
dot.className = 'status-dot status-ok';
info.textContent = `${archivio.righe.length.toLocaleString('it-IT')} righe · ultima: ${archivio.ultimaData}`;
document.getElementById('btnValida').disabled = false;
document.getElementById('btnUseLast1').disabled = false;
document.getElementById('btnUseLast3').disabled = false;
} else {
dot.className = 'status-dot status-no';
info.textContent = cacheEsiste() ? 'Cache presente (premi Carica Cache)' : 'Nessuna cache';
document.getElementById('btnLoadCache').disabled = !cacheEsiste();
document.getElementById('btnValida').disabled = !cacheEsiste();
document.getElementById('btnUseLast1').disabled = true;
document.getElementById('btnUseLast3').disabled = true;
}
}
function popolaSelectRuote(){
const options = Object.entries(RUOTE_NOME).map(([k,v])=>`<option value="${k}">${k} - ${v}</option>`).join('');
document.getElementById('archivioRuotaSelect').innerHTML = options;
document.getElementById('statsRuota').innerHTML = options;
}
// ============================================================
// PARSING DATA AGGIORNATO PER FORMATO YYYY/MM/DD
// ============================================================
function parseData(s){
if(!s) return null;
s = String(s).trim();
// Formato YYYY/MM/DD o YYYY-MM-DD
let m = s.match(/^(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})/);
if(m){ const d = new Date(+m[1], +m[2]-1, +m[3]); return isNaN(d.getTime()) ? null : d; }
// Formato DD/MM/YYYY o DD-MM-YYYY
m = s.match(/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})/);
if(m){ const d = new Date(+m[3], +m[2]-1, +m[1]); return isNaN(d.getTime()) ? null : d; }
const d = new Date(s);
return isNaN(d.getTime()) ? null : d;
}
function toISOData(d){
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}
// ============================================================
// VALIDAZIONE ESTRAZIONE AVANZATA
// ============================================================
function validaEstrazione(riga, ruoteViste, dateViste){
const errors = [];
// Verifica numeri validi
const nums = [riga.n1, riga.n2, riga.n3, riga.n4, riga.n5];
if(nums.some(n => isNaN(n) || n < 1 || n > 90)){
errors.push('Numeri fuori range 1-90');
}
// Verifica duplicati nella stessa estrazione
if(new Set(nums).size !== nums.length){
errors.push('Numeri duplicati nella stessa estrazione');
}
// Verifica duplicati tra estrazioni
const key = `${riga.data}_${riga.ruota}`;
if(dateViste.has(key)){
errors.push('Estrazione duplicata');
} else {
dateViste.add(key);
}
// Verifica ruota valida
if(!CODICE_TO_NOME[riga.ruota]){
errors.push('Codice ruota non valido');
}
return { valid: errors.length === 0, errors };
}
// ============================================================
// CARICAMENTO ARCHIVIO ZIP LOCALE - NUOVA FUNZIONE PRINCIPALE
// ============================================================
async function caricaArchivioLocale(file){
setProgress(10, 'Decomprimo ZIP...');
logArchivio(`📦 Decomprimo ${file.name}...`);
try{
const zip = await JSZip.loadAsync(file);
const storicoFile = zip.file('storico.txt');
if(!storicoFile){
throw new Error('File storico.txt non trovato nel ZIP');
}
const raw = await storicoFile.async('text');
setProgress(50, 'Analizzo estrazioni...');
logArchivio(`📄 Analisi file storico.txt...`);
const frames = [];
const ruoteViste = new Set();
const dateViste = new Set();
for(const line of raw.split('\n')){
const trimmed = line.trim();
if(!trimmed) continue;
const parti = trimmed.split('\t');
if(parti.length !== 7) continue;
const [dataStr, codiceRuota, ...numsStr] = parti;
// Mappatura codici
if(!CODICE_TO_NOME[codiceRuota]) continue;
const nums = numsStr.map(Number);
const d = parseData(dataStr);
if(!d) continue;
const riga = {
data: dataStr,
ruota: codiceRuota,
n1:nums[0], n2:nums[1], n3:nums[2], n4:nums[3], n5:nums[4]
};
const validazione = validaEstrazione(riga, ruoteViste, dateViste);
if(!validazione.valid) continue;
frames.push(riga);
}
setProgress(80, 'Ordino e salvo...');
if(!frames.length){
logArchivio('❌ Nessun dato valido trovato', 'err');
showNotification('❌ Nessun dato valido nel file');
setProgress(100, 'Completato');
return;
}
frames.sort((a,b)=>parseData(a.data)-parseData(b.data)||a.ruota.localeCompare(b.ruota));
archivio = { righe: frames, ultimaData: frames[frames.length-1].data };
const ok = await salvaCache(frames, archivio.ultimaData);
if(!ok){
logArchivio('⚠️ Dati disponibili SOLO per la sessione corrente', 'warn');
showNotification('⚠️ Salvataggio non riuscito: dati solo per questa sessione', 4000);
}
updateArchiveUI();
setupBtDateRange();
showNotification(`📥 ${archivio.righe.length.toLocaleString('it-IT')} righe in archivio`);
}catch(e){
logArchivio(`❌ Errore: ${e.message}`, 'err');
showNotification(`❌ Errore: ${e.message}`);
}
setProgress(100, 'Completato');
}
// ============================================================
// ARCHIVIO: VALIDAZIONE
// ============================================================
function validaDati(){
if(!archivio||!archivio.righe.length){ showNotification('⚠️ Nessun dato'); return; }
const box = document.getElementById('validationReport');
box.style.display = 'block';
const perRuota = {};
archivio.righe.forEach(r=>{
if(!perRuota[r.ruota]) perRuota[r.ruota] = {n:0, min:r.data, max:r.data};
perRuota[r.ruota].n++;
if(parseData(r.data) < parseData(perRuota[r.ruota].min)) perRuota[r.ruota].min = r.data;
if(parseData(r.data) > parseData(perRuota[r.ruota].max)) perRuota[r.ruota].max = r.data;
});
let html = `<div style="font-weight:600;color:#4fae7a;">✅ VALIDAZIONE OK</div>`;
html += `<div style="margin-top:4px;">Righe: <b>${archivio.righe.length.toLocaleString('it-IT')}</b> · Ruote: <b>${Object.keys(perRuota).length}/11</b></div>`;
const venus = archivio.righe.filter(r=>parseData(r.data) >= parseData('2005-05-05')).length;
const quattro = archivio.righe.filter(r=>parseData(r.data) >= parseData('2023-07-07')).length;
html += `<div style="margin-top:4px;color:#c9b98f;">Epoca Venus: ${venus.toLocaleString('it-IT')} righe · 4 Estrazioni: ${quattro.toLocaleString('it-IT')} righe</div>`;
html += `<table><tr><th>Ruota</th><th>Estrazioni</th><th>Prima</th><th>Ultima</th></tr>`;
for(const [ruota,s] of Object.entries(perRuota)){
html += `<tr><td>${ruota} ${CODICE_TO_NOME[ruota]||''}</td><td>${s.n.toLocaleString('it-IT')}</td><td>${s.min}</td><td>${s.max}</td></tr>`;
}
html += `</table>`;
box.innerHTML = html;
logArchivio(`🔍 Validazione completata`, 'ok');
}
// ============================================================
// ARCHIVIO: USO SUL CERCHIO
// ============================================================
function filtraPerEpoca(righe){
const epocaKey = document.getElementById('epocaSelect').value;
const epoca = EPOCHE[epocaKey];
if(!epoca.dataInizio) return righe;
const dInizio = parseData(epoca.dataInizio);
return righe.filter(r => parseData(r.data) >= dInizio);
}
function portaEstrazioni(n){
if(!archivio) return;
const ruota = document.getElementById('archivioRuotaSelect').value;
let sub = archivio.righe.filter(r=>r.ruota===ruota);
sub = filtraPerEpoca(sub);
sub.sort((a,b)=>parseData(a.data)-parseData(b.data));
if(!sub.length){ showNotification('⚠️ Nessuna estrazione per questa ruota/epoca'); return; }
const ultime = sub.slice(-n);
drawHighlighted.clear();
ultime.forEach(r=>[r.n1,r.n2,r.n3,r.n4,r.n5].forEach(x=>drawHighlighted.add(x)));
renderDrawChips(); renderAll();
analizzaCoerenzaEstrazioneUI([...drawHighlighted]);
const dateStr = ultime.map(r=>r.data).join(', ');
showNotification(`🎯 ${CODICE_TO_NOME[ruota]}: ${ultime.length} estraz. (${dateStr})`, 3500);
logArchivio(`🎯 Caricate ${ultime.length} estrazioni di ${ruota} sul Cerchio`, 'ok');
}
// ============================================================
// STATISTICHE E PATTERN
// ============================================================
function analizzaFrequenze(ruota, ultimeN=100){
if(!archivio) return null;
const sub = archivio.righe
.filter(r => r.ruota === ruota)
.sort((a,b) => parseData(b.data) - parseData(a.data))
.slice(0, ultimeN);
if(!sub.length) return null;
const freq = new Array(91).fill(0);
const ultimeUscite = new Array(91).fill(-1);
sub.forEach((r, idx) => {
[r.n1, r.n2, r.n3, r.n4, r.n5].forEach(n => {
freq[n]++;
if(ultimeUscite[n] === -1) ultimeUscite[n] = idx;
});
});
const top10 = freq
.map((f,i) => ({n:i, f, ritardo: ultimeUscite[i]}))
.filter(x=>x.n>0)
.sort((a,b)=>b.f-a.f)
.slice(0,10);
return { freq, ultimeUscite, top10, totale: sub.length };
}
function analizzaPattern(ruota, ultimeN=200){
if(!archivio) return null;
const sub = archivio.righe
.filter(r => r.ruota === ruota)
.sort((a,b) => parseData(b.data) - parseData(a.data))
.slice(0, ultimeN);
if(!sub.length) return null;
const coppie = new Map();
const triple = new Map();
sub.forEach(r => {
const nums = [r.n1, r.n2, r.n3, r.n4, r.n5].sort((a,b)=>a-b);
for(let i=0; i<nums.length; i++){
for(let j=i+1; j<nums.length; j++){
const key = `${nums[i]}-${nums[j]}`;
coppie.set(key, (coppie.get(key)||0) + 1);
}
}
for(let i=0; i<nums.length; i++){
for(let j=i+1; j<nums.length; j++){
for(let k=j+1; k<nums.length; k++){
const key = `${nums[i]}-${nums[j]}-${nums[k]}`;
triple.set(key, (triple.get(key)||0) + 1);
}
}
}
});
return {
coppie: [...coppie.entries()]
.map(([c,f]) => ({coppia:c, freq:f}))
.sort((a,b) => b.freq - a.freq)
.slice(0,15),
triple: [...triple.entries()]
.map(([c,f]) => ({tripla:c, freq:f}))
.sort((a,b) => b.freq - a.freq)
.slice(0,10),
totale: sub.length
};
}
function mostraStatisticheTop10(){
const ruota = document.getElementById('statsRuota').value;
const analisi = analizzaFrequenze(ruota, 100);
if(!analisi){
document.getElementById('statsResults').innerHTML = '<div style="color:#8a7a5c;padding:8px;text-align:center;">Carica prima l\'archivio</div>';
return;
}
let html = `<div style="color:var(--brass-hi);font-weight:600;margin:8px 0 6px;font-size:12px;">Top 10 numeri più frequenti (${analisi.totale} estrazioni):</div>`;
html += `<div class="stats-grid">`;
analisi.top10.forEach(t => {
html += `<div class="stat-item" data-n="${t.n}">
<div class="stat-num">${t.n}</div>
<div class="stat-freq">${t.f} volte</div>
<div class="stat-freq" style="font-size:9px;opacity:.7;">${t.ritardo===0?'ultimo':'ritardo '+t.ritardo}</div>
</div>`;
});
html += `</div>`;
const box = document.getElementById('statsResults');
box.innerHTML = html;
box.querySelectorAll('.stat-item').forEach(el => {
el.addEventListener('click', () => {
const n = parseInt(el.dataset.n);
highlighted.add(n);
renderChips();
renderAll();
showNotification(`📊 ${n} evidenziato sul Cerchio`);
});
});
showNotification(`📊 Statistiche ${CODICE_TO_NOME[ruota]} caricate`);
}
function mostraStatistichePattern(){
const ruota = document.getElementById('statsRuota').value;
const analisi = analizzaPattern(ruota, 200);
if(!analisi){
document.getElementById('statsResults').innerHTML = '<div style="color:#8a7a5c;padding:8px;text-align:center;">Carica prima l\'archivio</div>';
return;
}
let html = `<div style="color:var(--brass-hi);font-weight:600;margin:8px 0 6px;font-size:12px;">Top 15 coppie più frequenti (${analisi.totale} estrazioni):</div>`;
html += `<div class="pattern-box">`;
analisi.coppie.slice(0,15).forEach(p => {
html += `<div class="pattern-item" data-pair="${p.coppia}">
<span class="pair">${p.coppia}</span>
<span class="freq">${p.freq} volte</span>
</div>`;
});
html += `</div>`;
html += `<div style="color:var(--brass-hi);font-weight:600;margin:12px 0 6px;font-size:12px;">Top 10 triple più frequenti:</div>`;
html += `<div class="pattern-box">`;
analisi.triple.slice(0,10).forEach(p => {
html += `<div class="pattern-item" data-triple="${p.tripla}">
<span class="pair">${p.tripla}</span>
<span class="freq">${p.freq} volte</span>
</div>`;
});
html += `</div>`;
const box = document.getElementById('statsResults');
box.innerHTML = html;
box.querySelectorAll('.pattern-item').forEach(el => {
el.addEventListener('click', () => {
const nums = (el.dataset.pair || el.dataset.triple).split('-').map(Number);
nums.forEach(n => highlighted.add(n));
renderChips();
renderAll();
showNotification(`📊 ${nums.length} numeri evidenziati`);
});
});
showNotification(`📊 Pattern ${CODICE_TO_NOME[ruota]} caricati`);
}
// ============================================================
// ANALISI COERENZA GEOMETRICA
// ============================================================
function analizzaCoerenzaEstrazione(numeri){
if(numeri.length < 2) return {score:0, dettagli:[]};
const dettagli = [];
let score = 0;
const figs = detectBaseFigures(numeri);
figs.forEach(f => {
score += f.type === 'Opposti' ? 3 :
f.type === 'Triangolo' ? 4 :
f.type === 'Quadrato' ? 5 : 2;
dettagli.push({tipo:'figura', testo:`✓ ${f.type}: ${f.numbers.join('-')}`});
});
for(let i=0; i<numeri.length; i++){
for(let j=i+1; j<numeri.length; j++){
const diff = Math.abs(numeri[i] - numeri[j]);
if(diff === 45){
score += 2;
dettagli.push({tipo:'distanza', testo:`✓ Opposti (Δ45): ${numeri[i]}-${numeri[j]}`});
} else if(diff === 30 || diff === 60){
score += 1.5;
dettagli.push({tipo:'distanza', testo:`✓ Trigono (Δ${diff}): ${numeri[i]}-${numeri[j]}`});
} else if(diff === 15 || diff === 75){
score += 1;
dettagli.push({tipo:'distanza', testo:`✓ Sesto (Δ${diff}): ${numeri[i]}-${numeri[j]}`});
}
}
}
numeri.forEach(n => {
const ds = String(n).split('').reduce((s,d)=>s+ +d,0);
const ridotto = ds > 9 ? ds - 9 : ds;
if(numeri.includes(ridotto) && ridotto !== n){
score += 0.5;
dettagli.push({tipo:'somma', testo:`✓ Somma cifre: ${n} → ${ridotto}`});
}
});
return {score, dettagli: dettagli.slice(0,10)};
}
function analizzaCoerenzaEstrazioneUI(numeri){
const analisi = analizzaCoerenzaEstrazione(numeri);
const box = document.getElementById('coerenzaBox');
if(!analisi.dettagli.length){
box.innerHTML = '';
return;
}
let html = `<div class="coerenza-box">`;
html += `<div style="color:var(--brass-hi);font-weight:600;margin-bottom:6px;">Coerenza geometrica: <span style="color:#4fae7a;">${analisi.score.toFixed(1)}</span></div>`;
analisi.dettagli.forEach(d => {
html += `<div class="coerenza-item">${d.testo}</div>`;
});
html += `</div>`;
box.innerHTML = html;
}
// ============================================================
// VERIFICA A RITROSO (BACKTEST)
// ============================================================
function popolaBtRuote(){
document.getElementById('btRuotaSelect').innerHTML =
Object.entries(RUOTE_NOME).map(([k,v])=>`<option value="${k}">${k} - ${v}</option>`).join('');
}
function setupBtDateRange(){
const inp = document.getElementById('btDateInput');
if(archivio && archivio.righe.length){
const dates = archivio.righe.map(r=>parseData(r.data)).filter(Boolean).sort((a,b)=>a-b);
inp.min = toISOData(dates[0]);
inp.max = toISOData(dates[dates.length-1]);
if(!inp.value) inp.value = toISOData(dates[dates.length-1]);
} else {
if(!inp.value) inp.value = toISOData(new Date());
}
}
function computeBacktestCandidates(baseEstrazioni){
const n = baseEstrazioni.length;
if(!n) return [];
const lastSeen = {};
const freq = new Array(91).fill(0);
baseEstrazioni.forEach((r, idx)=>{
[r.n1,r.n2,r.n3,r.n4,r.n5].forEach(x=>{
lastSeen[x]=idx;
freq[x]++;
});
});
const lastDraw = baseEstrazioni[n-1];
const lastNums = [lastDraw.n1,lastDraw.n2,lastDraw.n3,lastDraw.n4,lastDraw.n5];
const scores = [];
for(let num=1; num<=90; num++){
const delay = (lastSeen[num]!==undefined) ? (n-1-lastSeen[num]) : n;
const frequenza = freq[num] / n;
let bonus = 0;
const motivi = [];
lastNums.forEach(ln=>{
if(ln===num) return;
if(rot(ln,45)===num){ bonus+=15; motivi.push(`opposto ${ln}`); }
else if(rot(ln,30)===num){ bonus+=8; motivi.push(`trigono ${ln}`); }
else if(rot(ln,60)===num){ bonus+=8; motivi.push(`trigono ${ln}`); }
else if(rot(ln,15)===num){ bonus+=5; motivi.push(`sesto ${ln}`); }
});
if(frequenza > 0.15){
bonus += 5;
motivi.push('frequente');
}
const scoreFinale = delay + bonus + (frequenza * 10);
scores.push({ n:num, delay, score:scoreFinale, frequenza, motivi });
}
scores.sort((a,b)=> b.score-a.score || b.delay-a.delay);
return scores;
}
function runBacktest(){
if(!archivio || !archivio.righe.length){
showNotification('⚠️ Carica prima l\'archivio (Carica ZIP Locale)');
return;
}
const ruota = document.getElementById('btRuotaSelect').value;
const dateStr = document.getElementById('btDateInput').value;
if(!dateStr){ showNotification('⚠️ Seleziona una data dal calendario'); return; }
const dSel = parseData(dateStr);
const nBase = parseInt(document.getElementById('btNumBase').value)||60;
const topN = parseInt(document.getElementById('btTopN').value)||10;
const sub = archivio.righe.filter(r=>r.ruota===ruota).sort((a,b)=>parseData(a.data)-parseData(b.data));
if(!sub.length){ showNotification('⚠️ Nessuna estrazione per questa ruota'); return; }
const upTo = sub.filter(r=>parseData(r.data) <= dSel);
if(upTo.length < 5){ showNotification('⚠️ Troppo poche estrazioni fino a questa data'); return; }
const base = upTo.slice(-nBase);
const exact = sub.find(r => parseData(r.data).getTime() === dSel.getTime());
const precedente = upTo[upTo.length-1];
const after = sub.filter(r=>parseData(r.data) > dSel);
const nextDraw = after.length ? after[0] : null;
const candidates = computeBacktestCandidates(base);
const topCand = candidates.slice(0, topN);
LAST_BACKTEST = topCand;
renderBacktestResult({
ruota, dSel, base, exact, precedente, nextDraw, topCand,
precISO: precedente ? toISOData(parseData(precedente.data)) : '',
nextISO: nextDraw ? toISOData(parseData(nextDraw.data)) : ''
});
renderNumeriPromettenti();
}
function renderBacktestResult(d){
const box = document.getElementById('btResults');
const nome = CODICE_TO_NOME[d.ruota];
const actualNums = d.nextDraw ? [d.nextDraw.n1,d.nextDraw.n2,d.nextDraw.n3,d.nextDraw.n4,d.nextDraw.n5] : [];
const topNums = d.topCand.map(c=>c.n);
const hits = actualNums.filter(n=>topNums.includes(n));
let html = `<div class="bt-result">`;
html += `<div style="color:var(--brass-hi);font-weight:600;margin-bottom:4px;">📊 ${nome} (${d.ruota}) — verifica al ${d.dSel.toLocaleDateString('it-IT')}</div>`;
html += `<div style="color:#c9b98f;">Base: ${d.base.length} estrazioni (${d.base[0].data} → ${d.base[d.base.length-1].data})</div>`;
if(d.exact){
html += `<div class="bt-label">Estrazione esatta del giorno ${d.exact.data}:</div><div class="bt-row">`;
[d.exact.n1,d.exact.n2,d.exact.n3,d.exact.n4,d.exact.n5].forEach(n=>{ html += `<span class="bt-num actual">${n}</span>`; });
html += `</div>`;
} else {
html += `<div style="margin-top:8px;padding:8px;background:rgba(224,176,74,.15);border-radius:6px;color:#e0b04a;font-size:11.5px;line-height:1.6;">
⚠️ <b>Nessuna estrazione in data ${d.dSel.toLocaleDateString('it-IT')}</b> per ${nome}.<br>
Ultima precedente usata come base: <b>${d.precedente.data}</b><br>
<span class="bt-jump" data-data="${d.precISO}">◀ vai alla data precedente</span> ·
${d.nextISO?`<span class="bt-jump" data-data="${d.nextISO}">vai alla successiva ▶</span>`:''}
</div>`;
}
html += `<div class="bt-label">Top ${d.topCand.length} numeri previsti (ritardatari + figure + frequenza):</div><div class="bt-row">`;
d.topCand.forEach(c=>{
const isHit = actualNums.includes(c.n);
const title = `ritardo ${c.delay} · freq ${(c.frequenza*100).toFixed(1)}%${c.motivi.length?` · ${c.motivi.join(', ')}`:''}`;
html += `<span class="bt-num ${isHit?'hit':''}" title="${title}">${c.n}</span>`;
});
html += `</div>`;
if(d.nextDraw){
html += `<div class="bt-label">Estrazione reale successiva (${d.nextDraw.data}):</div><div class="bt-row">`;
actualNums.forEach(n=>{
const isHit = topNums.includes(n);
html += `<span class="bt-num actual" style="${isHit?'background:var(--gold-fill);color:#1a1206;border:2px solid #4fae7a;':''}">${n}</span>`;
});
html += `</div>`;
const pct = ((hits.length/5)*100).toFixed(0);
html += `<div style="margin-top:8px;font-weight:600;color:${hits.length?'#4fae7a':'#e0a0a0'};">`;
html += `🎯 Match: ${hits.length}/5 numeri previsti (${pct}%)${hits.length?` → ${hits.join(', ')}`:''}</div>`;
} else {
html += `<div style="margin-top:8px;color:#e0b04a;">ℹ️ Nessuna estrazione successiva disponibile (data troppo recente)</div>`;
}
html += `<div class="btn-row" style="margin-top:10px;">`;
html += `<button class="step" id="btBringCand">🎯 Porta previsti sul Cerchio</button>`;
if(d.nextDraw) html += `<button class="step" id="btBringActual">📗 Porta estrazione reale</button>`;
html += `</div></div>`;
box.innerHTML = html;
box.querySelectorAll('.bt-jump').forEach(s=>{
s.addEventListener('click',()=>{ document.getElementById('btDateInput').value = s.dataset.data; runBacktest(); });
});
document.getElementById('btBringCand').addEventListener('click', ()=>{
drawHighlighted.clear(); topNums.forEach(n=>drawHighlighted.add(n));
renderDrawChips(); renderAll(); showNotification(`🎯 ${topNums.length} numeri previsti sul Cerchio`);
});
if(d.nextDraw){
document.getElementById('btBringActual').addEventListener('click', ()=>{
drawHighlighted.clear(); actualNums.forEach(n=>drawHighlighted.add(n));
renderDrawChips(); renderAll(); showNotification('📗 Estrazione reale sul Cerchio');
});
}
}
// ============================================================
// NUMERI PROMETTENTI (SCORING AVANZATO)
// ============================================================
function calcolaNumeriPromettenti(){
const mappa = new Map();
const addFonte = (n, score, fonte, peso=1)=>{
if(!mappa.has(n)) mappa.set(n, {n, score:0, fonti:new Map(), dettagli:{}});
const e = mappa.get(n);
e.score += score * peso;
const count = e.fonti.get(fonte) || 0;
e.fonti.set(fonte, count + 1);
};
(ENGINE.candidates||[]).slice(0,15).forEach(c=>{
addFonte(c.n, Math.min(100, c.score), 'motore', 1.0);
});
(CONVERGENCE||[]).slice(0,15).forEach(c=>{
const peso = Math.min(1.5, 1 + (c.drawCount - 2) * 0.1);
addFonte(c.n, Math.min(100, c.score), 'conv', peso);
});
(LAST_BACKTEST||[]).slice(0,15).forEach((c,i)=>{
const peso = 1.2 - (i * 0.05);
addFonte(c.n, Math.min(100, c.score), 'backtest', Math.max(0.5, peso));
});
return [...mappa.values()]
.map(e => ({
...e,
scoreFinale: e.score * (1 + (e.fonti.size - 1) * 0.25)
}))
.sort((a,b) => b.scoreFinale - a.scoreFinale || b.fonti.size - a.fonti.size || a.n - b.n)
.slice(0,12);
}
function renderNumeriPromettenti(){
const box = document.getElementById('esitiContent');
const risultati = calcolaNumeriPromettenti();
if(!risultati.length){
box.innerHTML = `<div class="esiti-vuoto">Esegui almeno un'analisi (⚡ Motore, 🔀 Convergenze o 🕰️ Verifica a Ritroso)<br>poi premi <b>↻ Aggiorna</b>.</div>`;
return;
}
const maxScore = risultati[0].scoreFinale || 1;
box.innerHTML = `
<div class="esiti-grid">
${risultati.map((r,i)=>`
<div class="esito-item">
<div class="esito-num ${i===0?'top1':''}" data-n="${r.n}"
title="Score aggregato ${r.scoreFinale.toFixed(1)} — Fonti: ${[...r.fonti.keys()].join(', ')}">
<span class="n">${r.n}</span>
<span class="s">${(r.scoreFinale/maxScore*100).toFixed(0)}</span>
</div>
<div class="esito-fonti">
${r.fonti.has('motore') ?'<span class="fonte-dot fd-motore" title="Motore Previsivo"></span>':''}
${r.fonti.has('conv') ?'<span class="fonte-dot fd-conv" title="Convergenze"></span>':''}
${r.fonti.has('backtest')?'<span class="fonte-dot fd-backtest" title="Verifica a Ritroso"></span>':''}
</div>
</div>`).join('')}
</div>
<div class="esiti-legenda">
<span><span class="fonte-dot fd-motore"></span>Motore</span>
<span><span class="fonte-dot fd-conv"></span>Convergenze</span>
<span><span class="fonte-dot fd-backtest"></span>Backtest</span>
</div>`;
box.querySelectorAll('.esito-num').forEach(el=>{
el.addEventListener('click',()=>{
const n = parseInt(el.dataset.n);
highlighted.add(n); renderChips(); renderAll();
showNotification(`🎯 ${n} evidenziato sul Cerchio`);
});
});
}
// ============================================================
// CERCHIO DI SPILIMBERGO: BASE
// ============================================================
const CX = 350, CY = 350, SECTORS = 32, SECTOR_ANGLE = 360/SECTORS, HALF = SECTOR_ANGLE/2;
const REST_OFFSET = ((-HALF)%360+360)%360;
const RINGS = [
{ key:'r0', numbers:[1,24,47,70,3,26,49,72,5,28,51,74,7,30,53,76,9,32,55,78,11,34,57,80,13,36,59,82,15,38], rIn:76, rOut:150, fs:16 },
{ key:'r1', numbers:[61,84,17,40,63,86,19,42,65,88,21,44,67,90,23,46,69,2,25,48,71,4,27,50,73,6,29,52,75,8], rIn:158, rOut:222, fs:18 },
{ key:'r2', numbers:[31,54,77,10,33,56,79,12,35,58,81,14,37,60,83,16,39,62,85,18,41,64,87,20,43,66,89,22,45,68], rIn:230, rOut:284, fs:20 }
];
const offset = [REST_OFFSET, REST_OFFSET, REST_OFFSET];
const highlighted = new Set(), drawHighlighted = new Set();
const svg = document.getElementById('wheel');
const NS = 'http://www.w3.org/2000/svg';
const ENGINE = { figures:[], candidates:[], base:[] };
let overlayG = null, heatOn = false, heatMap = new Map(), tourTimer = null;
const HISTORY = [];
let CONVERGENCE = [];
function rot(n, step){ return ((n-1+step)%90)+1; }
function showNotification(msg, dur=2500){
const n = document.getElementById('notification');
n.textContent = msg; n.classList.add('show');
setTimeout(()=>n.classList.remove('show'), dur);
}
function el(tag, attrs){
const e = document.createElementNS(NS, tag);
for(const k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
function normalizeDeg(d){ d = d%360; if(d<0) d+=360; return d; }
function polar(cx, cy, r, ang){
const a = (ang-90)*Math.PI/180;
return [cx+r*Math.cos(a), cy+r*Math.sin(a)];
}
function annulusPath(cx,cy,rin,rout){
return `M ${cx-rout} ${cy} A ${rout} ${rout} 0 1 0 ${cx+rout} ${cy} A ${rout} ${rout} 0 1 0 ${cx-rout} ${cy} Z M ${cx-rin} ${cy} A ${rin} ${rin} 0 1 1 ${cx+rin} ${cy} A ${rin} ${rin} 0 1 1 ${cx-rin} ${cy} Z`;
}
// ============================================================
// CERCHIO: COSTRUZIONE E RENDER
// ============================================================
function buildStatic(){
const grad = el('radialGradient',{id:'paperGrad',cx:'45%',cy:'40%',r:'70%'});
grad.appendChild(el('stop',{offset:'0%','stop-color':'#f8f1de'}));
grad.appendChild(el('stop',{offset:'75%','stop-color':'#efe3c4'}));
grad.appendChild(el('stop',{offset:'100%','stop-color':'#ddceac'}));
const defs = el('defs',{}); defs.appendChild(grad); svg.appendChild(defs);
svg.appendChild(el('circle',{cx:CX,cy:CY,r:332,fill:'url(#paperGrad)',stroke:'var(--brass)','stroke-width':3}));
svg.appendChild(el('circle',{cx:CX,cy:CY,r:326,fill:'none',stroke:'var(--brass)','stroke-width':1,opacity:.6}));
const dSize = 46;
svg.appendChild(el('path',{d:`M ${CX} ${CY-dSize} L ${CX+dSize} ${CY} L ${CX} ${CY+dSize} L ${CX-dSize} ${CY} Z`,fill:'#00000007',stroke:'var(--brass)','stroke-width':1.3,opacity:.75}));
svg.appendChild(el('circle',{cx:CX,cy:CY,r:3,fill:'var(--ink)'}));
}
const COMPASS_DEFS = [['N',HALF],['E',90+HALF],['S',180+HALF],['O',270+HALF]];
function buildCompass(ring2){
const compassTexts = [];
COMPASS_DEFS.forEach(([label,ang])=>{
const [x1,y1] = polar(CX,CY,ring2.ring.rOut+12,ang);
const [x2,y2] = polar(CX,CY,ring2.ring.rOut+26,ang);
ring2.g.appendChild(el('line',{x1,y1,x2,y2,stroke:'var(--ink)','stroke-width':2.5}));
const t = el('text',{class:'compass-label'}); t.textContent = label;
ring2.textG.appendChild(t);
compassTexts.push({el:t, ang});
});
ring2.compassTexts = compassTexts;
}
const ringGroups = [];
function buildRing(ring, idx){
const g = el('g',{id:'lines-'+idx});
g.appendChild(el('circle',{cx:CX,cy:CY,r:ring.rIn, class:'band'}));
g.appendChild(el('circle',{cx:CX,cy:CY,r:ring.rOut,class:'band'}));
for(let s=0;s<SECTORS;s++){
const ang = s*SECTOR_ANGLE;
const [x1,y1] = polar(CX,CY,ring.rIn,ang);
const [x2,y2] = polar(CX,CY,ring.rOut,ang);
g.appendChild(el('line',{x1,y1,x2,y2,class:'divider'}));
}
for(let s=30;s<32;s++){
const a1=s*SECTOR_ANGLE, a2=(s+1)*SECTOR_ANGLE;
const [ox1,oy1]=polar(CX,CY,ring.rOut,a1),[ox2,oy2]=polar(CX,CY,ring.rOut,a2);
const [ix2,iy2]=polar(CX,CY,ring.rIn,a2),[ix1,iy1]=polar(CX,CY,ring.rIn,a1);
g.appendChild(el('path',{d:`M ${ox1} ${oy1} A ${ring.rOut} ${ring.rOut} 0 0 1 ${ox2} ${oy2} L ${ix2} ${iy2} A ${ring.rIn} ${ring.rIn} 0 0 0 ${ix1} ${iy1} Z`,class:'gap-mark'}));
}
svg.appendChild(g);
const textG = el('g',{id:'text-'+idx});
const dots=[], drawDots=[];
const texts = ring.numbers.map(n=>{
const ddot = el('circle',{r:18,class:'draw-dot'}); textG.appendChild(ddot); drawDots.push(ddot);
const dot = el('circle',{r:13,class:'highlight-dot'}); textG.appendChild(dot); dots.push(dot);
const t = el('text',{class:'num','font-size':ring.fs,'data-number':n,'data-ring':idx});
t.textContent = n; textG.appendChild(t);
return t;
});
svg.appendChild(textG);
const hit = el('path',{class:'ring-track','data-ring':idx,fill:'#fff',opacity:0});
hit.setAttribute('d',annulusPath(CX,CY,ring.rIn-4,ring.rOut+4));
hit.style.pointerEvents='fill'; svg.appendChild(hit);
ringGroups.push({g,textG,texts,dots,drawDots,hit,ring,idx});
}
function renderRing(idx){
const rg = ringGroups[idx];
rg.g.setAttribute('transform',`rotate(${offset[idx]} ${CX} ${CY})`);
const rMid = (rg.ring.rIn+rg.ring.rOut)/2;
rg.texts.forEach((t,i)=>{
const ang = (i+0.5)*SECTOR_ANGLE + offset[idx];
const [x,y] = polar(CX,CY,rMid,ang);
t.setAttribute('x',x); t.setAttribute('y',y);
const num = rg.ring.numbers[i];
t.classList.toggle('highlighted', highlighted.has(num));
t.classList.toggle('draw-hit', drawHighlighted.has(num));
const dot = rg.dots[i]; dot.setAttribute('cx',x); dot.setAttribute('cy',y);
dot.style.opacity = highlighted.has(num)?1:0;
const ddot = rg.drawDots[i]; ddot.setAttribute('cx',x); ddot.setAttribute('cy',y);
ddot.style.opacity = drawHighlighted.has(num)?1:0;
});
if(rg.compassTexts) rg.compassTexts.forEach(ct=>{
const [x,y] = polar(CX,CY,rg.ring.rOut+34, ct.ang+offset[idx]);
ct.el.setAttribute('x',x); ct.el.setAttribute('y',y);
});
updateReadout(); updateDistancePanel(); updateFigureList(); drawOppositeLines(); drawFigureOverlay(); applyHeat();
}
function compassAngles(){
const n = normalizeDeg(offset[2]+HALF);
return { N:n, E:normalizeDeg(n+90), S:normalizeDeg(n+180), O:normalizeDeg(n+270) };
}
function sectorAt(ringIdx, compassAngle){
const rel = normalizeDeg(compassAngle-offset[ringIdx]);
let idx = Math.round(rel/SECTOR_ANGLE-0.5);
return ((idx%SECTORS)+SECTORS)%SECTORS;
}
function valueAt(ringIdx, compassAngle){
const idx = sectorAt(ringIdx, compassAngle);
const nums = RINGS[ringIdx].numbers;
return idx<nums.length ? nums[idx] : null;
}
function updateReadout(){
const dirs = compassAngles();
for(let r=0;r<3;r++) for(const d in dirs){
const v = valueAt(r,dirs[d]);
const cell = document.getElementById(`r${r}-${d}`);
if(v===null){ cell.textContent='—'; cell.classList.add('blank'); }
else { cell.textContent=v; cell.classList.remove('blank'); }
}
}
function snap(idx){
offset[idx] = normalizeDeg(Math.round((offset[idx]-HALF)/SECTOR_ANGLE)*SECTOR_ANGLE+HALF);
renderRing(idx);
}
function stepRing(idx,dir){
offset[idx] = normalizeDeg(offset[idx]+dir*SECTOR_ANGLE);
renderRing(idx);
}
let dragging = null;
function angleFromEvent(evt){
const pt = svg.createSVGPoint(); pt.x = evt.clientX; pt.y = evt.clientY;
const loc = pt.matrixTransform(svg.getScreenCTM().inverse());
return { ang: Math.atan2(loc.y-CY,loc.x-CX)*180/Math.PI };
}
svg.addEventListener('pointerdown',e=>{
const target = e.target;
if(!target.classList.contains('ring-track')) return;
const idx = parseInt(target.getAttribute('data-ring'));
const p = angleFromEvent(e);
dragging = { idx, startAngle:p.ang, startOffset:offset[idx], moved:false, startClientX:e.clientX, startClientY:e.clientY };
target.classList.add('dragging');
svg.setPointerCapture(e.pointerId);
e.preventDefault();
});
svg.addEventListener('pointermove',e=>{
if(!dragging) return;
if(Math.hypot(e.clientX-dragging.startClientX,e.clientY-dragging.startClientY)>4) dragging.moved = true;
const p = angleFromEvent(e);
offset[dragging.idx] = dragging.startOffset+(p.ang-dragging.startAngle);
renderRing(dragging.idx);
});
function endDrag(e){
if(!dragging) return;
const idx = dragging.idx, wasDrag = dragging.moved;
document.querySelectorAll('.ring-track').forEach(t=>t.classList.remove('dragging'));
if(wasDrag) snap(idx);
else {
const p = angleFromEvent(e);
const sIdx = sectorAt(idx, normalizeDeg(p.ang+90));
const nums = RINGS[idx].numbers;
if(sIdx<nums.length) toggleHighlight(nums[sIdx]);
renderRing(idx);
}
dragging = null;
}
svg.addEventListener('pointerup',endDrag);
svg.addEventListener('pointercancel',endDrag);
// ============================================================
// DISTANZE E FIGURE
// ============================================================
function calculateDistances(){
if(highlighted.size<2) return [];
const nums = [...highlighted].sort((a,b)=>a-b);
const res = [];
for(let i=0;i<nums.length;i++) for(let j=i+1;j<nums.length;j++){
const diff = Math.abs(nums[i]-nums[j]);
res.push({pair:`${nums[i]}-${nums[j]}`, dist:diff, comp:90-diff});
}
return res;
}
function updateDistancePanel(){
const list = document.getElementById('distanceList');
const d = calculateDistances();
if(!d.length){ list.innerHTML = '<div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Evidenzia almeno 2 numeri</div>'; return; }
list.innerHTML = d.map(x=>`<div class="distance-item"><span style="color:var(--brass-hi);font-weight:600;">${x.pair}</span><span style="color:#c9b98f;">Δ ${x.dist}</span><span style="color:#b3a37e;font-style:italic;">C ${x.comp}</span></div>`).join('');
}
function detectGeometricFigures(){
const figs = [], S = new Set([...highlighted]);
[...highlighted].forEach(n=>{
const o = rot(n,45);
if(S.has(o)&&n<o) figs.push({type:'Opposti 180°',numbers:[n,o],color:'#c0392b'});
const t1=rot(n,30),t2=rot(n,60);
if(S.has(t1)&&S.has(t2)&&n<t1&&t1<t2) figs.push({type:'Triangolo 120°',numbers:[n,t1,t2],color:'#2e8b57'});
const e1=rot(n,15),e2=rot(n,30);
if(S.has(e1)&&S.has(e2)&&n<e1&&e1<e2) figs.push({type:'Terzina 60°',numbers:[n,e1,e2],color:'#d98e2b'});
const q1=rot(n,22),q2=rot(n,45),q3=rot(n,67);
if(S.has(q1)&&S.has(q2)&&S.has(q3)&&n<q1&&q1<q2)
figs.push({type:'Quadrato 90°',numbers:[n,q1,q2,q3],color:'#2980b9'});
});
return figs;
}
function updateFigureList(){
document.getElementById('figureList').innerHTML = detectGeometricFigures().map(f=>`<div class="figure-item"><span class="type">${f.type}</span><span>${f.numbers.join(' - ')}</span></div>`).join('');
}
function findNumberPosition(num){
for(let idx=0;idx<RINGS.length;idx++){
const ring = RINGS[idx];
const pos = ring.numbers.indexOf(num);
if(pos!==-1){
const rMid = (ring.rIn+ring.rOut)/2;
const ang = (pos+0.5)*SECTOR_ANGLE+offset[idx];
const [x,y] = polar(CX,CY,rMid,ang);
return {x,y};
}
}
return null;
}
function drawOppositeLines(){
document.querySelectorAll('.opposite-line').forEach(l=>l.remove());
const nums = [...highlighted];
nums.forEach(n=>{
const o = rot(n,45);
if(nums.includes(o)&&n<o){
const p1=findNumberPosition(n),p2=findNumberPosition(o);
if(p1&&p2) svg.insertBefore(el('line',{x1:p1.x,y1:p1.y,x2:p2.x,y2:p2.y,class:'opposite-line'}),svg.querySelector('#lines-0'));
}
});
}
// ============================================================
// FIGURE GEOMETRICHE REALI
// ============================================================
function angleOfPoint(x, y){
return (Math.atan2(y - CY, x - CX) * 180 / Math.PI + 90 + 360) % 360;
}
function midPoint(pts){
const mx = pts.reduce((s,p)=>s+p.x,0)/pts.length;
const my = pts.reduce((s,p)=>s+p.y,0)/pts.length;
return {x:mx, y:my};
}
function arcBetween(p1, p2, r){
const a1 = angleOfPoint(p1.x, p1.y);
const a2 = angleOfPoint(p2.x, p2.y);
let start = a1, end = a2;
let diff = (end - start + 360) % 360;
let large = diff > 180 ? 1 : 0;
let sweep = 1;
if(diff > 180){
[start, end] = [end, start];
diff = (end - start + 360) % 360;
large = diff > 180 ? 1 : 0;
}
const s = polar(CX, CY, r, start);
const e = polar(CX, CY, r, end);
return `M ${s[0]} ${s[1]} A ${r} ${r} 0 ${large} ${sweep} ${e[0]} ${e[1]}`;
}
function polygonPath(pts){
return pts.map((p,i)=>`${i?'L':'M'} ${p.x} ${p.y}`).join(' ')+' Z';
}
function addLabel(cx, cy, text, color){
const pad = 5;
const w = text.length * 6.5 + pad*2;
const bg = el('rect',{x:cx-w/2, y:cy-9, width:w, height:18, rx:9,
fill:'rgba(0,0,0,.6)', stroke:color, 'stroke-width':1});
const lab = el('text',{x:cx, y:cy+0.5, class:'fig-label', fill:'#fff'});
lab.textContent = text;
overlayG.appendChild(bg);
overlayG.appendChild(lab);
}
function drawVertex(p, color, r=6){
overlayG.appendChild(el('circle',{cx:p.x, cy:p.y, r:r, class:'fig-vertex',
fill:color, stroke:'#fff'}));
overlayG.appendChild(el('circle',{cx:p.x, cy:p.y, r:r+4, class:'fig-vertex-ring',
stroke:color}));
}
function drawRays(pts, color){
pts.forEach(p=>{
overlayG.appendChild(el('line',{x1:CX, y1:CY, x2:p.x, y2:p.y,
class:'fig-ray', stroke:color}));
});
}
function drawFigOpposti(pts, color, label){
const [p1, p2] = pts;
overlayG.appendChild(el('line',{x1:p1.x, y1:p1.y, x2:p2.x, y2:p2.y,
class:'fig-diameter fig-pulse', stroke:color}));
const rOut = 295;
overlayG.appendChild(el('path',{d:arcBetween(p1, p2, rOut),
class:'fig-arc fig-pulse', stroke:color}));
overlayG.appendChild(el('path',{d:arcBetween(p2, p1, rOut),
class:'fig-arc-inner', stroke:color}));
const cr = 8;
overlayG.appendChild(el('circle',{cx:CX, cy:CY, r:cr, class:'fig-center',
fill:'none', stroke:color, 'stroke-width':2}));
overlayG.appendChild(el('line',{x1:CX-cr, y1:CY, x2:CX+cr, y2:CY,
class:'fig-crosshair', stroke:color}));
overlayG.appendChild(el('line',{x1:CX, y1:CY-cr, x2:CX, y2:CY+cr,
class:'fig-crosshair', stroke:color}));
drawVertex(p1, color); drawVertex(p2, color);
const mid = midPoint(pts);
const dx = mid.x - CX, dy = mid.y - CY;
const dist = Math.hypot(dx,dy) || 1;
const lx = CX + dx/dist * 60, ly = CY + dy/dist * 60;
addLabel(lx, ly, label, color);
const ang = el('text',{x:CX, y:CY+28, class:'fig-angle-label', fill:color});
ang.textContent = '180°';
overlayG.appendChild(ang);
}
function drawFigTriangolo(pts, color, label){
overlayG.appendChild(el('path',{d:polygonPath(pts), class:'fig-shape fig-pulse',
fill:color, stroke:color}));
drawRays(pts, color);
const rOut = 295;
overlayG.appendChild(el('path',{d:arcBetween(pts[0], pts[1], rOut),
class:'fig-arc', stroke:color}));
overlayG.appendChild(el('path',{d:arcBetween(pts[1], pts[2], rOut),
class:'fig-arc', stroke:color}));
overlayG.appendChild(el('path',{d:arcBetween(pts[2], pts[0], rOut),
class:'fig-arc', stroke:color}));
pts.forEach(p=>drawVertex(p, color));
const c = midPoint(pts);
overlayG.appendChild(el('circle',{cx:c.x, cy:c.y, r:4, class:'fig-center',
fill:color, stroke:'#fff', 'stroke-width':1.5}));
addLabel(c.x, c.y - 18, label, color);
}
function drawFigTerzina(pts, color, label){
const rOut = 295;
overlayG.appendChild(el('path',{d:arcBetween(pts[0], pts[2], rOut),
class:'fig-arc fig-pulse', stroke:color}));
overlayG.appendChild(el('path',{d:polygonPath(pts), class:'fig-shape',
fill:color, stroke:color}));
drawRays(pts, color);
const rIn = 70;
overlayG.appendChild(el('path',{d:arcBetween(pts[0], pts[2], rIn),
class:'fig-arc-inner', stroke:color}));
pts.forEach(p=>drawVertex(p, color, 5));
const c = midPoint(pts);
overlayG.appendChild(el('circle',{cx:c.x, cy:c.y, r:3.5, class:'fig-center',
fill:color, stroke:'#fff', 'stroke-width':1.5}));
addLabel(c.x, c.y - 16, label, color);
}
function drawFigQuadrato(pts, color, label){
overlayG.appendChild(el('path',{d:polygonPath(pts), class:'fig-shape fig-pulse',
fill:color, stroke:color}));
drawRays(pts, color);
overlayG.appendChild(el('line',{x1:pts[0].x, y1:pts[0].y, x2:pts[2].x, y2:pts[2].y,
class:'fig-ray', stroke:color}));
overlayG.appendChild(el('line',{x1:pts[1].x, y1:pts[1].y, x2:pts[3].x, y2:pts[3].y,
class:'fig-ray', stroke:color}));
const rOut = 295;
for(let i=0;i<4;i++){
overlayG.appendChild(el('path',{
d:arcBetween(pts[i], pts[(i+1)%4], rOut),
class:'fig-arc', stroke:color}));
}
pts.forEach(p=>drawVertex(p, color));
const c = midPoint(pts);
overlayG.appendChild(el('circle',{cx:c.x, cy:c.y, r:4, class:'fig-center',
fill:color, stroke:'#fff', 'stroke-width':1.5}));
addLabel(c.x, c.y - 18, label, color);
}
function drawFigDefault(pts, color, label){
overlayG.appendChild(el('path',{d:polygonPath(pts), class:'fig-shape',
fill:color, stroke:color}));
drawRays(pts, color);
pts.forEach(p=>drawVertex(p, color, 5));
const c = midPoint(pts);
addLabel(c.x, c.y - 16, label, color);
}
function drawFigureOverlay(){
if(!overlayG) return;
overlayG.innerHTML = '';
ENGINE.figures.forEach(f=>{
const pts = f.numbers.map(findNumberPosition);
if(pts.some(p=>!p)) return;
if(pts.length < 2) return;
switch(f.type){
case 'Opposti': drawFigOpposti(pts, f.color, 'Opposti 180°'); break;
case 'Triangolo': drawFigTriangolo(pts, f.color, 'Triangolo 120°'); break;
case 'Terzina 60°': drawFigTerzina(pts, f.color, 'Terzina 60°'); break;
case 'Quadrato': drawFigQuadrato(pts, f.color, 'Quadrato 90°'); break;
default: drawFigDefault(pts, f.color, f.type);
}
});
}
// ============================================================
// MOTORE PREVISIVO L1
// ============================================================
function getBase(){ return drawHighlighted.size>=2 ? [...drawHighlighted] : [...highlighted]; }
function detectBaseFigures(nums){
const figs = [], S = new Set(nums);
nums.forEach(n=>{
const o = rot(n,45);
if(S.has(o)&&n<o) figs.push({type:'Opposti',numbers:[n,o],color:'#c0392b'});
const t1=rot(n,30),t2=rot(n,60);
if(S.has(t1)&&S.has(t2)&&n<t1&&t1<t2) figs.push({type:'Triangolo',numbers:[n,t1,t2],color:'#2e8b57'});
const e1=rot(n,15),e2=rot(n,30);
if(S.has(e1)&&S.has(e2)&&n<e1&&e1<e2) figs.push({type:'Terzina 60°',numbers:[n,e1,e2],color:'#d98e2b'});
const q1=rot(n,22),q2=rot(n,45),q3=rot(n,67);
if(S.has(q1)&&S.has(q2)&&S.has(q3)&&n<q1&&q1<q2)
figs.push({type:'Quadrato',numbers:[n,q1,q2,q3],color:'#2980b9'});
});
return figs;
}
function computeCandidates(base){
const scores = new Map();
const add = (n,peso,motivo)=>{
if(base.includes(n)) return;
if(!scores.has(n)) scores.set(n,{n,score:0,motivi:[]});
const c = scores.get(n);
c.score += peso;
if(!c.motivi.includes(motivo)) c.motivi.push(motivo);
};
base.forEach(n=>{
add(rot(n,45),3.0,`opposto di ${n}`);
add(rot(n,30),2.0,`trigono di ${n}`);
add(rot(n,60),2.0,`trigono di ${n}`);
add(rot(n,15),1.5,`sesto di ${n}`);
const ds = String(n).split('').reduce((s,d)=>s+ +d,0);
const ridotto = ds > 9 ? ds - 9 : ds;
add(ridotto,0.8,`somma cifre di ${n}`);
});
for(let i=0;i<base.length;i++) for(let j=i+1;j<base.length;j++){
const a=base[i],b=base[j];
const dc = Math.min(Math.abs(a-b),90-Math.abs(a-b));
add(dc,1.2,`distanza di ${a}-${b}`);
const s = a+b;
add(s>90?s-90:s,1.2,`somma di ${a}+${b}`);
if((a+b)%2===0){
const m=(a+b)/2;
add(m,1.5,`equidistante di ${a}-${b}`);
add(rot(m,45),1.5,`opposto equidistante`);
}
}
return [...scores.values()].sort((x,y)=>y.score-x.score||x.n-y.n);
}
function runEngine(){
const base = getBase();
if(base.length<2){ showNotification('⚠️ Carica prima almeno 2 numeri'); return; }
ENGINE.base = base;
ENGINE.candidates = computeCandidates(base);
const top = ENGINE.candidates.slice(0,5).map(c=>c.n);
const withTop = detectBaseFigures(base.concat(top)).filter(f=>f.numbers.some(n=>top.includes(n)));
ENGINE.figures = detectBaseFigures(base).concat(withTop);
renderEnginePanel(); drawFigureOverlay();
if(heatOn) applyHeat();
showNotification(`⚡ ${ENGINE.figures.length} figure, ${ENGINE.candidates.length} candidati`);
renderNumeriPromettenti();
}
function renderEnginePanel(){
const box = document.getElementById('engineResults');
const c = ENGINE.candidates;
if(!c.length){ box.innerHTML=''; return; }
const pick = k=>c.slice(0,k).map(x=>x.n).join(' – ');
const counts = {};
ENGINE.figures.forEach(f=>counts[f.type]=(counts[f.type]||0)+1);
const badges = Object.entries(counts).map(([t,k])=>{
const col = ENGINE.figures.find(f=>f.type===t).color;
return `<span class="fig-badge" style="border-color:${col};color:${col}">${k}× ${t}</span>`;
}).join('');
box.innerHTML = `
<div style="margin-top:10px;">${badges||'<span style="font-size:12px;color:#8a7a5c">Nessuna figura</span>'}</div>
<div class="pred-grid">
<div class="pred-box"><div class="lbl">Amata</div><div class="val">${c[0]?.n??'—'}</div></div>
<div class="pred-box"><div class="lbl">Ambo</div><div class="val" style="font-size:16px;">${pick(2)||'—'}</div></div>
<div class="pred-box"><div class="lbl">Terno</div><div class="val" style="font-size:15px;">${pick(3)||'—'}</div></div>
<div class="pred-box"><div class="lbl">Cinquina</div><div class="val" style="font-size:13px;">${pick(5)||'—'}</div></div>
</div>
<div class="cand-list">${c.slice(0,8).map(x=>`
<div class="cand-item" data-n="${x.n}">
<span class="cn">${x.n}</span>
<span class="cm">${x.motivi.slice(0,2).join(' • ')}</span>
<span class="cs">${x.score.toFixed(1)}</span>
</div>`).join('')}
</div>`;
box.querySelectorAll('.cand-item').forEach(it=>{
it.addEventListener('click',()=>{ highlighted.add(parseInt(it.dataset.n)); renderChips(); renderAll(); });
});
}
function toggleHeat(){
heatOn = !heatOn;
if(heatOn&&ENGINE.candidates.length===0) runEngine();
heatMap = new Map(ENGINE.candidates.slice(0,30).map((c,i)=>[c.n,1-i/30]));
applyHeat();
showNotification(heatOn?'🔥 Mappa calore attiva':'Mappa calore spenta');
}
function applyHeat(){
ringGroups.forEach(rg=>{
rg.texts.forEach((t,i)=>{
const n = rg.ring.numbers[i];
if(heatOn&&heatMap.has(n)){
const k = heatMap.get(n);
t.style.fill = `rgba(224,${Math.round(176-60*k)},40,${0.35+0.65*k})`;
t.style.fontSize = (rg.ring.fs+2*k)+'px';
} else { t.style.fill=''; t.style.fontSize=''; }
});
});
}
function startTour(){
if(!ENGINE.figures.length){ showNotification('⚠️ Prima premi ⚡'); return; }
stopTour();
let i = 0;
const out = document.getElementById('alignmentResult');
const step = ()=>{
const f = ENGINE.figures[i];
out.innerHTML = `<span><strong>${i+1}/${ENGINE.figures.length}:</strong> ${f.type} → ${f.numbers.join(' – ')}</span>`;
tourTimer = setTimeout(()=>{
i++;
if(i>=ENGINE.figures.length){ stopTour(); showNotification('🎬 Tour completato'); }
else step();
},2600);
};
step();
}
function stopTour(){ if(tourTimer){clearTimeout(tourTimer);tourTimer=null;} }
function clearEngine(){
stopTour();
ENGINE.figures=[];ENGINE.candidates=[];ENGINE.base=[];
heatOn=false;heatMap.clear();applyHeat();drawFigureOverlay();
document.getElementById('engineResults').innerHTML='';
showNotification('🧹 Motore ripulito');
}
// ============================================================
// STORICO E CONVERGENZE L2
// ============================================================
const RUOTE_L2 = ['Bari','Cagliari','Firenze','Genova','Milano','Napoli','Palermo','Roma','Torino','Venezia','Nazionale'];
function parseHistory(text){
const draws = [];
text.split(/\n+/).map(l=>l.trim()).filter(Boolean).forEach(line=>{
let ruota = null;
for(const r of RUOTE_L2){ if(line.toLowerCase().includes(r.toLowerCase())){ ruota=r; break; } }
const nums = [...new Set((line.match(/\d{1,2}/g)||[]).map(Number).filter(n=>n>=1&&n<=90))];
if(nums.length) draws.push({ruota,numeri:nums.slice(0,5)});
});
return draws;
}
function loadHistoryFromTextarea(){
const draws = parseHistory(document.getElementById('historyInput').value);
if(!draws.length){ showNotification('⚠️ Nessuna estrazione valida'); return; }
draws.forEach(d=>HISTORY.push(d));
document.getElementById('historyInput').value='';
renderHistList(); updateRuotaFilter();
showNotification(`📥 ${draws.length} estrazioni caricate`);
}
function renderHistList(){
const box = document.getElementById('histList');
box.innerHTML = HISTORY.map((d,i)=>`
<div class="hist-item">
<span class="ruota">${d.ruota||'—'}</span>
<span class="nums">${d.numeri.join(' ')}</span>
${d.data ? `<span style="font-size:10px;color:#8a7a5c;white-space:nowrap;">${d.data}</span>` : ''}
<span class="del" data-i="${i}">✕</span>
</div>`).join('');
box.querySelectorAll('.del').forEach(x=>{
x.addEventListener('click',()=>{ HISTORY.splice(parseInt(x.dataset.i),1); renderHistList(); updateRuotaFilter(); });
});
}
function updateRuotaFilter(){
const sel = document.getElementById('ruotaFilter');
const ruote = [...new Set(HISTORY.map(d=>d.ruota).filter(Boolean))];
sel.innerHTML = '<option value="all">Tutte le ruote</option>'+ruote.map(r=>`<option value="${r}">${r}</option>`).join('');
}
function runConvergence(){
const filter = document.getElementById('ruotaFilter').value;
const draws = filter==='all'?HISTORY:HISTORY.filter(d=>d.ruota===filter);
if(draws.length<2){ showNotification('⚠️ Servono almeno 2 estrazioni'); return; }
const conv = new Map();
draws.forEach((draw,di)=>{
computeCandidates(draw.numeri).forEach(c=>{
if(!conv.has(c.n)) conv.set(c.n,{n:c.n,drawCount:0,score:0,motivi:[],ruote:new Set()});
const e = conv.get(c.n);
e.drawCount++; e.score += c.score;
e.motivi.push(`estr.${di+1}: ${c.motivi[0]}`);
if(draw.ruota) e.ruote.add(draw.ruota);
});
});
CONVERGENCE = [...conv.values()].filter(e=>e.drawCount>=2).sort((a,b)=>b.drawCount-a.drawCount||b.score-a.score);
renderConvergencePanel(draws.length);
showNotification(`🔀 ${CONVERGENCE.length} convergenti su ${draws.length} estrazioni`);
renderNumeriPromettenti();
}
function renderConvergencePanel(totalDraws){
const box = document.getElementById('convResults');
if(!CONVERGENCE.length){ box.innerHTML='<div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Nessuna convergenza</div>'; return; }
box.innerHTML = `<div style="font-size:12px;color:#c9b98f;margin:8px 0 6px;">Numeri "forti": candidati da più estrazioni</div>`+
CONVERGENCE.slice(0,12).map(e=>`<div class="conv-item" data-n="${e.n}"><span class="cn">${e.n}</span><span class="conv-badge">in ${e.drawCount}/${totalDraws}</span><span class="cm">${[...e.ruote].join(', ')||'—'}</span><span class="cs">${e.score.toFixed(1)}</span></div>`).join('')+
`<button class="step" id="convHighlight" style="margin-top:6px;">🎯 Evidenzia i convergenti</button>`;
box.querySelectorAll('.conv-item').forEach(it=>{
it.addEventListener('click',()=>{ highlighted.add(parseInt(it.dataset.n)); renderChips(); renderAll(); });
});
document.getElementById('convHighlight').addEventListener('click',()=>{
CONVERGENCE.slice(0,8).forEach(e=>highlighted.add(e.n));
renderChips(); renderAll();
showNotification('🎯 Convergenti evidenziati');
});
}
function loadHistoryDemo(){
document.getElementById('historyInput').value = `Bari 7 23 45 68 81\nNapoli 12 34 56 72 90\nMilano 3 18 27 44 62\nBari 15 30 60 75 88\nRoma 9 27 45 54 72`;
loadHistoryFromTextarea();
}
function clearHistory(){
HISTORY.length=0;CONVERGENCE=[];
renderHistList();updateRuotaFilter();
document.getElementById('convResults').innerHTML='';
}
// ============================================================
// CARICA RUOTE DA ARCHIVIO PER CONVERGENZE
// ============================================================
function popolaConvArchivioRuote(){
document.getElementById('convArchivioRuota').innerHTML =
'<option value="all">🌐 Tutte le ruote</option>' +
Object.entries(RUOTE_NOME).map(([k,v]) => `<option value="${k}">${k} - ${v}</option>`).join('');
}
function caricaRuoteDaArchivioPerConvergenze(){
if(!archivio || !archivio.righe.length){
showNotification('⚠️ Prima scarica o carica l\'archivio');
return;
}
const codiceRuota = document.getElementById('convArchivioRuota').value;
const nEstrazioni = parseInt(document.getElementById('convArchivioN').value) || 10;
let righeFiltrate;
if(codiceRuota === 'all'){
righeFiltrate = archivio.righe;
} else {
righeFiltrate = archivio.righe.filter(r => r.ruota === codiceRuota);
}
if(!righeFiltrate.length){
showNotification('⚠️ Nessuna estrazione trovata');
return;
}
const perRuota = {};
righeFiltrate.forEach(r => {
if(!perRuota[r.ruota]) perRuota[r.ruota] = [];
perRuota[r.ruota].push(r);
});
let aggiunte = 0, ruoteCaricate = 0;
for(const [codice, righe] of Object.entries(perRuota)){
righe.sort((a,b) => parseData(a.data) - parseData(b.data));
const ultime = righe.slice(-nEstrazioni);
const nomeRuota = CODICE_TO_NOME[codice];
ultime.forEach(r => {
const esiste = HISTORY.some(h => h.ruota === nomeRuota && h.data === r.data);
if(!esiste){
HISTORY.push({ ruota: nomeRuota, numeri:[r.n1,r.n2,r.n3,r.n4,r.n5], data:r.data });
aggiunte++;
}
});
ruoteCaricate++;
}
renderHistList();
updateRuotaFilter();
showNotification(`📥 ${aggiunte} estrazioni da ${ruoteCaricate} ruote caricate nelle Convergenze`);
if(HISTORY.length >= 2){
runConvergence();
}
}
// ============================================================
// ALLINEAMENTI POTENZIATI
// ============================================================
function alignmentScoreFor(offsets, mode){
const dirs = {N:offsets[2]+HALF, E:offsets[2]+HALF+90, S:offsets[2]+HALF+180, O:offsets[2]+HALF+270};
let score = 0;
for(let r=0;r<3;r++) for(const d in dirs){
const rel = normalizeDeg(normalizeDeg(dirs[d]) - offsets[r]);
let idx = Math.round(rel/SECTOR_ANGLE - 0.5);
idx = ((idx%SECTORS)+SECTORS)%SECTORS;
const num = RINGS[r].numbers[idx];
if(mode!=='verde' && highlighted.has(num)) score += 1;
if(mode!=='oro' && drawHighlighted.has(num)) score += (mode==='entrambi' ? 0.5 : 1);
}
return score;
}
function findBestAlignments(){
const mode = document.getElementById('alignMode').value;
if(!highlighted.size && !drawHighlighted.size){
showNotification('⚠️ Evidenzia prima almeno un numero');
return;
}
document.getElementById('alignmentResult').innerHTML = '<span>⏳ Ricerca in corso...</span>';
setTimeout(()=>{
const results = [];
let minScore = -1;
for(let o0=0;o0<360;o0+=SECTOR_ANGLE)
for(let o1=0;o1<360;o1+=SECTOR_ANGLE)
for(let o2=0;o2<360;o2+=SECTOR_ANGLE){
const t=[o0+REST_OFFSET,o1+REST_OFFSET,o2+REST_OFFSET];
const s = alignmentScoreFor(t, mode);
if(results.length<5){
results.push({offsets:t, score:s});
if(results.length===5){ results.sort((a,b)=>b.score-a.score); minScore=results[4].score; }
} else if(s>minScore){
results[4]={offsets:t, score:s};
results.sort((a,b)=>b.score-a.score);
minScore=results[4].score;
}
}
results.sort((a,b)=>b.score-a.score);
renderAlignTopList(results, mode);
}, 30);
}
function renderAlignTopList(results, mode){
const out = document.getElementById('alignmentResult');
const box = document.getElementById('alignTopList');
if(!results[0] || results[0].score<=0){
out.innerHTML = '<span style="color:#e0a0a0;">Nessun allineamento possibile con i numeri attuali</span>';
box.innerHTML = '';
return;
}
out.innerHTML = `<span>Trovate <b>${results.length}</b> configurazioni — scegline una</span>`;
box.innerHTML = results.map((r,i)=>`
<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;background:rgba(0,0,0,.15);border-radius:6px;margin-top:6px;font-size:12.5px;">
<span style="color:var(--brass-hi);font-weight:700;">#${i+1}</span>
<span style="flex:1;color:#c9b98f;">Punteggio <b style="color:#4fae7a;">${r.score.toFixed(1)}</b></span>
<button class="step" data-idx="${i}" style="flex:0 0 auto;padding:6px 12px;">Applica</button>
</div>`).join('');
box.querySelectorAll('button[data-idx]').forEach(btn=>{
btn.addEventListener('click',()=>{
const i = parseInt(btn.dataset.idx);
offset[0]=results[i].offsets[0]; offset[1]=results[i].offsets[1]; offset[2]=results[i].offsets[2];
renderAll();
out.innerHTML = `<span><strong>#${i+1} applicata — Punteggio ${results[i].score.toFixed(1)}</strong></span>`;
renderAlignDetail();
showNotification(`✨ Allineamento applicato (score ${results[i].score.toFixed(1)})`);
});
});
}
function renderAlignDetail(){
const dirs = compassAngles();
const nomi = ['Interno','Medio','Esterno'];
let html = '<div style="margin-top:8px;font-size:12px;">';
html += '<div style="color:var(--brass-hi);font-weight:600;margin-bottom:4px;">Numeri sui punti cardinali:</div>';
let found = 0;
for(const d in dirs) for(let r=0;r<3;r++){
const v = valueAt(r, dirs[d]);
if(v===null) continue;
const isOro = highlighted.has(v), isVerde = drawHighlighted.has(v);
if(isOro || isVerde){
const badges = [isOro?'🟡':'', isVerde?'🟢':''].filter(Boolean).join('');
html += `<div style="padding:3px 6px;background:rgba(79,174,122,.12);border-radius:4px;margin-bottom:3px;">
<b style="color:var(--gold-fill);">${d}</b> · ${nomi[r]} · <b>${v}</b> ${badges}
</div>`;
found++;
}
}
if(!found) html += '<div style="color:#8a7a5c;">Nessun numero evidenziato sui cardinali</div>';
html += '</div>';
document.getElementById('alignDetail').innerHTML = html;
}
// ============================================================
// EVIDENZIAZIONI E CHIP
// ============================================================
function toggleHighlight(n){
if(highlighted.has(n)) highlighted.delete(n); else highlighted.add(n);
renderChips(); renderAll();
}
function renderChips(){
const box = document.getElementById('chips');
box.innerHTML = [...highlighted].sort((a,b)=>a-b).map(n=>`<span class="chip" data-n="${n}">${n} ✕</span>`).join('');
box.querySelectorAll('.chip').forEach(chip=>{
chip.addEventListener('click',()=>{ highlighted.delete(parseInt(chip.dataset.n)); renderChips(); renderAll(); });
});
}
function renderDrawChips(){
const box = document.getElementById('drawChips');
box.innerHTML = [...drawHighlighted].sort((a,b)=>a-b).map(n=>`<span class="chip draw-chip" data-n="${n}">${n} ✕</span>`).join('');
box.querySelectorAll('.chip').forEach(chip=>{
chip.addEventListener('click',()=>{ drawHighlighted.delete(parseInt(chip.dataset.n)); renderDrawChips(); renderAll(); });
});
if(drawHighlighted.size >= 2){
analizzaCoerenzaEstrazioneUI([...drawHighlighted]);
} else {
document.getElementById('coerenzaBox').innerHTML = '';
}
}
function parseDrawNumbers(text){
return [...new Set((text.match(/\d{1,2}/g)||[]).map(s=>parseInt(s)).filter(n=>n>=1&&n<=90))];
}
function renderAll(){ renderRing(0);renderRing(1);renderRing(2); }
// ============================================================
// CONFIGURAZIONI, CONDIVISIONE, TEMA
// ============================================================
function saveConfiguration(){
const nameInput = document.getElementById('configNameInput');
const name = nameInput.value.trim()||`Config ${new Date().toLocaleString('it-IT')}`;
const config = {name,offsets:[...offset],highlighted:[...highlighted],drawHighlighted:[...drawHighlighted]};
const saved = JSON.parse(localStorage.getItem('cerchioConfigs')||'[]');
saved.push(config);
localStorage.setItem('cerchioConfigs',JSON.stringify(saved));
nameInput.value='';
updateSavedList();
showNotification(`💾 "${name}" salvata`);
}
function loadConfiguration(i){
const saved = JSON.parse(localStorage.getItem('cerchioConfigs')||'[]');
const c = saved[i]; if(!c) return;
offset[0]=c.offsets[0];offset[1]=c.offsets[1];offset[2]=c.offsets[2];
highlighted.clear();c.highlighted.forEach(n=>highlighted.add(n));
drawHighlighted.clear();c.drawHighlighted.forEach(n=>drawHighlighted.add(n));
renderAll();
showNotification(`📂 Caricata: ${c.name}`);
}
function deleteConfiguration(i){
const saved = JSON.parse(localStorage.getItem('cerchioConfigs')||'[]');
saved.splice(i,1);
localStorage.setItem('cerchioConfigs',JSON.stringify(saved));
updateSavedList();
}
function updateSavedList(){
const list = document.getElementById('savedList');
const saved = JSON.parse(localStorage.getItem('cerchioConfigs')||'[]');
if(!saved.length){ list.innerHTML='<div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Nessuna configurazione</div>'; return; }
list.innerHTML = saved.map((c,i)=>`<div class="saved-item"><span class="name" onclick="loadConfiguration(${i})">${c.name}</span><span class="del" onclick="deleteConfiguration(${i})">✕</span></div>`).join('');
}
function generateShareLink(){
const params = new URLSearchParams();
params.set('o',offset.map(o=>o.toFixed(2)).join(','));
params.set('h',[...highlighted].join(','));
params.set('d',[...drawHighlighted].join(','));
navigator.clipboard.writeText(`${location.origin}${location.pathname}?${params}`).then(()=>showNotification('🔗 Link copiato')).catch(()=>prompt('Copia:',`${location.origin}${location.pathname}?${params}`));
}
function loadFromURL(){
const params = new URLSearchParams(location.search);
if(params.has('o')){
const offs = params.get('o').split(',').map(Number);
if(offs.length===3){offset[0]=offs[0];offset[1]=offs[1];offset[2]=offs[2];}
}
if(params.has('h')) params.get('h').split(',').forEach(n=>{n=parseInt(n);if(n>=1&&n<=90)highlighted.add(n);});
if(params.has('d')) params.get('d').split(',').forEach(n=>{n=parseInt(n);if(n>=1&&n<=90)drawHighlighted.add(n);});
}
function toggleTheme(){
const html = document.documentElement;
const next = html.getAttribute('data-theme')==='light'?'dark':'light';
html.setAttribute('data-theme',next);
localStorage.setItem('theme',next);
document.getElementById('themeToggle').textContent = next==='dark'?'🌙 Tema':'☀️ Tema';
}
function initTheme(){
const saved = localStorage.getItem('theme');
if(saved){ document.documentElement.setAttribute('data-theme',saved); document.getElementById('themeToggle').textContent = saved==='dark'?'🌙 Tema':'☀️ Tema'; }
}
function doFind(){
const input = document.getElementById('finderInput');
const n = parseInt(input.value);
if(!n||n<1||n>90) return;
highlighted.add(n);
renderChips(); renderAll();
input.select();
}
// ============================================================
// EVENT LISTENERS
// ============================================================
document.querySelectorAll('button.step[data-ring]').forEach(btn=>{
btn.addEventListener('click',()=>stepRing(parseInt(btn.dataset.ring),parseInt(btn.dataset.dir)));
});
document.getElementById('resetBtn').addEventListener('click',()=>{offset[0]=offset[1]=offset[2]=REST_OFFSET;renderAll();});
document.getElementById('clearHighlights').addEventListener('click',()=>{highlighted.clear();renderChips();renderAll();});
document.getElementById('finderGo').addEventListener('click',doFind);
document.getElementById('finderInput').addEventListener('keydown',e=>{if(e.key==='Enter')doFind();});
document.getElementById('drawGo').addEventListener('click',()=>{
parseDrawNumbers(document.getElementById('drawInput').value).forEach(n=>drawHighlighted.add(n));
renderDrawChips();renderAll();
});
document.getElementById('drawClear').addEventListener('click',()=>{
drawHighlighted.clear();
document.getElementById('drawInput').value='';
document.getElementById('coerenzaBox').innerHTML='';
renderDrawChips();renderAll();
});
document.getElementById('themeToggle').addEventListener('click',toggleTheme);
document.getElementById('shareBtn').addEventListener('click',generateShareLink);
document.getElementById('guideBtn').addEventListener('click',()=>document.getElementById('guideModal').classList.add('open'));
document.getElementById('guideClose').addEventListener('click',()=>document.getElementById('guideModal').classList.remove('open'));
document.getElementById('guideModal').addEventListener('click',e=>{if(e.target.id==='guideModal')e.currentTarget.classList.remove('open');});
document.getElementById('clearAllBtn').addEventListener('click',()=>{
if(!confirm('Cancellare tutte le evidenziazioni?')) return;
highlighted.clear();drawHighlighted.clear();
offset[0]=offset[1]=offset[2]=REST_OFFSET;
clearEngine();clearHistory();LAST_BACKTEST=[];
document.getElementById('coerenzaBox').innerHTML='';
renderChips();renderDrawChips();renderAll();renderNumeriPromettenti();
showNotification('🗑️ Tutto pulito');
});
document.getElementById('importDemoBtn').addEventListener('click',()=>{
[7,23,45,68,81,12,34,56,72,90].forEach(n=>drawHighlighted.add(n));
renderDrawChips();renderAll();
showNotification('📥 Demo caricata');
});
// Statistiche
document.getElementById('statsRun').addEventListener('click',mostraStatisticheTop10);
document.getElementById('statsTop10').addEventListener('click',mostraStatisticheTop10);
document.getElementById('statsPattern').addEventListener('click',mostraStatistichePattern);
// Archivi - SISTEMA UPLOAD LOCALE
document.getElementById('btnLoadLocal').addEventListener('click',()=>{
document.getElementById('fileInput').click();
});
document.getElementById('fileInput').addEventListener('change',async(e)=>{
const file = e.target.files[0];
if(!file) return;
if(!file.name.toLowerCase().endsWith('.zip')){
showNotification('⚠️ Seleziona un file ZIP');
return;
}
await caricaArchivioLocale(file);
e.target.value = ''; // Reset per permettere ricaricamento stesso file
});
document.getElementById('btnLoadCache').addEventListener('click',async()=>{
const dati = await caricaCache();
if(dati){ updateArchiveUI(); setupBtDateRange(); showNotification('💾 Cache caricata'); }
});
document.getElementById('btnValida').addEventListener('click',validaDati);
document.getElementById('btnClearCache').addEventListener('click',async()=>{
if(confirm('Cancellare l\'archivio delle estrazioni?')) await cancellaCache();
});
document.getElementById('btnUseLast1').addEventListener('click',()=>portaEstrazioni(1));
document.getElementById('btnUseLast3').addEventListener('click',()=>portaEstrazioni(3));
// Verifica a Ritroso
document.getElementById('btRun').addEventListener('click',runBacktest);
// Numeri Promettenti
document.getElementById('esitiRefresh').addEventListener('click',renderNumeriPromettenti);
// Motore L1
document.getElementById('engineRun').addEventListener('click',runEngine);
document.getElementById('heatToggle').addEventListener('click',toggleHeat);
document.getElementById('tourBtn').addEventListener('click',startTour);
document.getElementById('engineClear').addEventListener('click',clearEngine);
// Storico L2
document.getElementById('histLoad').addEventListener('click',loadHistoryFromTextarea);
document.getElementById('histDemo').addEventListener('click',loadHistoryDemo);
document.getElementById('histClear').addEventListener('click',clearHistory);
document.getElementById('convRun').addEventListener('click',runConvergence);
// Carica ruote da archivio
document.getElementById('btnCaricaRuoteConv').addEventListener('click',caricaRuoteDaArchivioPerConvergenze);
// Allineamenti e configurazioni
document.getElementById('findAlignmentBtn').addEventListener('click',findBestAlignments);
document.getElementById('saveConfigBtn').addEventListener('click',saveConfiguration);
// ============================================================
// INIZIALIZZAZIONE
// ============================================================
buildStatic();
RINGS.forEach((r,i)=>buildRing(r,i));
buildCompass(ringGroups[2]);
overlayG = el('g',{id:'figureOverlay'});
svg.appendChild(overlayG);
renderAll();
renderChips();
renderDrawChips();
updateSavedList();
updateRuotaFilter();
popolaSelectRuote();
popolaBtRuote();
popolaConvArchivioRuote();
initTheme();
loadFromURL();
updateArchiveUI();
setupBtDateRange();
if(cacheEsiste()){
setTimeout(async()=>{
const dati = await caricaCache();
if(dati){ updateArchiveUI(); setupBtDateRange(); }
}, 300);
}
</script>
</body>
</html>