<!DOCTYPE html>
<html lang="it" data-theme="dark">
<head>
<meta charset="UTF-8">
<title>Cerchio di Spilimbergo — Suite Completa v2.9</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; }
.fd-manna { background: #e0552c; }
.fd-piramide { background: #9b59b6; }
.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;
}
.firma-badge { cursor: pointer; transition: all .15s; }
.firma-badge:hover { transform: scale(1.08); }
.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; transition: opacity .2s; }
.fig-vertex-ring { fill: none; stroke-width: 1.5; opacity: .6; transition: opacity .2s; }
.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; }
.manna-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin: 8px 0; }
.manna-cell { background: rgba(0,0,0,.18); border-radius: 6px; padding: 6px; text-align: center; font-family: 'Cormorant Garamond', serif; font-size: 14px; font-weight: 600; }
.manna-cell.header { background: rgba(224,176,74,.15); color: var(--brass-hi); font-size: 11px; font-weight: 700; font-family: 'Inter', sans-serif; }
.manna-cell.result { background: rgba(224,85,44,.2); color: #e0552c; font-size: 16px; font-weight: 700; border: 1px solid #e0552c; }
.manna-cell.diff { background: rgba(79,174,122,.15); color: #4fae7a; font-size: 12px; }
.piramide { display: flex; flex-direction: column; align-items: center; gap: 4px; margin: 10px 0; }
.piramide-row { display: flex; gap: 4px; justify-content: center; }
.piramide-num {
background: rgba(155,89,182,.15);
border: 1px solid #9b59b6;
border-radius: 6px;
padding: 4px 8px;
font-family: 'Cormorant Garamond', serif;
font-size: 14px;
font-weight: 600;
color: #9b59b6;
min-width: 32px;
text-align: center;
}
.piramide-num.vertice {
background: rgba(155,89,182,.3);
border: 2px solid #9b59b6;
font-size: 18px;
box-shadow: 0 0 10px rgba(155,89,182,.4);
}
.spia-card { background: rgba(224,85,44,.08); border: 1px solid #e0552c; border-radius: 8px; padding: 10px; margin-top: 8px; }
.spia-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.spia-num {
background: #e0552c;
color: #fff;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Cormorant Garamond', serif;
font-weight: 700;
font-size: 16px;
}
.spia-results { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.spia-item {
background: rgba(0,0,0,.2);
border-radius: 6px;
padding: 6px;
text-align: center;
cursor: pointer;
transition: background .15s;
}
.spia-item:hover { background: rgba(224,176,74,.18); }
.spia-item .num { font-family: 'Cormorant Garamond', serif; font-size: 16px; font-weight: 700; color: var(--gold-fill); }
.spia-item .pct { font-size: 10px; color: #c9b98f; }
.cadenze-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; margin: 8px 0; }
.cadenza-cell { background: rgba(0,0,0,.18); border-radius: 4px; padding: 6px 2px; text-align: center; font-size: 11px; }
.cadenza-cell .num { font-family: 'Cormorant Garamond', serif; font-weight: 700; font-size: 14px; color: var(--gold-fill); }
.cadenza-cell .count { color: #c9b98f; font-size: 9px; margin-top: 2px; }
.cadenza-cell.cold { background: rgba(224,160,160,.15); border: 1px solid #e0a0a0; }
.cadenza-cell.hot { background: rgba(79,174,122,.15); border: 1px solid #4fae7a; }
.figure-delay { margin-top: 10px; }
.fig-delay-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: rgba(0,0,0,.15);
border-radius: 6px;
margin-bottom: 4px;
font-size: 12px;
}
.fig-delay-item .fname { color: var(--brass-hi); font-weight: 600; flex: 1; }
.fig-delay-item .fdelay { font-family: 'Cormorant Garamond', serif; font-size: 14px; font-weight: 700; }
.fig-delay-item.alert { background: rgba(224,85,44,.15); border: 1px solid #e0552c; }
.fig-delay-item.alert .fdelay { color: #e0552c; }
.l38-col { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 4px; align-items: center; margin-bottom: 4px; }
.l38-cell { background: rgba(0,0,0,.18); border-radius: 6px; padding: 5px; text-align: center; font-family: 'Cormorant Garamond', serif; font-size: 15px; font-weight: 600; }
.l38-cell.ambo { color: var(--gold-fill); border: 1px solid var(--gold-fill); }
.l38-cell.escl { color: #e0a0a0; text-decoration: line-through; opacity: .7; }
.l38-swap { background: #332512; border: 1px solid var(--brass); color: var(--paper-1); border-radius: 6px; cursor: pointer; font-size: 11px; padding: 4px 6px; }
.shrink-line {
font-family: 'Courier New', monospace;
font-size: 10.5px;
color: #b3a37e;
padding: 3px 6px;
background: rgba(0,0,0,.15);
border-radius: 4px;
margin-bottom: 3px;
}
.shrink-line .ok { color: #4fae7a; }
.shrink-line .no { color: #e0a0a0; }
/* v2.10: Doppie Distanze */
.dist-card{border:2px solid #0f6e6e;margin-top:12px;}
.dist-result{margin-top:10px;padding:10px;background:rgba(15,110,110,.12);border-radius:8px;}
.dist-distanze{display:flex;flex-wrap:wrap;gap:4px;margin:6px 0;}
.dist-tag{background:rgba(224,176,74,.15);border:1px solid var(--gold-fill);color:var(--gold-fill);
font-family:'Cormorant Garamond',serif;font-weight:600;font-size:13px;padding:2px 8px;border-radius:10px;}
.dist-tag.double{background:rgba(224,85,44,.18);border-color:#e0552c;color:#e0552c;}
</style>
</head>
<body>
<div class="wrap">
<h1>Cerchio di Spilimbergo — Suite Completa v2.10</h1>
<p class="subtitle">🔍 Doppie Distanze · Logica 38 · Rx = ritardo MAX storico · Data da calendario · Tracker per ruota · Vertici Interattivi · Fuori 90</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">
<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>
<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>
<div class="card" style="border:2px solid #9b59b6;">
<h3>🧭 Logica 38 — Colonne di Cadenza <span style="font-size:11px;color:#8a7a5c;">v2.9</span></h3>
<div class="bt-grid">
<div><label>Ruota</label><select id="l38Ruota"></select></div>
<div><label>Primo estratto</label><input type="number" id="l38Num" min="1" max="90" placeholder="es. 38"></div>
<div>
<label>Derivazione cadenza</label>
<select id="l38Mode">
<option value="radice">Radice (somma cifre)</option>
<option value="compl">Complemento 90</option>
<option value="manuale">Manuale</option>
</select>
</div>
<div><label>Cadenza</label><input type="number" id="l38Cad" min="0" max="9" disabled></div>
<div style="grid-column:1/-1;"><label>Data analisi (calendario)</label><input type="date" id="l38Date"></div>
</div>
<div class="btn-row" style="margin-top:8px;">
<button class="step primary" id="l38Run">🧭 Imposta cerchio & colonne</button>
</div>
<div id="l38Cols"></div>
<div class="btn-row" style="margin-top:6px;">
<button class="step" id="l38Shrink">📉 Shrink 9→2</button>
<button class="step" id="l38Terzine">🧩 Terzine 280</button>
<button class="step" id="l38Save">💾 Salva (9 conc.)</button>
</div>
<div style="font-size:10px;color:#8a7a5c;margin-top:6px;line-height:1.5;">
Rc = ritardo corrente (ambo nella combinazione) · Rx = <b>massimo ritardo storico</b> mai registrato dalla combinazione (filtro Rc ≥ Rx = "mai stata così ritardata"). Shrink e Terzine usano l'archivio fino alla data scelta.
</div>
<div id="l38Results"></div>
<div id="l38Tracker"></div>
</div>
<!-- v2.9.1: Verifica Estrazioni per Periodo -->
<div class="card" style="border:2px solid #4fae7a;margin-top:12px;">
<h3>📅 Verifica Estrazioni per Periodo</h3>
<div class="bt-grid">
<div><label>Dal</label><input type="date" id="verDataDal"></div>
<div><label>Al</label><input type="date" id="verDataAl"></div>
</div>
<div class="btn-row" style="margin-top:8px;">
<button class="step primary" id="verEstrazioni" style="flex:1;"> Conta Estrazioni</button>
</div>
<div id="verRisultato" style="margin-top:10px;font-size:12px;color:#c9b98f;"></div>
</div>
<!-- v2.10: RICERCA AUTOMATICA PER DOPPIE DISTANZE -->
<div class="card dist-card">
<h3>🔍 Doppie Distanze <span style="font-size:11px;color:#8a7a5c;">v2.10</span></h3>
<div class="esiti-subtitle">Cerca terzine sulla ruota target che hanno le doppie distanze dell'estrazione sorgente, con filtro Rc ≥ Rx.</div>
<div class="bt-grid">
<div><label>Ruota Sorgente</label><select id="distRuotaSorgente"></select></div>
<div><label>Ruota Target</label><select id="distRuotaTarget"></select></div>
<div style="grid-column:1/-1;">
<label>Numeri estrazione sorgente (es. 26 83 40 69 53)</label>
<input type="text" id="distNumeriSorgente" placeholder="Inserisci 5 numeri separati da spazio">
</div>
</div>
<div class="btn-row" style="margin-top:8px;">
<button class="step primary" id="distRun" style="flex:1;">🔍 Cerca Terzine Ottimali</button>
</div>
<div id="distDistanze"></div>
<div id="distResults"></div>
</div>
<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>
<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>
<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 Motore, Convergenze, Backtest, Manna e Piramide con <b>regola del Fuori 90</b> applicata a tutti i calcoli.
Arancio = top assoluto. Clicca un pallino per evidenziarlo sul Cerchio.
</div>
<div id="esitiContent">
<div class="esiti-vuoto">
Esegui almeno un'analisi (⚡ Motore, 🔀 Convergenze, 🕰️ Backtest, 🎲 Manna o 🔺 Piramide)<br>
poi premi <b>↻ Aggiorna</b> per vedere i numeri promettenti.
</div>
</div>
</div>
<div class="card">
<h3>🎲 Metodo delle 3 Estrazioni <span style="font-size:11px;color:#8a7a5c;">Manna</span></h3>
<div class="btn-row">
<select id="mannaRuota" style="flex:1;"></select>
<button class="step primary" id="mannaRun" style="flex:0 0 auto;padding:8px 14px;">Calcola</button>
</div>
<div id="mannaResults"></div>
</div>
<div class="card">
<h3>🔺 Piramide Ciclotimica</h3>
<div class="esiti-subtitle">Costruisce la piramide dalle differenze successive dei numeri evidenziati (VERDI o ORO).</div>
<div class="btn-row">
<button class="step primary" id="piramideRun">🔺 Genera Piramide</button>
<button class="step" id="piramideClear">🧹</button>
</div>
<div id="piramideResults"></div>
</div>
<div class="card">
<h3>🕵️ Spie per Ruota</h3>
<div class="esiti-subtitle">Analizza i numeri evidenziati e mostra quali numeri "annunciano" storicamente nelle 5 estrazioni successive.</div>
<div class="btn-row">
<select id="spieRuota" style="flex:1;"></select>
<button class="step primary" id="spieRun" style="flex:0 0 auto;padding:8px 14px;">Analizza Spie</button>
</div>
<div id="spieResults"></div>
</div>
<div class="card">
<h3>⏰ Figure Mancanti (Ritardo Figure)</h3>
<div class="esiti-subtitle">Mostra quali figure geometriche non escono da più tempo sulla ruota scelta.</div>
<div class="btn-row">
<select id="figDelayRuota" style="flex:1;"></select>
<button class="step" id="figDelayRun" style="flex:0 0 auto;padding:8px 14px;">Analizza</button>
</div>
<div id="figDelayResults"></div>
</div>
<div class="card">
<h3>🎯 Cadenze & Decine</h3>
<div class="esiti-subtitle">Analizza la distribuzione dei numeri evidenziati per cadenza (cifra finale) e decina.</div>
<div id="cadenzeResults">
<div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Evidenzia almeno 2 numeri per vedere l'analisi</div>
</div>
</div>
<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>
<div class="card">
<h3>🤖 Motore Previsivo <span style="font-size:11px;color:#8a7a5c;">L1 · Vertici Interattivi</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>
<div class="card">
<h3>📜 Storico & Convergenze <span style="font-size:11px;color:#8a7a5c;">L2</span></h3>
<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>
<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>
<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>
<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>
<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>
<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">
v2.9: Rx = massimo ritardo storico della combinazione (come script "Lunghetta per Ritardo Max"); data da calendario nella Logica 38; tracker solo ruota selezionata con eliminazione. v2.7: mouse sui vertici per leggere i numeri coperti.
</p>
</div>
</div>
</div>
<div class="guide-modal" id="guideModal">
<div class="guide-box">
<button class="guide-close" id="guideClose">✕ Chiudi</button>
<h2>Guida alla Suite Completa v2.9</h2>
<h4>Rc / Rx corretti (NOVITÀ v2.9)</h4>
<p><b>Rc</b> = ritardo corrente della combinazione (concorsi dall'ultimo ambo realizzato al suo interno). <b>Rx</b> = <b>massimo ritardo storico</b> mai registrato dalla stessa combinazione sull'archivio della ruota fino alla data scelta (il più lungo intervallo passato senza ambi). Il filtro <b>Rc ≥ Rx</b> segnala combinazioni "mai state così ritardate". Per i gruppi di 3 terzine: Rc = somma dei ritardi correnti, Rx = somma delle attese empiriche.</p>
<h4>Data da calendario (NOVITÀ v2.9)</h4>
<p>Nella Logica 38 puoi fissare la data di analisi: shrink e terzine vengono calcolati sull'archivio <b>fino a quella data</b>, come nello script originale. Il tracker verifica invece sempre sulle ultime 9 estrazioni reali.</p>
<h4>Tracker per ruota (NOVITÀ v2.9)</h4>
<p>Il riepilogo previsioni mostra <b>solo la ruota selezionata</b>; ogni riga ha ✕ per eliminarla e c'è ️ per cancellarle tutte in blocco.</p>
<h4>Logica 38 — Colonne di Cadenza (v2.8)</h4>
<p>Imposta ruota e primo estratto (es. Genova 38). Cadenza da radice/complemento/manuale; la novina occupa 3 colonne sul cerchio: ambo = interno+medio, escluso = esterno (⇄ scambia). 📉 Shrink 9→2 e 🧩 280 terzine; 💾 salva con durata 9 concorsi.</p>
<h4>Vertici Interattivi (v2.7)</h4>
<p>Mouse/tap su un vertice: il numero coperto appare ingrandito e leggibile.</p>
<h4>Figure Selezionabili (v2.6) · Sviluppo Dinamico (v2.5) · Fuori 90</h4>
<p>Clicca una Δ della firma per vedere solo quella figura; i calcoli >90 normalizzati in 1-90.</p>
<h4>Altri moduli</h4>
<p>Archivio ZIP+IndexedDB · Manna · Piramide · Spie · Figure Mancanti · Cadenze & Decine · Backtest · Convergenze · Allineamenti · Numeri Promettenti.</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
// ============================================================
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'
};
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 }
};
let archivio = null;
let LAST_BACKTEST = [];
let MANNA_RESULTS = [];
let PIRAMIDE_RESULTS = [];
// ============================================================
// REGOLA DEL FUORI 90
// ============================================================
function fuori90(n) {
n = Math.round(n);
while (n > 90) n -= 90;
while (n <= 0) n += 90;
return n;
}
// ============================================================
// DISTANZA CICLOMETRICA MINIMA (v2.5)
// ============================================================
function distanzaCiclo(a, b) {
const d = Math.abs(a - b);
return Math.min(d, 90 - d);
}
// ============================================================
// FIRMA GEOMETRICA (v2.5)
// ============================================================
function firmaGeometrica(nums) {
const freq = new Map();
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
const d = distanzaCiclo(nums[i], nums[j]);
if (!freq.has(d)) freq.set(d, { d, count: 0, pairs: [] });
const entry = freq.get(d);
entry.count++;
entry.pairs.push([nums[i], nums[j]]);
}
}
return [...freq.values()].sort((a, b) => b.count - a.count || a.d - b.d);
}
// ============================================================
// 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');
}
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;
document.getElementById('mannaRuota').innerHTML = options;
document.getElementById('spieRuota').innerHTML = options;
document.getElementById('figDelayRuota').innerHTML = options;
document.getElementById('l38Ruota').innerHTML = options;
}
function parseData(s) {
if (!s) return null;
s = String(s).trim();
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; }
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')}`;
}
function validaEstrazione(riga, ruoteViste, dateViste) {
const errors = [];
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');
if (new Set(nums).size !== nums.length) errors.push('Numeri duplicati nella stessa estrazione');
const key = `${riga.data}_${riga.ruota}`;
if (dateViste.has(key)) errors.push('Estrazione duplicata');
else dateViste.add(key);
if (!CODICE_TO_NOME[riga.ruota]) errors.push('Codice ruota non valido');
return { valid: errors.length === 0, errors };
}
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;
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();
setupL38DateRange();
renderL38Tracker();
showNotification(`📥 ${archivio.righe.length.toLocaleString('it-IT')} righe in archivio`);
} catch (e) {
logArchivio(`❌ Errore: ${e.message}`, 'err');
showNotification(`❌ Errore: ${e.message}`);
}
setProgress(100, 'Completato');
}
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');
}
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]);
aggiornaCadenzeDecine();
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();
aggiornaCadenzeDecine();
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();
aggiornaCadenzeDecine();
showNotification(`📊 ${nums.length} numeri evidenziati`);
});
});
showNotification(`📊 Pattern ${CODICE_TO_NOME[ruota]} caricati`);
}
// ============================================================
// METODO DELLE 3 ESTRAZIONI (MANNA)
// ============================================================
function runManna() {
if (!archivio || !archivio.righe.length) {
showNotification('⚠️ Carica prima l\'archivio');
return;
}
const ruota = document.getElementById('mannaRuota').value;
const sub = archivio.righe.filter(r => r.ruota === ruota).sort((a, b) => parseData(b.data) - parseData(a.data)).slice(0, 3);
if (sub.length < 3) {
showNotification('⚠️ Servono almeno 3 estrazioni');
return;
}
const e1 = [sub[2].n1, sub[2].n2, sub[2].n3, sub[2].n4, sub[2].n5];
const e2 = [sub[1].n1, sub[1].n2, sub[1].n3, sub[1].n4, sub[1].n5];
const e3 = [sub[0].n1, sub[0].n2, sub[0].n3, sub[0].n4, sub[0].n5];
const somme = [];
for (let i = 0; i < 5; i++) {
somme.push(fuori90(e1[i] + e2[i] + e3[i]));
}
const diff12 = [];
const diff23 = [];
for (let i = 0; i < 5; i++) {
diff12.push(fuori90(Math.abs(e1[i] - e2[i])));
diff23.push(fuori90(Math.abs(e2[i] - e3[i])));
}
const spie = [];
for (let i = 0; i < 5; i++) {
spie.push(fuori90(e1[i] + e3[i]));
}
MANNA_RESULTS = [...new Set([...somme, ...diff12, ...diff23, ...spie])].slice(0, 12);
let html = `<div style="font-size:11px;color:#c9b98f;margin:8px 0;">Ultime 3 estrazioni di ${CODICE_TO_NOME[ruota]}:</div>`;
html += `<div class="manna-grid">`;
html += `<div class="manna-cell header">E3</div>`;
e3.forEach(n => { html += `<div class="manna-cell">${n}</div>`; });
html += `<div class="manna-cell header">E2</div>`;
e2.forEach(n => { html += `<div class="manna-cell">${n}</div>`; });
html += `<div class="manna-cell header">E1</div>`;
e1.forEach(n => { html += `<div class="manna-cell">${n}</div>`; });
html += `</div>`;
html += `<div style="font-size:11px;color:#c9b98f;margin:8px 0 4px;">Somme verticali (Fuori 90):</div>`;
html += `<div class="manna-grid">`;
html += `<div class="manna-cell header">Somma</div>`;
somme.forEach(n => { html += `<div class="manna-cell result">${n}</div>`; });
html += `</div>`;
html += `<div style="font-size:11px;color:#c9b98f;margin:8px 0 4px;">Differenze E1-E2 (Fuori 90):</div>`;
html += `<div class="manna-grid">`;
html += `<div class="manna-cell header">Diff</div>`;
diff12.forEach(n => { html += `<div class="manna-cell diff">${n}</div>`; });
html += `</div>`;
html += `<div style="font-size:11px;color:#c9b98f;margin:8px 0 4px;">Spie incrociate E1+E3 (Fuori 90):</div>`;
html += `<div class="manna-grid">`;
html += `<div class="manna-cell header">Spie</div>`;
spie.forEach(n => { html += `<div class="manna-cell result">${n}</div>`; });
html += `</div>`;
html += `<div class="btn-row" style="margin-top:10px;"><button class="step primary" id="mannaHighlight">🎯 Evidenzia previsioni</button></div>`;
document.getElementById('mannaResults').innerHTML = html;
document.getElementById('mannaHighlight').addEventListener('click', () => {
MANNA_RESULTS.forEach(n => highlighted.add(n));
renderChips();
renderAll();
aggiornaCadenzeDecine();
showNotification(` ${MANNA_RESULTS.length} numeri Manna evidenziati`);
});
showNotification(` Metodo Manna calcolato per ${CODICE_TO_NOME[ruota]}`);
renderNumeriPromettenti();
}
// ============================================================
// PIRAMIDE CICLOTIMICA
// ============================================================
function runPiramide() {
const base = getBase();
if (base.length < 2) {
showNotification('️ Carica prima almeno 2 numeri');
return;
}
const nums = base.slice(0, 5).sort((a, b) => a - b);
if (nums.length < 2) {
showNotification('⚠️ Servono almeno 2 numeri');
return;
}
let rows = [nums];
while (rows[rows.length - 1].length > 1) {
const prev = rows[rows.length - 1];
const next = [];
for (let i = 0; i < prev.length - 1; i++) {
next.push(fuori90(Math.abs(prev[i] - prev[i + 1])));
}
rows.push(next);
}
const vertice = rows[rows.length - 1][0];
PIRAMIDE_RESULTS = [vertice];
let html = `<div class="piramide">`;
rows.forEach((row, idx) => {
html += `<div class="piramide-row">`;
row.forEach(n => {
const isVertice = idx === rows.length - 1;
html += `<div class="piramide-num ${isVertice ? 'vertice' : ''}">${n}</div>`;
});
html += `</div>`;
});
html += `</div>`;
html += `<div style="text-align:center;margin-top:10px;font-size:13px;color:#c9b98f;">Vertice previsto: <b style="color:#9b59b6;font-size:18px;">${vertice}</b></div>`;
html += `<div class="btn-row" style="margin-top:10px;"><button class="step primary" id="piramideHighlight">🎯 Evidenzia vertice</button></div>`;
document.getElementById('piramideResults').innerHTML = html;
document.getElementById('piramideHighlight').addEventListener('click', () => {
highlighted.add(vertice);
renderChips();
renderAll();
aggiornaCadenzeDecine();
showNotification(`🔺 Vertice ${vertice} evidenziato`);
});
showNotification(`🔺 Piramide generata con vertice ${vertice}`);
renderNumeriPromettenti();
}
// ============================================================
// SPIE PER RUOTA
// ============================================================
function runSpie() {
if (!archivio || !archivio.righe.length) {
showNotification('️ Carica prima l\'archivio');
return;
}
const ruota = document.getElementById('spieRuota').value;
const base = [...drawHighlighted, ...highlighted];
if (base.length === 0) {
showNotification('⚠️ Evidenzia prima almeno un numero');
return;
}
const sub = archivio.righe.filter(r => r.ruota === ruota).sort((a, b) => parseData(a.data) - parseData(b.data));
const spieMap = new Map();
base.forEach(spia => {
const risultati = new Map();
for (let i = 0; i < sub.length - 1; i++) {
const nums = [sub[i].n1, sub[i].n2, sub[i].n3, sub[i].n4, sub[i].n5];
if (!nums.includes(spia)) continue;
for (let j = 1; j <= 5 && i + j < sub.length; j++) {
const nextNums = [sub[i + j].n1, sub[i + j].n2, sub[i + j].n3, sub[i + j].n4, sub[i + j].n5];
nextNums.forEach(n => {
risultati.set(n, (risultati.get(n) || 0) + 1);
});
}
}
const usciteSpia = sub.filter(r => [r.n1, r.n2, r.n3, r.n4, r.n5].includes(spia)).length;
if (usciteSpia > 0 && risultati.size > 0) {
const top3 = [...risultati.entries()]
.map(([n, count]) => ({ n, count, pct: (count / (usciteSpia * 5) * 100) }))
.sort((a, b) => b.count - a.count)
.slice(0, 3);
spieMap.set(spia, { uscite: usciteSpia, top3 });
}
});
let html = '';
if (spieMap.size === 0) {
html = '<div style="color:#8a7a5c;padding:8px;text-align:center;">Nessuna spia trovata nell\'archivio</div>';
} else {
spieMap.forEach((data, spia) => {
html += `<div class="spia-card">`;
html += `<div class="spia-header"><div class="spia-num">${spia}</div><div style="font-size:11px;color:#c9b98f;">Uscita ${data.uscite} volte · Top 3 annunciati:</div></div>`;
html += `<div class="spia-results">`;
data.top3.forEach(r => {
html += `<div class="spia-item" data-n="${r.n}">
<div class="num">${r.n}</div>
<div class="pct">${r.pct.toFixed(0)}%</div>
</div>`;
});
html += `</div></div>`;
});
}
document.getElementById('spieResults').innerHTML = html;
document.querySelectorAll('.spia-item').forEach(el => {
el.addEventListener('click', () => {
const n = parseInt(el.dataset.n);
highlighted.add(n);
renderChips();
renderAll();
aggiornaCadenzeDecine();
showNotification(`️ ${n} evidenziato come spia`);
});
});
showNotification(`️ Analizzate ${spieMap.size} spie su ${CODICE_TO_NOME[ruota]}`);
}
// ============================================================
// FIGURE MANCANTI (RITARDO FIGURE)
// ============================================================
function runFigureDelay() {
if (!archivio || !archivio.righe.length) {
showNotification('⚠️ Carica prima l\'archivio');
return;
}
const ruota = document.getElementById('figDelayRuota').value;
const sub = archivio.righe.filter(r => r.ruota === ruota).sort((a, b) => parseData(b.data) - parseData(a.data)).slice(0, 100);
if (sub.length < 10) {
showNotification('️ Servono almeno 10 estrazioni');
return;
}
const figureDelay = {
'Opposti 180°': { ritardo: 0, trovato: false },
'Triangolo 120°': { ritardo: 0, trovato: false },
'Terzina 60°': { ritardo: 0, trovato: false },
'Quadrato 90°': { ritardo: 0, trovato: false }
};
sub.forEach((r, idx) => {
const nums = [r.n1, r.n2, r.n3, r.n4, r.n5];
const figs = detectBaseFigures(nums);
figs.forEach(f => {
if (!figureDelay[f.type].trovato) {
figureDelay[f.type].ritardo = idx;
figureDelay[f.type].trovato = true;
}
});
});
const figs = Object.entries(figureDelay).map(([nome, data]) => ({
nome,
ritardo: data.trovato ? data.ritardo : sub.length,
alert: (data.trovato ? data.ritardo : sub.length) > 15
})).sort((a, b) => b.ritardo - a.ritardo);
let html = `<div class="figure-delay">`;
figs.forEach(f => {
html += `<div class="fig-delay-item ${f.alert ? 'alert' : ''}">
<span class="fname">${f.nome}</span>
<span class="fdelay">${f.ritardo === sub.length ? 'Mai' : f.ritardo + ' estr.'}</span>
</div>`;
});
html += `</div>`;
html += `<div style="font-size:10px;color:#8a7a5c;margin-top:8px;text-align:center;">Analisi ultime 100 estrazioni · Figure con ritardo >15 sono evidenziate</div>`;
document.getElementById('figDelayResults').innerHTML = html;
showNotification(`⏰ Ritardo figure calcolato per ${CODICE_TO_NOME[ruota]}`);
}
// ============================================================
// CADENZE E DECINE
// ============================================================
function aggiornaCadenzeDecine() {
const nums = [...highlighted, ...drawHighlighted];
const box = document.getElementById('cadenzeResults');
if (nums.length < 2) {
box.innerHTML = '<div style="text-align:center;color:#8a7a5c;font-size:12px;padding:8px;">Evidenzia almeno 2 numeri per vedere l\'analisi</div>';
return;
}
const cadenze = new Array(10).fill(0);
nums.forEach(n => { cadenze[n % 10]++; });
const decine = new Array(9).fill(0);
nums.forEach(n => { decine[Math.floor((n - 1) / 10)]++; });
let html = `<div style="font-size:11px;color:#c9b98f;margin:8px 0 4px;">Distribuzione per Cadenza (cifra finale):</div>`;
html += `<div class="cadenze-grid">`;
for (let i = 0; i < 10; i++) {
const cls = cadenze[i] === 0 ? 'cold' : (cadenze[i] >= 2 ? 'hot' : '');
html += `<div class="cadenza-cell ${cls}">
<div class="num">${i}</div>
<div class="count">${cadenze[i]}</div>
</div>`;
}
html += `</div>`;
html += `<div style="font-size:11px;color:#c9b98f;margin:12px 0 4px;">Distribuzione per Decina:</div>`;
html += `<div class="cadenze-grid" style="grid-template-columns:repeat(9,1fr);">`;
const decLabels = ['1-10', '11-20', '21-30', '31-40', '41-50', '51-60', '61-70', '71-80', '81-90'];
for (let i = 0; i < 9; i++) {
const cls = decine[i] === 0 ? 'cold' : (decine[i] >= 2 ? 'hot' : '');
html += `<div class="cadenza-cell ${cls}">
<div class="num" style="font-size:11px;">${decLabels[i]}</div>
<div class="count">${decine[i]}</div>
</div>`;
}
html += `</div>`;
html += `<div style="font-size:10px;color:#8a7a5c;margin-top:8px;line-height:1.5;">
<span style="color:#e0a0a0;">■</span> Fredde (assenti) · <span style="color:#4fae7a;">■</span> Calde (≥2 numeri)
</div>`;
box.innerHTML = html;
}
// ============================================================
// 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(); aggiornaCadenzeDecine(); 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(); aggiornaCadenzeDecine(); showNotification(' Estrazione reale sul Cerchio');
});
}
}
// ============================================================
// NUMERI PROMETTENTI
// ============================================================
function calcolaNumeriPromettenti() {
const mappa = new Map();
const addFonte = (n, score, fonte, peso = 1) => {
n = fuori90(n);
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));
});
MANNA_RESULTS.slice(0, 12).forEach((n, i) => {
const peso = 1.3 - (i * 0.05);
addFonte(n, 80 - i * 5, 'manna', Math.max(0.5, peso));
});
PIRAMIDE_RESULTS.forEach(n => { addFonte(n, 90, 'piramide', 1.2); });
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, 🕰️ Backtest, 🎲 Manna o 🔺 Piramide)<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>' : ''}
${r.fonti.has('manna') ? '<span class="fonte-dot fd-manna" title="Metodo Manna"></span>' : ''}
${r.fonti.has('piramide') ? '<span class="fonte-dot fd-piramide" title="Piramide"></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>Conv</span>
<span><span class="fonte-dot fd-backtest"></span>Backtest</span>
<span><span class="fonte-dot fd-manna"></span>Manna</span>
<span><span class="fonte-dot fd-piramide"></span>Piramide</span>
</div>`;
box.querySelectorAll('.esito-num').forEach(el => {
el.addEventListener('click', () => {
const n = parseInt(el.dataset.n);
highlighted.add(n); renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification(`🎯 ${n} evidenziato sul Cerchio`);
});
});
}
// ============================================================
// CERCHIO DI SPILIMBERGO
// ============================================================
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: [], firma: [], figDin: [], activeDist: null };
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`;
}
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);
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 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 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 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 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'));
}
});
}
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);
}
// ============================================================
// v2.7: VERTICI INTERATTIVI
// ============================================================
function revealNumber(num, on) {
const t = svg.querySelector(`text.num[data-number="${num}"]`);
if (!t) return;
if (on) {
t.dataset.origfs = t.getAttribute('font-size') || t.dataset.origfs || '';
t.setAttribute('font-size', (parseFloat(t.getAttribute('font-size')) || 16) + 7);
t.setAttribute('font-weight', '800');
t.style.paintOrder = 'stroke';
t.style.stroke = 'rgba(255,255,255,.85)';
t.style.strokeWidth = '3px';
overlayG.querySelectorAll(`[data-num="${num}"]`).forEach(c => { c.style.opacity = '0.25'; });
} else {
if (t.dataset.origfs) t.setAttribute('font-size', t.dataset.origfs);
t.removeAttribute('font-weight');
t.style.stroke = '';
t.style.strokeWidth = '';
t.style.paintOrder = '';
overlayG.querySelectorAll(`[data-num="${num}"]`).forEach(c => { c.style.opacity = ''; });
}
}
function drawVertex(p, color, r = 6, num = null) {
const v = el('circle', { cx: p.x, cy: p.y, r: r, class: 'fig-vertex', fill: color, stroke: '#fff' });
const ring = el('circle', { cx: p.x, cy: p.y, r: r + 4, class: 'fig-vertex-ring', stroke: color });
if (num != null) {
[v, ring].forEach(c => {
c.setAttribute('data-num', num);
c.style.pointerEvents = 'all';
c.style.cursor = 'pointer';
c.addEventListener('pointerenter', () => revealNumber(num, true));
c.addEventListener('pointerleave', () => revealNumber(num, false));
c.addEventListener('pointerdown', (ev) => { revealNumber(num, true); setTimeout(() => revealNumber(num, false), 1200); ev.stopPropagation(); });
});
}
overlayG.appendChild(v);
overlayG.appendChild(ring);
}
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, 6, p1.num); drawVertex(p2, color, 6, p2.num);
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, 6, p.num));
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, p.num));
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, 6, p.num));
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, p.num));
const c = midPoint(pts);
addLabel(c.x, c.y - 16, label, color);
}
// ============================================================
// FIGURA DINAMICA (v2.5)
// ============================================================
function drawFigDinamica(pts, color, label) {
overlayG.appendChild(el('path', { d: polygonPath(pts), class: 'fig-shape fig-pulse', fill: color, stroke: color, 'stroke-dasharray': '6 4' }));
drawRays(pts, color);
pts.forEach(p => drawVertex(p, color, 5, p.num));
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 - 16, label, color);
}
// ============================================================
// v2.6: FIGURE VISIBILI = classiche + dinamiche filtrate
// ============================================================
function getVisibleFigures() {
const vis = [...ENGINE.figures];
if (ENGINE.activeDist === 'all') {
vis.push(...ENGINE.figDin);
} else if (typeof ENGINE.activeDist === 'number') {
ENGINE.figDin.forEach(f => { if (f.dist === ENGINE.activeDist) vis.push(f); });
}
return vis;
}
function drawFigureOverlay() {
if (!overlayG) return;
overlayG.innerHTML = '';
getVisibleFigures().forEach(f => {
const pts = f.numbers.map(n => {
const pos = findNumberPosition(n);
return pos ? { x: pos.x, y: pos.y, num: n } : null;
});
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;
case 'Dinamica': drawFigDinamica(pts, f.color, `Figura Δ${f.dist} ×${f.count}`); break;
default: drawFigDefault(pts, f.color, f.type);
}
});
}
function getBase() { return drawHighlighted.size >= 2 ? [...drawHighlighted] : [...highlighted]; }
function computeCandidates(base) {
const scores = new Map();
const add = (n, peso, motivo) => {
n = fuori90(n);
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);
}
// ============================================================
// MOTORE PREVISIVO v2.6/v2.7
// ============================================================
function runEngine() {
const base = getBase();
if (base.length < 2) { showNotification('️ Carica prima almeno 2 numeri'); return; }
ENGINE.base = base;
let cands = computeCandidates(base);
const firma = firmaGeometrica(base);
ENGINE.firma = firma;
const dominanti = (firma.filter(f => f.count >= 2).length ? firma.filter(f => f.count >= 2) : firma.slice(0, 2));
const figDin = [];
firma.forEach(sd => {
const vertici = new Set();
sd.pairs.forEach(pair => { vertici.add(pair[0]); vertici.add(pair[1]); });
const vertArr = [...vertici].sort((a, b) => a - b);
if (vertArr.length >= 2) {
figDin.push({ type: 'Dinamica', numbers: vertArr, color: '#9b59b6', dist: sd.d, count: sd.count });
}
if (dominanti.includes(sd)) {
vertArr.forEach(v => {
[sd.d, 90 - sd.d].forEach(step => {
const n = fuori90(v + step);
const existing = cands.find(c => c.n === n);
if (existing) {
existing.score += 1.5 * sd.count;
if (!existing.motivi.includes(`sviluppo Δ${sd.d}`)) existing.motivi.push(`sviluppo Δ${sd.d}`);
} else if (!base.includes(n)) {
cands.push({ n, score: 1.5 * sd.count, motivi: [`sviluppo Δ${sd.d} da ${v}`] });
}
});
});
}
});
cands.sort((x, y) => y.score - x.score || x.n - y.n);
ENGINE.candidates = cands;
ENGINE.figures = detectBaseFigures(base);
ENGINE.figDin = figDin;
ENGINE.activeDist = dominanti.length ? dominanti[0].d : null;
renderEnginePanel();
drawFigureOverlay();
if (heatOn) applyHeat();
showNotification(`⚡ Firma: Δ${firma.map(f => f.d).join(', Δ')} · selezionata Δ${ENGINE.activeDist}`);
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 = {};
getVisibleFigures().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) || ENGINE.figDin.find(f => f.type === t) || { color: '#9b59b6' }).color;
return `<span class="fig-badge" style="border-color:${col};color:${col}">${k}× ${t}</span>`;
}).join('');
let firmaHtml = '';
if (ENGINE.firma && ENGINE.firma.length) {
firmaHtml = `<div style="margin-top:8px;font-size:11px;color:#c9b98f;">Firma geometrica — <b>clicca una Δ per vedere SOLO quella figura</b> (riclic = nascondi):</div>`;
firmaHtml += `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;">`;
ENGINE.firma.slice(0, 8).forEach(f => {
const active = ENGINE.activeDist === f.d;
firmaHtml += `<button class="fig-badge firma-badge" data-dist="${f.d}" style="border-color:${active ? '#e0b04a' : (f.count >= 2 ? '#9b59b6' : '#5a4630')};color:${active ? '#e0b04a' : (f.count >= 2 ? '#9b59b6' : '#b3a37e')};background:${active ? 'rgba(224,176,74,.18)' : 'transparent'};font-weight:${active ? '700' : '400'}">Δ${f.d} ×${f.count}</button>`;
});
firmaHtml += `<button class="fig-badge firma-badge" id="firmaAll" style="border-color:${ENGINE.activeDist === 'all' ? '#e0b04a' : '#5a4630'};color:${ENGINE.activeDist === 'all' ? '#e0b04a' : '#b3a37e'}">👁 Tutte</button>`;
firmaHtml += `</div>`;
if (typeof ENGINE.activeDist === 'number') {
const sel = ENGINE.figDin.filter(f => f.dist === ENGINE.activeDist);
if (sel.length) {
const vert = [...new Set(sel.flatMap(f => f.numbers))].sort((a, b) => a - b);
const dev = [...new Set(vert.flatMap(v => [fuori90(v + ENGINE.activeDist), fuori90(v + 90 - ENGINE.activeDist)]))].filter(n => !vert.includes(n)).sort((a, b) => a - b);
firmaHtml += `<div style="margin-top:6px;font-size:11px;color:#c9b98f;">Vertici Δ${ENGINE.activeDist}: <b style="color:#9b59b6">${vert.join(', ')}</b></div>`;
firmaHtml += `<div style="margin-top:3px;font-size:11px;color:#c9b98f;">Sviluppi (Δ e 90−Δ): <b style="color:#e0b04a">${dev.join(', ')}</b></div>`;
firmaHtml += `<div class="btn-row" style="margin-top:6px;"><button class="step" id="firmaHighlight">🎯 Evidenzia vertici + sviluppi</button></div>`;
}
}
}
box.innerHTML = firmaHtml + `
<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('.firma-badge[data-dist]').forEach(b => {
b.addEventListener('click', () => {
const d = parseInt(b.dataset.dist);
ENGINE.activeDist = (ENGINE.activeDist === d) ? null : d;
renderEnginePanel();
drawFigureOverlay();
});
});
const allBtn = box.querySelector('#firmaAll');
if (allBtn) allBtn.addEventListener('click', () => {
ENGINE.activeDist = (ENGINE.activeDist === 'all') ? null : 'all';
renderEnginePanel();
drawFigureOverlay();
});
const hlBtn = box.querySelector('#firmaHighlight');
if (hlBtn) hlBtn.addEventListener('click', () => {
const sel = ENGINE.figDin.filter(f => f.dist === ENGINE.activeDist);
const vert = [...new Set(sel.flatMap(f => f.numbers))];
const dev = [...new Set(vert.flatMap(v => [fuori90(v + ENGINE.activeDist), fuori90(v + 90 - ENGINE.activeDist)]))].filter(n => !vert.includes(n));
[...vert, ...dev].forEach(n => highlighted.add(n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification(`🎯 ${vert.length + dev.length} numeri evidenziati`);
});
box.querySelectorAll('.cand-item').forEach(it => {
it.addEventListener('click', () => { highlighted.add(parseInt(it.dataset.n)); renderChips(); renderAll(); aggiornaCadenzeDecine(); });
});
}
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() {
const vis = getVisibleFigures();
if (!vis.length) { showNotification('⚠️ Prima premi ⚡'); return; }
stopTour();
let i = 0;
const out = document.getElementById('alignmentResult');
const step = () => {
const f = vis[i];
out.innerHTML = `<span><strong>${i + 1}/${vis.length}:</strong> ${f.type === 'Dinamica' ? `Figura Δ${f.dist}` : f.type} → ${f.numbers.join(' – ')}</span>`;
tourTimer = setTimeout(() => {
i++;
if (i >= vis.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 = []; ENGINE.firma = []; ENGINE.figDin = []; ENGINE.activeDist = null;
heatOn = false; heatMap.clear(); applyHeat(); drawFigureOverlay();
document.getElementById('engineResults').innerHTML = '';
showNotification(' Motore ripulito');
}
// ============================================================
// v2.8/v2.9: LOGICA 38 — COLONNE DI CADENZA
// Rc = ritardo corrente · Rx = MASSIMO ritardo storico
// ============================================================
let L38 = { ruota: null, num: null, cadenza: null, colonne: [], excl: [2, 2, 2], ambi: [], esclusi: [], prevHl: [], shrinkFinal: [] };
function radice(n) { let s = n; while (s > 9) s = String(s).split('').reduce((a, d) => a + +d, 0); return s; }
function novinaCadenza(c) { c = ((c % 10) + 10) % 10; const out = []; for (let n = 1; n <= 90; n++) if (n % 10 === c) out.push(n); return out; }
function colonnaIndexOf(num) { for (let r = 0; r < 3; r++) { const i = RINGS[r].numbers.indexOf(num); if (i !== -1) return { ring: r, idx: i }; } return null; }
function colonneCadenza(c) {
const m = new Map();
novinaCadenza(c).forEach(n => { const p = colonnaIndexOf(n); if (!p) return; if (!m.has(p.idx)) m.set(p.idx, {}); m.get(p.idx)[p.ring] = n; });
return [...m.entries()].sort((a, b) => a[0] - b[0]).map(([idx, o]) => ({ idx, r0: o[0], r1: o[1], r2: o[2] }));
}
function subRuota(ruota) { return archivio ? archivio.righe.filter(r => r.ruota === ruota).sort((a, b) => parseData(a.data) - parseData(b.data)) : []; }
function subRuotaL38() {
let sub = subRuota(L38.ruota);
const ds = document.getElementById('l38Date').value;
if (ds) { const d = parseData(ds); if (d) sub = sub.filter(r => parseData(r.data) <= d); }
return sub;
}
function setupL38DateRange() {
const inp = document.getElementById('l38Date');
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());
}
}
// v2.9.1: Rc/Rx CALIBRATI - NO tratto iniziale in Rx
function delayAmbo(nums, sub){
for(let i=sub.length-1; i>=0; i--){
const d=sub[i]; let c=0;
[d.n1,d.n2,d.n3,d.n4,d.n5].forEach(n=>{ if(nums.includes(n)) c++; });
if(c>=2) return sub.length-1-i;
}
return sub.length;
}
function maxGapAmbo(nums, sub){
let lastHit=-1, maxGap=0, hits=0;
for(let i=0;i<sub.length;i++){
const d=sub[i]; let c=0;
[d.n1,d.n2,d.n3,d.n4,d.n5].forEach(n=>{ if(nums.includes(n)) c++; });
if(c>=2){
if(lastHit>=0){ const g=i-lastHit-1; if(g>maxGap) maxGap=g; } // -1: il concorso i è quello di uscita
lastHit=i; hits++;
}
}
if(hits<2) return 0; // Non abbiamo abbastanza dati per calcolare max gap
return maxGap;
}
function meanGapAmbo(nums, sub){
let hits=0;
for(let i=0;i<sub.length;i++){
const d=sub[i]; let c=0;
[d.n1,d.n2,d.n3,d.n4,d.n5].forEach(n=>{ if(nums.includes(n)) c++; });
if(c>=2) hits++;
}
if(!hits) return 400.5/Math.max(1, nums.length*(nums.length-1)/2);
return sub.length/hits;
}
function delayTerna(t, sub){ return delayAmbo(t, sub); }
function runLogica38() {
const ruota = document.getElementById('l38Ruota').value;
const num = parseInt(document.getElementById('l38Num').value);
if (!num || num < 1 || num > 90) { showNotification('⚠️ Inserisci il primo estratto (1-90)'); return; }
const mode = document.getElementById('l38Mode').value;
let cad;
if (mode === 'radice') cad = radice(num) % 10;
else if (mode === 'compl') cad = (90 - num) % 10;
else cad = parseInt(document.getElementById('l38Cad').value) || 0;
L38.ruota = ruota; L38.num = num; L38.cadenza = cad;
L38.colonne = colonneCadenza(cad);
L38.excl = [2, 2, 2];
renderL38Cols();
renderL38Tracker();
showNotification(`🧭 ${CODICE_TO_NOME[ruota]} n.${num} → cadenza ${cad} · 3 colonne sul cerchio`);
}
function renderL38Cols() {
const box = document.getElementById('l38Cols');
if (!L38.colonne.length) { box.innerHTML = ''; return; }
L38.prevHl.forEach(n => highlighted.delete(n));
L38.ambi = []; L38.esclusi = [];
let h = `<div style="margin-top:8px;font-size:11px;color:#c9b98f;">Cadenza ${L38.cadenza} · 3 colonne (ambo = interno+medio, escluso = esterno, scambia):</div>`;
L38.colonne.forEach((col, ci) => {
const ex = L38.excl[ci];
const rings = [0, 1, 2].filter(r => r !== ex);
const ambo = [col['r' + rings[0]], col['r' + rings[1]]];
const escl = col['r' + ex];
L38.ambi.push(ambo); L38.esclusi.push(escl);
h += `<div class="l38-col">`;
[0, 1, 2].forEach(r => { const n = col['r' + r]; h += `<div class="l38-cell ${r === ex ? 'escl' : 'ambo'}">${n}</div>`; });
h += `<button class="l38-swap" data-col="${ci}">⇄</button></div>`;
});
h += `<div style="margin-top:4px;font-size:11px;color:#c9b98f;">Ambi secchi: <b style="color:var(--gold-fill)">${L38.ambi.map(a => a.join('-')).join(' · ')}</b> · Esclusi: <span style="color:#e0a0a0">${L38.esclusi.join(', ')}</span></div>`;
box.innerHTML = h;
L38.prevHl = L38.ambi.flat();
L38.prevHl.forEach(n => highlighted.add(n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
box.querySelectorAll('.l38-swap').forEach(b => b.addEventListener('click', () => {
const ci = +b.dataset.col;
L38.excl[ci] = (L38.excl[ci] + 1) % 3;
renderL38Cols();
}));
}
function runShrink38(){
if(L38.cadenza==null){ showNotification('⚠️ Prima imposta il cerchio (🧭)'); return; }
if(!archivio || !archivio.righe.length){ showNotification('⚠️ Carica l\'archivio'); return; }
const sub = subRuotaL38();
if(sub.length < 50){ showNotification('⚠️ Servono almeno 50 estrazioni fino alla data scelta'); return; }
let combo = novinaCadenza(L38.cadenza);
const ds = document.getElementById('l38Date').value;
let h = `<div style="margin-top:8px;font-size:11px;color:#c9b98f;">📉 Shrink lunghetta cad.${L38.cadenza} (${L38.ruota}) — classi 8→2 · al ${ds||'ultima data'}:</div>`;
for(let cls=8; cls>=2; cls--){
const opts = [];
for(let i=0;i<combo.length;i++){
const rest = combo.filter((_,j)=>j!==i);
const rc = delayAmbo(rest, sub);
const rx = maxGapAmbo(rest, sub);
opts.push({excl:combo[i], rest, rc, rx});
}
opts.sort((a,b)=>((b.rc>=b.rx)?1:0)-((a.rc>=a.rx)?1:0)||b.rc-a.rc);
h += `<div class="shrink-line"><b>--- SVILUPPO CLASSE ${cls} ---</b></div>`;
opts.slice(0,2).forEach((o,pi)=>{
h += `<div class="shrink-line">Pos.${pi+1} [Rc ${o.rc} / Rx ${o.rx}] ${o.rc>=o.rx?'<span class="ok">≥</span>':'<span class="no"><</span>'} → Escluso n° <b>${o.excl}</b> → ${o.rest.join('.')}</div>`;
});
combo = opts[0].rest;
}
L38.shrinkFinal = combo;
h += `<div style="margin-top:4px;font-size:11px;color:#c9b98f;">Esito shrink: <b style="color:var(--gold-fill)">${combo.join(' - ')}</b></div>`;
document.getElementById('l38Results').innerHTML = h;
showNotification(`📉 Shrink completato: ${combo.join('-')}`);
}
function partitions280(nums) {
const res = [];
for (let a = 1; a < 9; a++) for (let b = a + 1; b < 9; b++) {
const t1 = [nums[0], nums[a], nums[b]];
const rem = nums.filter((_, i) => i !== 0 && i !== a && i !== b);
for (let c = 1; c < rem.length; c++) for (let d = c + 1; d < rem.length; d++) {
const t2 = [rem[0], rem[c], rem[d]];
const t3 = rem.filter((_, i) => i !== 0 && i !== c && i !== d);
res.push([t1, t2, t3]);
}
}
return res;
}
function runTerzine280() {
if (L38.cadenza == null) { showNotification('⚠️ Prima imposta il cerchio (🧭)'); return; }
if (!archivio || !archivio.righe.length) { showNotification('️ Carica l\'archivio'); return; }
const sub = subRuotaL38();
if (sub.length < 50) { showNotification('⚠️ Servono almeno 50 estrazioni fino alla data scelta'); return; }
const parts = partitions280(novinaCadenza(L38.cadenza));
const rows = parts.map(g=>{
let rcSum = 0, rxSum = 0;
const seq = [];
g.forEach(t=>{
rcSum += delayTerna(t, sub);
rxSum += Math.round(meanGapAmbo(t, sub)); // Rx = attesa empirica (somma delle attese, come da Guida)
const s=[...t].sort((x,y)=>x-y);
seq.push(distanzaCiclo(s[0],s[1]), distanzaCiclo(s[1],s[2]));
});
const sim = seq.every((v,i)=>v===seq[seq.length-1-i]);
return {g, rc:rcSum, rx:Math.round(rxSum), seq, sim};
});
const filt = rows.filter(r => r.rc >= r.rx).sort((a, b) => b.rc - a.rc);
const maxRc = filt.length ? filt[0].rc : 0;
let h = `<div style="margin-top:8px;font-size:11px;color:#c9b98f;">🧩 Gruppi di 3 terzine cad.${L38.cadenza} (${L38.ruota}) · sviluppate ${rows.length} · filtrate Rc≥Rx ${filt.length} · ritardo max ${maxRc}</div>`;
filt.slice(0, 16).forEach(r => {
h += `<div class="shrink-line">${r.g.map(t => [...t].sort((a, b) => a - b).join('.')).join(' ')} · Rc ${r.rc} · Rx ${r.rx}${r.sim ? ' · <span class="ok">Simmetriche</span>' : ''} · ${r.seq.join('.')}</div>`;
});
document.getElementById('l38Results').innerHTML = h;
showNotification(`🧩 280 terzine: ${filt.length} filtrate Rc≥Rx · max Rc ${maxRc}`);
}
function saveL38() {
if (!L38.ambi.length) { showNotification('⚠️ Prima imposta il cerchio (🧭)'); return; }
const list = JSON.parse(localStorage.getItem('l38preds') || '[]');
list.push({ ruota: L38.ruota, num: L38.num, cad: L38.cadenza, ambi: L38.ambi, esclusi: L38.esclusi, shrink: L38.shrinkFinal || [], salvataIl: new Date().toISOString() });
localStorage.setItem('l38preds', JSON.stringify(list));
renderL38Tracker();
showNotification(' Previsione salvata (durata 9 concorsi)');
}
// ============================================================
// v2.10.1: RICERCA AUTOMATICA PER DOPPIE DISTANZE — LOGICA COMPLETA CORRETTA
// Le uniche terzine con una distanza ripetuta 2 volte (doppia distanza Δ)
// sono le terzine simmetriche/in progressione {x, x+Δ, x+2Δ} (mod 90).
// Rc = ritardo corrente ambo nella terzina
// Rx att. = attesa empirica (media gap) · Rx max = massimo ritardo storico CHIUSO (corretto -1)
// Filtro gruppo: Rc ≥ Rx(attesa). Monitor multi-ruota + inserimento nel Cerchio.
// ============================================================
let DIST_FOUND = [];
let DIST_RUOTE_MONITOR = [];
function calcolaDistanzeCiclometriche(numeri) {
const distanze = [];
for (let i = 0; i < numeri.length; i++) {
for (let j = i + 1; j < numeri.length; j++) {
distanze.push({ n1: numeri[i], n2: numeri[j], d: distanzaCiclo(numeri[i], numeri[j]) });
}
}
return distanze;
}
function trovaDoppieDistanze(distanze) {
const conteggio = {};
distanze.forEach(d => { conteggio[d.d] = (conteggio[d.d] || 0) + 1; });
return Object.entries(conteggio)
.filter(([d, count]) => count >= 2)
.map(([d, count]) => ({ d: parseInt(d), count }))
.sort((a, b) => b.count - a.count || a.d - b.d);
}
// Statistiche ambo in UN solo passaggio (gap chiusi corretti: g = i - lastHit - 1)
function statisticheAmbo(nums, sub) {
const S = new Set(nums);
let lastHit = -1, maxGap = 0, hits = 0;
for (let i = 0; i < sub.length; i++) {
const d = sub[i];
let c = 0;
if (S.has(d.n1)) c++;
if (S.has(d.n2)) c++;
if (S.has(d.n3)) c++;
if (S.has(d.n4)) c++;
if (S.has(d.n5)) c++;
if (c >= 2) {
if (lastHit >= 0) { const g = i - lastHit - 1; if (g > maxGap) maxGap = g; }
lastHit = i; hits++;
}
}
const rc = lastHit >= 0 ? sub.length - 1 - lastHit : sub.length;
const rxMax = hits >= 2 ? maxGap : 0;
const rxAtt = hits > 0 ? sub.length / hits : 400.5 / Math.max(1, nums.length * (nums.length - 1) / 2);
return { rc, rxMax, rxAtt: Math.round(rxAtt) };
}
// Genera le terzine simmetriche con doppia distanza Δ: {x, x+Δ, x+2Δ}
function generaTerzineDoppieDistanze(doppie) {
const set = new Map();
doppie.forEach(dd => {
for (let x = 1; x <= 90; x++) {
const t = [fuori90(x), fuori90(x + dd.d), fuori90(x + 2 * dd.d)].sort((a, b) => a - b);
if (new Set(t).size !== 3) continue; // esclude Δ=45 (numeri coincidenti)
const key = t.join('-');
if (!set.has(key)) set.set(key, { t, d: dd.d });
}
});
return [...set.values()];
}
function runRicercaDistanze() {
if (!archivio || !archivio.righe.length) { showNotification('⚠️ Carica prima l\'archivio'); return; }
const ruotaSorgente = document.getElementById('distRuotaSorgente').value;
const selTarget = document.getElementById('distRuotaTarget').value;
const inputNum = document.getElementById('distNumeriSorgente').value.trim();
if (!inputNum) { showNotification('⚠️ Inserisci i numeri dell\'estrazione sorgente'); return; }
const numeriSorgente = [...new Set(inputNum.split(/[\s,;]+/).map(Number).filter(n => n >= 1 && n <= 90))];
if (numeriSorgente.length < 3) { showNotification('⚠️ Servono almeno 3 numeri sorgente'); return; }
// 1) Distanze e doppie distanze dell'estrazione sorgente
const distanze = calcolaDistanzeCiclometriche(numeriSorgente);
const doppie = trovaDoppieDistanze(distanze);
let htmlDist = `<div class="dist-result"><div style="font-size:11px;color:#c9b98f;margin-bottom:6px;">📐 Distanze ciclometriche di ${numeriSorgente.join('-')} su ${CODICE_TO_NOME[ruotaSorgente]}:</div><div class="dist-distanze">`;
distanze.forEach(d => {
const dd = doppie.find(x => x.d === d.d);
htmlDist += `<span class="dist-tag ${dd ? 'double' : ''}" title="${d.n1}-${d.n2}">Δ${d.d}${dd ? ' ×' + dd.count : ''}</span>`;
});
htmlDist += `</div>`;
if (!doppie.length) {
htmlDist += `<div style="color:#e0a0a0;font-size:12px;margin-top:6px;">⚠️ Nessuna distanza si ripete: la ricerca richiede almeno una doppia distanza.</div></div>`;
document.getElementById('distDistanze').innerHTML = htmlDist;
document.getElementById('distResults').innerHTML = '';
DIST_FOUND = [];
return;
}
htmlDist += `<div style="font-size:11px;color:#e0b04a;margin-top:6px;">🎯 Doppie distanze target: <b>${doppie.map(d => 'Δ' + d.d).join(', ')}</b> → terzine simmetriche {x, x+Δ, x+2Δ}</div></div>`;
document.getElementById('distDistanze').innerHTML = htmlDist;
// 2) Ricerca sulle ruote selezionate (fissa o tutte)
const ruote = selTarget === 'all' ? Object.keys(RUOTE_NOME) : [selTarget];
DIST_RUOTE_MONITOR = ruote;
const candidati = generaTerzineDoppieDistanze(doppie);
const risultati = [];
ruote.forEach(rw => {
const sub = subRuota(rw);
if (sub.length < 50) return;
candidati.forEach(c => {
const s = statisticheAmbo(c.t, sub);
if (s.rc >= s.rxAtt) risultati.push({ t: c.t, d: c.d, ruota: rw, rc: s.rc, rxAtt: s.rxAtt, rxMax: s.rxMax, rapp: s.rxAtt ? s.rc / s.rxAtt : 0 });
});
});
risultati.sort((a, b) => b.rapp - a.rapp || b.rc - a.rc);
DIST_FOUND = risultati.slice(0, 60);
// 3) Render risultati + pulsanti workflow
let html = `<div class="dist-result"><div style="font-size:11px;color:#c9b98f;margin-bottom:6px;">🧩 Terzine simmetriche con doppie distanze ${doppie.map(d => 'Δ' + d.d).join(', ')} · candidate ${candidati.length} · filtro Rc ≥ Rx(attesa) · trovate <b style="color:#4fae7a;">${risultati.length}</b>${selTarget === 'all' ? ' su tutte le ruote' : ' su ' + CODICE_TO_NOME[selTarget]}</div>`;
if (!risultati.length) html += `<div class="shrink-line">Nessuna terzina soddisfa Rc ≥ Rx in questo momento</div>`;
else risultati.slice(0, 30).forEach((r, i) => {
html += `<div class="shrink-line" style="cursor:pointer;" data-tidx="${i}" title="Clicca per evidenziare sul Cerchio"><b style="color:var(--gold-fill);">${r.t.join(' · ')}</b> <span style="color:#7fd4d4;">[${r.ruota}]</span> Δ${r.d} · Rc <b>${r.rc}</b> / Rx att. <b>${r.rxAtt}</b> (max ${r.rxMax}) · <span class="ok">✓</span> ${r.rapp.toFixed(2)}</div>`;
});
html += `<div class="btn-row" style="margin-top:8px;"><button class="step" id="distToWheel">🎯 Inserisci nel Cerchio</button><button class="step" id="distSviluppa">🧩 Sviluppa a gruppi di terzine + monitor Rc≥Rx</button></div></div>`;
document.getElementById('distResults').innerHTML = html;
document.querySelectorAll('#distResults .shrink-line[data-tidx]').forEach(elx => {
elx.addEventListener('click', () => {
const r = DIST_FOUND[+elx.dataset.tidx];
if (!r) return;
r.t.forEach(n => highlighted.add(n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification(`🎯 Terzina ${r.t.join('-')} evidenziata sul Cerchio`);
});
});
const b1 = document.getElementById('distToWheel');
if (b1) b1.addEventListener('click', () => {
const uni = [...new Set(DIST_FOUND.slice(0, 12).flatMap(r => r.t))];
if (!uni.length) { showNotification('⚠️ Nessun risultato da inserire'); return; }
uni.forEach(n => highlighted.add(n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification(`🎯 ${uni.length} numeri inseriti nel Cerchio`);
});
const b2 = document.getElementById('distSviluppa');
if (b2) b2.addEventListener('click', sviluppaGruppiTerzineMonitor);
showNotification(`🔍 ${risultati.length} terzine con Rc ≥ Rx trovate`);
}
// Sviluppo dei numeri trovati a gruppi di terzine + monitor Rc ≥ Rx per ruota
function sviluppaGruppiTerzineMonitor() {
if (!DIST_FOUND.length) { showNotification('⚠️ Prima esegui una ricerca con risultati'); return; }
const ruote = DIST_RUOTE_MONITOR.length ? DIST_RUOTE_MONITOR : [document.getElementById('distRuotaTarget').value];
const uniSet = new Set();
for (const r of DIST_FOUND) {
const nuovi = r.t.filter(n => !uniSet.has(n));
if (uniSet.size + nuovi.length > 12) continue; // max 12 numeri → max 220 terzine
nuovi.forEach(n => uniSet.add(n));
}
const uni = [...uniSet].sort((a, b) => a - b);
if (uni.length < 3) { showNotification('⚠️ Servono almeno 3 numeri trovati'); return; }
const terzine = [];
for (let i = 0; i < uni.length; i++)
for (let j = i + 1; j < uni.length; j++)
for (let k = j + 1; k < uni.length; k++)
terzine.push([uni[i], uni[j], uni[k]]);
let html = `<div class="dist-result"><div style="font-size:11px;color:#c9b98f;margin-bottom:6px;">🧩 Sviluppo a gruppi di terzine dai ${uni.length} numeri trovati (${uni.join(', ')}) · ${terzine.length} terzine · monitor Rc ≥ Rx(attesa) per ruota</div>`;
let tot = 0;
ruote.forEach(rw => {
const sub = subRuota(rw);
if (sub.length < 50) return;
const ok = [];
terzine.forEach(t => {
const s = statisticheAmbo(t, sub);
if (s.rc >= s.rxAtt) ok.push({ t, rc: s.rc, rxAtt: s.rxAtt, rxMax: s.rxMax });
});
tot += ok.length;
ok.sort((a, b) => b.rc - a.rc);
html += `<div style="margin:8px 0 3px;color:#7fd4d4;font-weight:600;font-size:11px;">${CODICE_TO_NOME[rw]} (${rw}) — ${ok.length} terzine Rc ≥ Rx</div>`;
if (!ok.length) html += `<div class="shrink-line">— nessuna —</div>`;
ok.slice(0, 10).forEach(o => {
html += `<div class="shrink-line" style="cursor:pointer;" data-mon="${o.t.join(',')}" title="Clicca per evidenziare"><b style="color:var(--gold-fill);">${o.t.join(' · ')}</b> · Rc <b>${o.rc}</b> / Rx att. <b>${o.rxAtt}</b> (max ${o.rxMax})</div>`;
});
});
html += `<div style="font-size:11px;color:#c9b98f;margin-top:6px;">Totale terzine monitorate: <b>${tot}</b></div></div>`;
document.getElementById('distResults').insertAdjacentHTML('beforeend', html);
document.querySelectorAll('#distResults .shrink-line[data-mon]').forEach(elx => {
elx.addEventListener('click', () => {
elx.dataset.mon.split(',').map(Number).forEach(n => highlighted.add(n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification('🎯 Terzina evidenziata sul Cerchio');
});
});
showNotification(`🧩 Monitor avviato su ${ruote.length} ruota/e`);
}
function popolaDistRuote() {
const opts = Object.entries(RUOTE_NOME).map(([k, v]) => `<option value="${k}">${k} - ${v}</option>`).join('');
document.getElementById('distRuotaSorgente').innerHTML = opts;
document.getElementById('distRuotaTarget').innerHTML = '<option value="all">🌐 Tutte le ruote</option>' + opts;
}
function renderL38Tracker() {
const box = document.getElementById('l38Tracker');
const list = JSON.parse(localStorage.getItem('l38preds') || '[]');
const ruota = document.getElementById('l38Ruota').value;
const mine = list.map((p, i) => ({ ...p, _i: i })).filter(p => p.ruota === ruota);
if (!mine.length) { box.innerHTML = ''; return; }
let h = `<div style="margin-top:8px;font-size:11px;color:#c9b98f;display:flex;justify-content:space-between;align-items:center;">
<span>📌 Previsioni ${CODICE_TO_NOME[ruota]} (verifica ultime 9):</span>
<button class="step danger" id="l38ClearWheel" style="flex:0 0 auto;padding:3px 8px;font-size:10px;">️ Tutte</button>
</div>`;
mine.forEach(p => {
let hits = 0, played = 0;
if (archivio) {
const sub = subRuota(p.ruota);
const last = sub.slice(-9);
played = last.length;
last.forEach(d => {
const arr = [d.n1, d.n2, d.n3, d.n4, d.n5];
p.ambi.forEach(pr => { if (arr.includes(pr[0]) && arr.includes(pr[1])) hits++; });
});
}
h += `<div class="shrink-line" style="display:flex;align-items:center;gap:6px;">
<span style="flex:1;">n.${p.num} cad.${p.cad} · ${p.ambi.map(a => a.join('-')).join(', ')} · hit <b class="${hits ? 'ok' : ''}">${hits}</b>/${played}</span>
<button class="step danger l38-del" data-idx="${p._i}" style="flex:0 0 auto;padding:2px 7px;font-size:10px;">✕</button>
</div>`;
});
box.innerHTML = h;
box.querySelectorAll('.l38-del').forEach(b => b.addEventListener('click', () => {
const lst = JSON.parse(localStorage.getItem('l38preds') || '[]');
lst.splice(+b.dataset.idx, 1);
localStorage.setItem('l38preds', JSON.stringify(lst));
renderL38Tracker();
showNotification('🗑️ Previsione eliminata');
}));
const cw = box.querySelector('#l38ClearWheel');
if (cw) cw.addEventListener('click', () => {
if (!confirm(`Cancellare tutte le previsioni di ${CODICE_TO_NOME[ruota]}?`)) return;
const lst = JSON.parse(localStorage.getItem('l38preds') || '[]').filter(p => p.ruota !== ruota);
localStorage.setItem('l38preds', JSON.stringify(lst));
renderL38Tracker();
showNotification(`️ Previsioni ${CODICE_TO_NOME[ruota]} cancellate`);
});
}
// ============================================================
// v2.9.1: Verifica estrazioni per periodo
// ============================================================
function setupVerDateRange(){
const dal = document.getElementById('verDataDal');
const al = document.getElementById('verDataAl');
if(archivio && archivio.righe.length){
const dates = archivio.righe.map(r=>parseData(r.data)).filter(Boolean).sort((a,b)=>a-b);
dal.min = toISOData(dates[0]);
dal.max = toISOData(dates[dates.length-1]);
al.min = toISOData(dates[0]);
al.max = toISOData(dates[dates.length-1]);
if(!dal.value) dal.value = toISOData(dates[0]);
if(!al.value) al.value = toISOData(dates[dates.length-1]);
}
}
function verificaEstrazioniPeriodo(){
if(!archivio || !archivio.righe.length){
showNotification('⚠️ Carica l\'archivio');
return;
}
const ruota = document.getElementById('l38Ruota').value;
const dal = document.getElementById('verDataDal').value;
const al = document.getElementById('verDataAl').value;
if(!dal || !al){
showNotification('️ Inserisci entrambe le date');
return;
}
const dDal = parseData(dal);
const dAl = parseData(al);
if(dDal > dAl){
showNotification('️ La data "Dal" deve essere precedente a "Al"');
return;
}
// Filtra estrazioni della ruota nel periodo
const sub = archivio.righe.filter(r=>{
const d = parseData(r.data);
return r.ruota===ruota && d>=dDal && d<=dAl;
}).sort((a,b)=>parseData(a.data)-parseData(b.data));
const conteggio = sub.length;
const prima = sub.length ? sub[0].data : '—';
const ultima = sub.length ? sub[sub.length-1].data : '—';
let html = `<div style="padding:10px;background:rgba(79,174,122,.1);border-radius:8px;">
<div style="color:#4fae7a;font-weight:600;margin-bottom:6px;">
📊 ${CODICE_TO_NOME[ruota]} — dal ${dal} al ${al}
</div>
<div style="margin-bottom:4px;">
<b>Estrazioni totali:</b> <span style="color:var(--gold-fill);font-size:16px;">${conteggio}</span>
</div>
<div style="font-size:11px;color:#b3a37e;">
Prima: ${prima} · Ultima: ${ultima}
</div>
${conteggio<50 ? '<div style="margin-top:6px;color:#e0a0a0;font-size:11px;">️ Meno di 50 estrazioni: risultati poco significativi</div>' : ''}
</div>`;
document.getElementById('verRisultato').innerHTML = html;
showNotification(`📊 ${conteggio} estrazioni trovate`);
}
// ============================================================
// STORICO & 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(); aggiornaCadenzeDecine(); });
});
document.getElementById('convHighlight').addEventListener('click', () => {
CONVERGENCE.slice(0, 8).forEach(e => highlighted.add(e.n));
renderChips(); renderAll(); aggiornaCadenzeDecine();
showNotification('🎯 Convergenti evidenziati');
});
}
function loadHistoryDemo() {
document.getElementById('historyInput').value = `Bari 7 23 45 68 81
Napoli 12 34 56 72 90
Milano 3 18 27 44 62
Bari 15 30 60 75 88
Roma 9 27 45 54 72`;
loadHistoryFromTextarea();
}
function clearHistory() {
HISTORY.length = 0; CONVERGENCE = [];
renderHistList(); updateRuotaFilter();
document.getElementById('convResults').innerHTML = '';
}
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
// ============================================================
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 & CHIP
// ============================================================
function toggleHighlight(n) {
if (highlighted.has(n)) highlighted.delete(n); else highlighted.add(n);
renderChips(); renderAll(); aggiornaCadenzeDecine();
}
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(); aggiornaCadenzeDecine(); });
});
}
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(); aggiornaCadenzeDecine(); });
});
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(); aggiornaCadenzeDecine();
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(); aggiornaCadenzeDecine();
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(); aggiornaCadenzeDecine(); });
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(); aggiornaCadenzeDecine();
});
document.getElementById('drawClear').addEventListener('click', () => {
drawHighlighted.clear();
document.getElementById('drawInput').value = '';
document.getElementById('coerenzaBox').innerHTML = '';
renderDrawChips(); renderAll(); aggiornaCadenzeDecine();
});
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 = []; MANNA_RESULTS = []; PIRAMIDE_RESULTS = [];
L38.prevHl = []; L38.ambi = []; L38.esclusi = []; L38.colonne = []; L38.shrinkFinal = [];
document.getElementById('coerenzaBox').innerHTML = '';
document.getElementById('l38Cols').innerHTML = '';
document.getElementById('l38Results').innerHTML = '';
renderChips(); renderDrawChips(); renderAll(); renderNumeriPromettenti(); aggiornaCadenzeDecine();
renderL38Tracker();
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(); aggiornaCadenzeDecine();
showNotification('📥 Demo caricata');
});
document.getElementById('statsRun').addEventListener('click', mostraStatisticheTop10);
document.getElementById('statsTop10').addEventListener('click', mostraStatisticheTop10);
document.getElementById('statsPattern').addEventListener('click', mostraStatistichePattern);
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 = '';
});
document.getElementById('btnLoadCache').addEventListener('click', async () => {
const dati = await caricaCache();
if (dati) { updateArchiveUI(); setupBtDateRange(); setupL38DateRange(); renderL38Tracker(); 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));
document.getElementById('btRun').addEventListener('click', runBacktest);
document.getElementById('esitiRefresh').addEventListener('click', renderNumeriPromettenti);
document.getElementById('engineRun').addEventListener('click', runEngine);
document.getElementById('heatToggle').addEventListener('click', toggleHeat);
document.getElementById('tourBtn').addEventListener('click', startTour);
document.getElementById('engineClear').addEventListener('click', clearEngine);
document.getElementById('histLoad').addEventListener('click', loadHistoryFromTextarea);
document.getElementById('histDemo').addEventListener('click', loadHistoryDemo);
document.getElementById('histClear').addEventListener('click', clearHistory);
document.getElementById('convRun').addEventListener('click', runConvergence);
document.getElementById('btnCaricaRuoteConv').addEventListener('click', caricaRuoteDaArchivioPerConvergenze);
document.getElementById('findAlignmentBtn').addEventListener('click', findBestAlignments);
document.getElementById('saveConfigBtn').addEventListener('click', saveConfiguration);
document.getElementById('mannaRun').addEventListener('click', runManna);
document.getElementById('piramideRun').addEventListener('click', runPiramide);
document.getElementById('piramideClear').addEventListener('click', () => {
PIRAMIDE_RESULTS = [];
document.getElementById('piramideResults').innerHTML = '';
showNotification('🧹 Piramide ripulita');
});
document.getElementById('spieRun').addEventListener('click', runSpie);
document.getElementById('figDelayRun').addEventListener('click', runFigureDelay);
document.getElementById('l38Mode').addEventListener('change', e => {
document.getElementById('l38Cad').disabled = (e.target.value !== 'manuale');
});
document.getElementById('l38Ruota').addEventListener('change', () => renderL38Tracker());
document.getElementById('l38Run').addEventListener('click', runLogica38);
document.getElementById('l38Shrink').addEventListener('click', runShrink38);
document.getElementById('l38Terzine').addEventListener('click', runTerzine280);
document.getElementById('l38Save').addEventListener('click', saveL38);
// v2.10: Doppie Distanze
document.getElementById('distRun').addEventListener('click', runRicercaDistanze);
// v2.9.1: Verifica periodo
document.getElementById('verEstrazioni').addEventListener('click', verificaEstrazioniPeriodo);
// ============================================================
// INIZIALIZZAZIONE
// ============================================================
buildStatic();
RINGS.forEach((r, i) => buildRing(r, i));
buildCompass(ringGroups[2]);
overlayG = el('g', { id: 'figureOverlay' });
svg.appendChild(overlayG);
renderAll();
renderChips();
renderDrawChips();
aggiornaCadenzeDecine();
updateSavedList();
updateRuotaFilter();
popolaSelectRuote();
popolaBtRuote();
popolaConvArchivioRuote();
popolaDistRuote();
initTheme();
loadFromURL();
updateArchiveUI();
setupBtDateRange();
setupL38DateRange();
renderL38Tracker();
setupVerDateRange();
if (cacheEsiste()) {
setTimeout(async () => {
const dati = await caricaCache();
if (dati) { updateArchiveUI(); setupBtDateRange(); setupL38DateRange(); renderL38Tracker(); }
}, 300);
}
</script>
</body>
</html>