const state = { draws: [], window: 50, frontMax: 35, backMax: 12, ticketLimitCNY: 20000, recommended: [] }; const el = (id) => document.getElementById(id); const pad2 = (n) => String(n).padStart(2, "0"); const parseNumList = (str, max) => { if (!str) return []; return str .split(/[,,\s]+/) .filter(Boolean) .map((s) => parseInt(s, 10)) .filter((n) => Number.isInteger(n) && n >= 1 && n <= max) .filter((v, i, a) => a.indexOf(v) === i) .sort((a, b) => a - b); }; const combo = (n, k) => { if (k < 0 || k > n) return 0; if (k === 0 || k === n) return 1; k = Math.min(k, n - k); let r = 1; for (let i = 1; i <= k; i++) r = (r * (n - k + i)) / i; return Math.round(r); }; const storageKey = "dlt_draws"; const defaultApiBase = `/api`; const API_BASE = window.API_BASE || defaultApiBase; state.useServer = true; // 禁用本地存储,避免数据不一致 const saveDrawsLocal = () => {}; const loadDraws = () => {}; const apiHealth = async () => { try { const res = await fetch(`${API_BASE}/health`, { cache: "no-store" }); return res.ok; } catch (_) { return false; } }; const apiGetDraws = async () => { const res = await fetch(`${API_BASE}/draws`, { cache: "no-store" }); if (!res.ok) throw new Error(`获取服务器数据失败: ${res.status}`); const list = await res.json(); // 服务器返回 front/back 为数组或逗号字符串,统一规范为数组 const norm = (arr) => Array.isArray(arr) ? arr.map((n) => parseInt(n, 10)).filter((x) => Number.isInteger(x)) : String(arr).split(/[,,\s]+/).filter(Boolean).map((n) => parseInt(n, 10)); return list.map((r) => ({ issue: String(r.issue), date: r.date || "", front: norm(r.front).sort((a,b)=>a-b), back: norm(r.back).sort((a,b)=>a-b), pool: Number(r.pool || 0) })).sort((a,b)=> (a.issue > b.issue ? 1 : -1)); }; const apiMergeRows = async (rows) => { const payload = { rows }; const res = await fetch(`${API_BASE}/draws/merge`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!res.ok) throw new Error(`服务器入库失败: ${res.status}`); return res.json(); }; const apiClearAll = async () => { const res = await fetch(`${API_BASE}/draws/clear`, { method: "POST" }); if (!res.ok) throw new Error(`服务器清库失败: ${res.status}`); return res.json(); }; // 从服务器刷新数据并覆盖前端状态,同时回写本地存储,保持离线可用 const refreshFromServer = async () => { const ok = await apiHealth(); if (!ok) { appendLog("服务器不可用,继续使用本地存储"); state.useServer = false; return false; } state.useServer = true; const list = await apiGetDraws(); state.draws = list; saveDrawsLocal(); appendLog(`从服务器同步:总 ${state.draws.length} 期`); updateMeta(); recomputeAll(); return true; }; // 新浪页面地址(大乐透 lottId=201) // 官方历史开奖页面(大乐透) const LOTTERY_URL = "https://www.lottery.gov.cn/kj/kjlb.html?dlt"; const logElSafe = () => document.getElementById("fetchLog"); const appendLog = (msg) => { const t = new Date(); const time = [t.getHours(), t.getMinutes(), t.getSeconds()].map((x) => String(x).padStart(2, "0")).join(":"); const line = `[${time}] ${msg}`; const elLog = logElSafe(); if (elLog) { elLog.textContent += line + "\n"; elLog.scrollTop = elLog.scrollHeight; } console.log(line); }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // 带重试与候选方案的抓取:优先直接,其次多个代理;每种方案最多3次,指数退避 const fetchTextWithCors = async (url) => { const strategies = [ { name: "直接抓取", build: (u) => u }, { name: "代理(r.jina.ai:http)", build: (u) => { try { const p = new URL(u); return `https://r.jina.ai/http://${p.host}${p.pathname}${p.search}`; } catch (_) { return `https://r.jina.ai/http://${u.replace(/^https?:\/\//, "")}`; } }, }, { name: "代理(r.jina.ai:https)", build: (u) => { try { const p = new URL(u); return `https://r.jina.ai/https://${p.host}${p.pathname}${p.search}`; } catch (_) { return `https://r.jina.ai/https://${u.replace(/^https?:\/\//, "")}`; } }, }, { name: "代理(cors.isomorphic-git.org)", build: (u) => `https://cors.isomorphic-git.org/${u}` }, ]; appendLog(`开始抓取:${url}`); for (const s of strategies) { const target = s.build(url); for (let attempt = 1; attempt <= 3; attempt++) { appendLog(`${s.name} 第 ${attempt} 次尝试 -> ${target}`); try { const res = await fetch(target, { cache: "no-store" }); if (!res.ok) { appendLog(`${s.name} 响应非200,状态=${res.status}`); } else { const text = await res.text(); appendLog(`${s.name} 成功,长度=${text.length}`); return text; } } catch (e) { appendLog(`${s.name} 异常:${e && e.message ? e.message : String(e)}`); } await sleep(500 * attempt); // 退避等待 } } throw new Error("抓取失败(跨域或网络异常)"); }; // 从新浪HTML解析期号、日期与开奖号码(前区5、后区2) // 解析官网页面的开奖号码与期号 const parseLotteryGovHtmlToRows = (html) => { const issueMatches = [...html.matchAll(/20\d{5}/g)].map((m) => ({ index: m.index || 0, issue: m[0] })); const codeRegex = /((?:\d{2}[ ,,]+){4}\d{2})\s*[++]\s*((?:\d{2}[ ,,]+)\d{2})/g; const dateRegex1 = /(20\d{2}-\d{2}-\d{2})/g; // 2024-01-01 const dateRegex2 = /(20\d{2})年(\d{1,2})月(\d{1,2})日/g; // 2024年1月1日 const rows = []; let m; while ((m = codeRegex.exec(html)) !== null) { const startIdx = Math.max(0, m.index - 200); const snippet = html.slice(startIdx, m.index + 200); // 找最近的期号 let issue = null; for (let i = issueMatches.length - 1; i >= 0; i--) { if (issueMatches[i].index <= (m.index || 0)) { issue = issueMatches[i].issue; break; } } // 找附近日期 let date = null; let dm; while ((dm = dateRegex1.exec(snippet)) !== null) { date = dm[1]; } if (!date) { let dm2; while ((dm2 = dateRegex2.exec(snippet)) !== null) { const y = dm2[1]; const mo = String(dm2[2]).padStart(2, "0"); const da = String(dm2[3]).padStart(2, "0"); date = `${y}-${mo}-${da}`; } } const front = m[1].split(/[ ,,]+/).filter(Boolean).map((x) => parseInt(x, 10)).sort((a, b) => a - b); const back = m[2].split(/[ ,,]+/).filter(Boolean).map((x) => parseInt(x, 10)).sort((a, b) => a - b); if (front.length === 5 && back.length === 2 && issue) { rows.push({ issue, date: date || "", front, back, pool: 0 }); } } // 去重,按期号唯一 const seen = new Set(); const uniq = []; for (const r of rows) { if (!seen.has(r.issue)) { seen.add(r.issue); uniq.push(r); } } // 排序(期号升序) uniq.sort((a, b) => parseInt(a.issue, 10) - parseInt(b.issue, 10)); appendLog(`解析完成:匹配到 ${uniq.length} 期号码`); return uniq; }; const mergeNewRows = (rows) => { const existing = new Set(state.draws.map((d) => d.issue)); const add = rows.filter((r) => r.issue && !existing.has(r.issue)); if (!add.length) { appendLog(`增量合并:新增 0 期,现有总数 ${state.draws.length}`); return 0; } // 仅在线入库:先写服务器,再刷新内存 (async () => { try { const r = await apiMergeRows(add); const inserted = r.inserted ?? r.added ?? add.length; appendLog(`服务器入库:新增 ${inserted} 期`); await refreshFromServer(); updateMeta(); recomputeAll(); } catch (e) { appendLog(`服务器入库失败:${e && e.message ? e.message : String(e)}`); } })(); return add.length; }; const parseCSV = (text) => { const lines = text.trim().split(/\r?\n/); const rows = []; for (let i = 0; i < lines.length; i++) { const raw = lines[i].trim(); if (!raw) continue; const parts = []; let cur = ""; let inQuote = false; for (let c of raw) { if (c === '"') { inQuote = !inQuote; continue; } if (c === "," && !inQuote) { parts.push(cur); cur = ""; } else cur += c; } parts.push(cur); if (parts.length < 5) continue; const issue = parts[0].trim(); const date = parts[1].trim(); const front = parts[2].trim(); const back = parts[3].trim(); const pool = parts[4].trim(); const frontArr = front.split(/[\s,,]+/).filter(Boolean).map((x) => parseInt(x, 10)); const backArr = back.split(/[\s,,]+/).filter(Boolean).map((x) => parseInt(x, 10)); if (frontArr.length === 5 && backArr.length === 2) { rows.push({ issue, date, front: frontArr, back: backArr, pool: Number(pool) || 0 }); } } rows.sort((a, b) => (a.issue > b.issue ? 1 : -1)); return rows; }; const computeFreq = (draws, window, range) => { const freq = Array(range).fill(0); const start = Math.max(0, draws.length - window); for (let i = start; i < draws.length; i++) { const d = draws[i]; const arr = range === state.frontMax ? d.front : d.back; for (let n of arr) freq[n - 1]++; } return freq; }; const computeMiss = (draws, window, range) => { const miss = Array(range).fill(0); const start = Math.max(0, draws.length - window); for (let n = 1; n <= range; n++) { let lastSeen = -1; for (let i = draws.length - 1; i >= start; i--) { const arr = range === state.frontMax ? draws[i].front : draws[i].back; if (arr.includes(n)) { lastSeen = i; break; } } miss[n - 1] = lastSeen === -1 ? (draws.length - start) : (draws.length - 1 - lastSeen); } return miss; }; const computeSummary = (draws, window, frontThr, backThr) => { const start = Math.max(0, draws.length - window); let frontSum = 0, backSum = 0; let frontOdd = 0, frontEven = 0, frontBig = 0, frontSmall = 0; let backOdd = 0, backEven = 0, backBig = 0, backSmall = 0; const frontRegion = [0, 0, 0]; const backRegion = [0, 0]; for (let i = start; i < draws.length; i++) { const d = draws[i]; frontSum += d.front.reduce((a, b) => a + b, 0); backSum += d.back.reduce((a, b) => a + b, 0); for (let n of d.front) { if (n % 2) frontOdd++; else frontEven++; if (n > frontThr) frontBig++; else frontSmall++; if (n <= 12) frontRegion[0]++; else if (n <= 24) frontRegion[1]++; else frontRegion[2]++; } for (let n of d.back) { if (n % 2) backOdd++; else backEven++; if (n > backThr) backBig++; else backSmall++; if (n <= 6) backRegion[0]++; else backRegion[1]++; } } const cnt = Math.max(1, draws.length - start); return { frontAvgSum: (frontSum / cnt).toFixed(2), backAvgSum: (backSum / cnt).toFixed(2), frontOdd, frontEven, frontBig, frontSmall, backOdd, backEven, backBig, backSmall, frontRegion, backRegion, }; }; const renderBars = (containerId, freq) => { const c = el(containerId); c.innerHTML = ""; const max = Math.max(1, ...freq); freq.forEach((f, idx) => { const row = document.createElement("div"); row.className = "bar-row"; const label = document.createElement("div"); label.className = "bar-label"; label.textContent = pad2(idx + 1); const bar = document.createElement("div"); bar.className = "bar"; const fill = document.createElement("div"); fill.className = "bar-fill"; fill.style.width = `${(f / max) * 100}%`; const val = document.createElement("div"); val.className = "bar-value"; val.textContent = String(f); bar.appendChild(fill); row.appendChild(label); row.appendChild(bar); row.appendChild(val); c.appendChild(row); }); }; const renderMiss = (containerId, miss) => { const c = el(containerId); c.innerHTML = ""; miss.forEach((m, idx) => { const item = document.createElement("div"); item.className = "grid-item"; const n = document.createElement("span"); n.className = "n"; n.textContent = pad2(idx + 1); const v = document.createElement("span"); v.className = "v"; v.textContent = String(m); item.appendChild(n); item.appendChild(v); c.appendChild(item); }); }; const renderSummary = (sum) => { const c = el("summaryStats"); c.innerHTML = ""; const add = (title, content) => { const d = document.createElement("div"); d.className = "summary-item"; const h5 = document.createElement("h5"); h5.textContent = title; const s = document.createElement("div"); s.className = "content"; s.textContent = content; d.appendChild(h5); d.appendChild(s); c.appendChild(d); }; add("前区平均和值", String(sum.frontAvgSum)); add("后区平均和值", String(sum.backAvgSum)); add("前区奇偶", `${sum.frontOdd} 奇 / ${sum.frontEven} 偶`); add("前区大小", `${sum.frontBig} 大 / ${sum.frontSmall} 小`); add("后区奇偶", `${sum.backOdd} 奇 / ${sum.backEven} 偶`); add("后区大小", `${sum.backBig} 大 / ${sum.backSmall} 小`); add("前区区间分布", `1-12:${sum.frontRegion[0]} 13-24:${sum.frontRegion[1]} 25-35:${sum.frontRegion[2]}`); add("后区区间分布", `1-6:${sum.backRegion[0]} 7-12:${sum.backRegion[1]}`); }; const updateMeta = () => { el("drawCount").textContent = String(state.draws.length); el("latestIssue").textContent = state.draws.length ? state.draws[state.draws.length - 1].issue : "-"; }; const recomputeAll = () => { const w = state.window; const fFreq = computeFreq(state.draws, w, state.frontMax); const bFreq = computeFreq(state.draws, w, state.backMax); const fMiss = computeMiss(state.draws, w, state.frontMax); const bMiss = computeMiss(state.draws, w, state.backMax); renderBars("frontFreq", fFreq); renderBars("backFreq", bFreq); renderMiss("frontMiss", fMiss); renderMiss("backMiss", bMiss); const sum = computeSummary(state.draws, w, Number(el("frontSizeThreshold").value), Number(el("backSizeThreshold").value)); renderSummary(sum); }; const switchTab = (target) => { document.querySelectorAll(".tab-btn").forEach((b) => b.classList.toggle("active", b.dataset.target === target)); document.querySelectorAll(".tab-panel").forEach((p) => p.classList.toggle("hidden", p.id !== target)); }; document.querySelectorAll(".tab-btn").forEach((b) => b.addEventListener("click", () => switchTab(b.dataset.target))); el("btnImport").addEventListener("click", async () => { const file = el("csvFile").files[0]; if (!file) return alert("请选择CSV文件"); const text = await file.text(); const rows = parseCSV(text); if (!rows.length) return alert("CSV内容无有效数据(需5前区+2后区)"); try { const r = await apiMergeRows(rows); appendLog(`服务器入库:新增 ${r.inserted ?? r.added ?? rows.length} 期`); await refreshFromServer(); updateMeta(); recomputeAll(); } catch (e) { alert(`导入失败:${e && e.message ? e.message : String(e)}`); } }); el("btnClear").addEventListener("click", async () => { if (!confirm("确认清空服务器历史数据?此操作不可恢复!")) return; try { const r = await apiClearAll(); appendLog(`服务器清库完成`); await refreshFromServer(); updateMeta(); el("frontFreq").innerHTML = ""; el("backFreq").innerHTML = ""; el("frontMiss").innerHTML = ""; el("backMiss").innerHTML = ""; el("summaryStats").innerHTML = ""; } catch (e) { alert(`清库失败:${e && e.message ? e.message : String(e)}`); } }); el("btnTemplate").addEventListener("click", () => { const csv = [ "期号,开奖日期,前区,后区,奖池资金", "2024001,2024-01-01,01 08 12 23 31,03 09,100000000", "2024002,2024-01-03,02 09 11 28 35,01 06,100500000" ].join("\n"); const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8;" })); a.download = "大乐透数据模板.csv"; a.click(); }); // 导出历史开奖数据为CSV el("btnExportDraws").addEventListener("click", () => { if (!state.draws.length) return alert("暂无历史数据可导出"); const rows = ["期号,开奖日期,前区,后区,奖池资金"]; // 与模板一致 state.draws.forEach((d) => { const front = d.front.map(pad2).join(" "); const back = d.back.map(pad2).join(" "); rows.push(`${d.issue},${d.date || ""},"${front}","${back}",${d.pool || 0}`); }); const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8;" })); a.download = `大乐透历史数据_${state.draws.length}期.csv`; a.click(); }); // 官网抓取按钮:抓取、解析并增量入库(不清空) el("btnFetchSina").addEventListener("click", async () => { const btn = el("btnFetchSina"); const prev = btn.textContent; btn.disabled = true; btn.textContent = "抓取中..."; try { const html = await fetchTextWithCors(LOTTERY_URL); const rows = parseLotteryGovHtmlToRows(html); if (!rows.length) { alert("解析失败:未发现有效的开奖号码格式"); return; } const added = mergeNewRows(rows); updateMeta(); recomputeAll(); alert(added ? `新增 ${added} 期数据,已合并到本地。` : "无新数据需要更新。"); } catch (e) { appendLog(`抓取失败:${e && e.message ? e.message : String(e)}`); alert(e.message || "抓取失败"); } finally { btn.disabled = false; btn.textContent = prev; } }); // 备用:解析粘贴的官网源码并入库 el("btnParseHtml").addEventListener("click", () => { const text = el("htmlPaste").value.trim(); if (!text) return alert("请先粘贴官网页面源代码"); appendLog("开始解析粘贴的源码..."); const rows = parseLotteryGovHtmlToRows(text); if (!rows.length) { alert("解析失败:未发现有效的开奖号码格式"); return; } const added = mergeNewRows(rows); updateMeta(); recomputeAll(); alert(added ? `新增 ${added} 期数据,已合并到本地。` : "无新数据需要更新。"); }); // 解析纯文本行:格式为 期号\tYYYY-MM-DD\t前区(5个)\t后区(2个)\t其它列... const parsePlainLinesToRows = (text) => { const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); const rows = []; for (const line of lines) { // 抓取期号与日期 const md = line.match(/(\d{5,6})\s+(20\d{2}-\d{2}-\d{2})/); if (!md) continue; const issue = md[1]; const date = md[2]; const tail = line.slice(md.index + md[0].length).trim(); // 抓取两位数号码,从左到右,优先填前区5个(1-35),再填后区2个(1-12) const tokens = (tail.match(/\b\d{2}\b/g) || []).map((x) => parseInt(x, 10)); const front = []; const back = []; for (const n of tokens) { if (front.length < 5 && n >= 1 && n <= 35 && !front.includes(n)) { front.push(n); continue; } if (front.length === 5 && back.length < 2 && n >= 1 && n <= 12 && !back.includes(n)) { back.push(n); continue; } } if (front.length === 5 && back.length === 2) { rows.push({ issue, date, front: front.sort((a,b)=>a-b), back: back.sort((a,b)=>a-b), pool: 0 }); } } appendLog(`纯文本解析完成:匹配到 ${rows.length} 期号码`); return rows; }; // 解析纯文本按钮 el("btnParsePlain").addEventListener("click", () => { const text = el("htmlPaste").value.trim(); if (!text) return alert("请先粘贴纯文本内容"); appendLog("开始解析纯文本行..."); const rows = parsePlainLinesToRows(text); if (!rows.length) { alert("解析失败:未发现有效的开奖号码格式"); return; } const added = mergeNewRows(rows); updateMeta(); recomputeAll(); alert(added ? `新增 ${added} 期数据,已合并到本地。` : "无新数据需要更新。"); }); // 日志清空 el("btnLogClear").addEventListener("click", () => { const elLog = logElSafe(); if (elLog) elLog.textContent = ""; }); el("periodWindow").addEventListener("change", () => { state.window = Math.max(1, Number(el("periodWindow").value)); }); el("btnRefresh").addEventListener("click", () => recomputeAll()); el("btnRecompute").addEventListener("click", () => recomputeAll()); el("hotPref").addEventListener("input", () => el("hotPrefVal").textContent = el("hotPref").value); el("coldPref").addEventListener("input", () => el("coldPrefVal").textContent = el("coldPref").value); const randomPick = () => { const front = new Set(); while (front.size < 5) front.add(1 + Math.floor(Math.random() * state.frontMax)); const back = new Set(); while (back.size < 2) back.add(1 + Math.floor(Math.random() * state.backMax)); const fa = [...front].sort((a, b) => a - b); const ba = [...back].sort((a, b) => a - b); return { front: fa, back: ba }; }; const weightedPick = (weights, count) => { const picks = new Set(); const entries = weights.map((w, i) => ({ n: i + 1, w })); const total = entries.reduce((a, b) => a + b.w, 0) || 1; while (picks.size < count) { let r = Math.random() * total; for (let e of entries) { r -= e.w; if (r <= 0) { picks.add(e.n); break; } } if (picks.size > count) break; } return [...picks].sort((a, b) => a - b); }; const makeSmart = () => { const w = state.window; const frontFreq = computeFreq(state.draws, w, state.frontMax); const backFreq = computeFreq(state.draws, w, state.backMax); const maxF = Math.max(1, ...frontFreq), maxB = Math.max(1, ...backFreq); const hot = Number(el("hotPref").value), cold = Number(el("coldPref").value); const fw = frontFreq.map((f) => (hot * (f / maxF) + cold * (1 - f / maxF)) / 100 + 0.01); const bw = backFreq.map((f) => (hot * (f / maxB) + cold * (1 - f / maxB)) / 100 + 0.01); let front = weightedPick(fw, 5); const [po, pe] = el("parityFront").value.split("-").map(Number); const frontThr = Number(el("frontSizeThreshold").value); const [pb, ps] = el("sizeFront").value.split("-").map(Number); for (let tries = 0; tries < 50; tries++) { const odd = front.filter((n) => n % 2).length; const big = front.filter((n) => n > frontThr).length; if (odd === po && big === pb) break; front = weightedPick(fw, 5); } const back = weightedPick(bw, 2); return { front, back }; }; // ------------------------------ AI快速五组策略 ------------------------------ const hasConsecutive = (arr) => { const s = new Set(arr); return arr.some((n) => s.has(n + 1) || s.has(n - 1)); }; const hasPairSum35 = (arr) => { for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { if (arr[i] + arr[j] === 35) return true; } } return false; }; const hasUnitsSum10 = (arr) => { const u = arr.map((n) => n % 10); for (let i = 0; i < u.length; i++) { for (let j = i + 1; j < u.length; j++) { if ((u[i] + u[j]) % 10 === 0) return true; } } return false; }; const ensureFrontBalance = (front) => { const [po] = el("parityFront").value.split("-").map(Number); const frontThr = Number(el("frontSizeThreshold").value); const [pb] = el("sizeFront").value.split("-").map(Number); let tries = 0; while (tries++ < 150) { const odd = front.filter((n) => n % 2).length; const big = front.filter((n) => n > frontThr).length; if (odd === po && big === pb) break; const poolOdd = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n % 2 === 1); const poolEven = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n % 2 === 0); const poolBig = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n > frontThr); const poolSmall = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n <= frontThr); const idx = Math.floor(Math.random() * front.length); const s = new Set(front); if (odd !== po) { const candidatePool = odd < po ? poolOdd : poolEven; const c = candidatePool.find((n) => !s.has(n)); if (c) front[idx] = c; } if (big !== pb) { const candidatePool = big < pb ? poolBig : poolSmall; const s2 = new Set(front); const c = candidatePool.find((n) => !s2.has(n)); if (c) front[idx] = c; } front = Array.from(new Set(front)).sort((a, b) => a - b); if (front.length < 5) { const all = Array.from({ length: state.frontMax }, (_, i) => i + 1); while (front.length < 5) { const nn = all[Math.floor(Math.random() * all.length)]; if (!front.includes(nn)) front.push(nn); } front.sort((a, b) => a - b); } } return front; }; const ensureBackBalance = (back) => { const backThr = Number(el("backSizeThreshold").value); const targetBig = 1; // 尽量一大一小 let tries = 0; while (tries++ < 60) { const big = back.filter((n) => n > backThr).length; if (big === targetBig) break; const poolBig = Array.from({ length: state.backMax }, (_, i) => i + 1).filter((n) => n > backThr); const poolSmall = Array.from({ length: state.backMax }, (_, i) => i + 1).filter((n) => n <= backThr); const idx = Math.floor(Math.random() * back.length); const s = new Set(back); const candidatePool = big < targetBig ? poolBig : poolSmall; const c = candidatePool.find((n) => !s.has(n)); if (c) back[idx] = c; back = Array.from(new Set(back)).sort((a, b) => a - b); if (back.length < 2) { const all = Array.from({ length: state.backMax }, (_, i) => i + 1); while (back.length < 2) { const nn = all[Math.floor(Math.random() * all.length)]; if (!back.includes(nn)) back.push(nn); } back.sort((a, b) => a - b); } } return back; }; const topOrder = (range, weights, desc = true) => { return Array.from({ length: range }, (_, i) => i + 1) .sort((a, b) => (desc ? (weights[b - 1] || 0) - (weights[a - 1] || 0) : (weights[a - 1] || 0) - (weights[b - 1] || 0))); }; const buildCandidateWeights = (range, baseWeights, candidates, fallback = 0) => { const s = new Set(candidates); return Array.from({ length: range }, (_, i) => { const n = i + 1; return s.has(n) ? (baseWeights[i] || 0) + 0.01 : fallback; }); }; // ------------------------------ ChatGPT 算法工具函数 ------------------------------ const laplaceWeights = (freq, alpha = 1) => { const total = freq.reduce((a, b) => a + b, 0); const range = freq.length; return freq.map((f) => (f + alpha) / (total + range * alpha)); }; const applyTemperature = (probs, T = 1) => { const t = Math.max(1e-6, T); return probs.map((p) => Math.pow(Math.max(1e-6, p), 1 / t) + 0.0001); }; const topKFromWeights = (weights, k) => { return Array.from({ length: weights.length }, (_, i) => i + 1) .sort((a, b) => (weights[b - 1] || 0) - (weights[a - 1] || 0)) .slice(0, Math.max(1, k)); }; const buildCooccMatrix = (draws, range, useFront) => { const C = Array.from({ length: range }, () => Array(range).fill(0)); for (const d of draws) { const arr = useFront ? d.front : d.back; for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) { const a = arr[i] - 1, b = arr[j] - 1; C[a][b]++; C[b][a]++; } } } return C; }; const buildTransitionMatrix = (draws, range, useFront) => { const T = Array.from({ length: range }, () => Array(range).fill(0)); for (let t = 0; t + 1 < draws.length; t++) { const cur = useFront ? draws[t].front : draws[t].back; const nxt = useFront ? draws[t + 1].front : draws[t + 1].back; const s = new Set(cur); for (const j of nxt) { for (const i of s) { T[i - 1][j - 1]++; } } } return T; }; const normalizeRow = (M) => M.map((row) => { const s = row.reduce((a, b) => a + b, 0) || 1; return row.map((x) => x / s); }); const mmrSelect = (baseScores, cooccNorm, count, lambda = 0.4) => { const selected = []; const used = new Set(); const cand = Array.from({ length: baseScores.length }, (_, i) => i + 1); while (selected.length < count) { let best = null, bestScore = -Infinity; for (const n of cand) { if (used.has(n)) continue; let div = 0; for (const s of selected) div += cooccNorm[n - 1][s - 1] || 0; const score = (baseScores[n - 1] || 0) - lambda * div; if (score > bestScore) { bestScore = score; best = n; } } if (best == null) break; selected.push(best); used.add(best); } return selected.sort((a, b) => a - b); }; const average = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length); const buildCondProb = (draws, range, useFront, alpha = 1) => { const cond = Array.from({ length: range }, () => Array(range).fill(0)); const countJ = Array(range).fill(0); for (const d of draws) { const arr = useFront ? d.front : d.back; const s = new Set(arr); for (const j of s) { countJ[j - 1]++; for (const i of s) cond[j - 1][i - 1]++; } } for (let j = 0; j < range; j++) { const denom = countJ[j] + range * alpha; for (let i = 0; i < range; i++) cond[j][i] = (cond[j][i] + alpha) / denom; } return cond; }; const gaOptimizeFront = (weights, fitnessFn, range, popSize = 80, gens = 40, mutRate = 0.08) => { const rndSet = () => weightedPick(weights, 5); let pop = Array.from({ length: popSize }, rndSet); const score = (s) => fitnessFn(s); for (let g = 0; g < gens; g++) { const scored = pop.map((s) => ({ s, f: score(s.slice().sort((a, b) => a - b)) })) .sort((a, b) => b.f - a.f); const elite = scored.slice(0, Math.floor(popSize / 2)).map((x) => x.s); const children = []; while (children.length + elite.length < popSize) { const a = elite[Math.floor(Math.random() * elite.length)]; const b = elite[Math.floor(Math.random() * elite.length)]; const cut = 2 + Math.floor(Math.random() * 2); const child = Array.from(new Set([...a.slice(0, cut), ...b.slice(cut)])).slice(0, 5); while (child.length < 5) { const n = 1 + Math.floor(Math.random() * range); if (!child.includes(n)) child.push(n); } children.push(child.sort((x, y) => x - y)); } const mutate = (s) => { if (Math.random() < mutRate) { const idx = Math.floor(Math.random() * 5); let n = 1 + Math.floor(Math.random() * range); while (s.includes(n)) n = 1 + Math.floor(Math.random() * range); s[idx] = n; s.sort((a, b) => a - b); } return s; }; pop = [...elite, ...children].map(mutate); } return pop.sort((a, b) => fitnessFn(b) - fitnessFn(a))[0]; }; // ------------------------------ QuickAI 配置与指标计算 ------------------------------ const quickAIConfig = { hotTopPercent: 0.3, // 热号池:加权频次前30% decay: 0.92, // 近期权重衰减(越近权重越高) coldMissMultiplier: 1.5, // 冷号阈值:当前遗漏 ≥ 平均遗漏 × 倍数 ratioFront: [3, 2], // 冷热均衡:前区热/冷比例 ratioBack: [1, 1], // 冷热均衡:后区热/冷比例 regionsFront: [ // 区间对称:每个区间目标数量 { from: 1, to: 12, count: 2 }, { from: 13, to: 24, count: 2 }, { from: 25, to: 35, count: 1 }, ], sumRange: [80, 140], // 和值优选区间 spanRange: [20, 30], // 跨度优选区间 primePrefer: [2, 3], // 质合比(质数个数优选值,配合5-质) acRange: [4, 8], // AC值优选区间 }; const computeWeightedFreq = (draws, range, window, decay) => { const freq = Array(range).fill(0); const start = Math.max(0, draws.length - window); const end = draws.length - 1; for (let i = start; i <= end; i++) { const weight = Math.pow(decay, end - i); const arr = range === state.frontMax ? draws[i].front : draws[i].back; for (let n of arr) freq[n - 1] += weight; } return freq; }; // 当前遗漏、最大遗漏、平均遗漏 const computeMissStats = (draws, range, window) => { const current = Array(range).fill(0); const maxMiss = Array(range).fill(0); const start = Math.max(0, draws.length - window); const seenIndices = Array.from({ length: range }, () => []); for (let i = start; i < draws.length; i++) { const f = range === state.frontMax ? draws[i].front : draws[i].back; for (let n of f) seenIndices[n - 1].push(i); } for (let n = 1; n <= range; n++) { const idxs = seenIndices[n - 1]; // 当前遗漏:距最近一次出现的期数差 const lastSeen = idxs.length ? idxs[idxs.length - 1] : -1; current[n - 1] = lastSeen === -1 ? (draws.length - start) : (draws.length - 1 - lastSeen); // 最大遗漏:相邻两次出现之间的最大间隔(含头尾) let prev = start - 1, mm = 0; for (let k = 0; k < idxs.length; k++) { mm = Math.max(mm, idxs[k] - prev - 1); prev = idxs[k]; } mm = Math.max(mm, (draws.length - 1) - prev); // 尾段 maxMiss[n - 1] = mm; } const avgMiss = current.reduce((a, b) => a + b, 0) / range; return { current, maxMiss, avgMiss }; }; const isPrime = (n) => { if (n < 2) return false; for (let i = 2; i * i <= n; i++) if (n % i === 0) return false; return true; }; const acValue = (arr) => { const s = new Set(); for (let i = 0; i < arr.length; i++) { for (let j = i + 1; j < arr.length; j++) s.add(arr[j] - arr[i]); } return s.size - (arr.length - 1); }; const scoreRange = (val, min, max) => { if (val >= min && val <= max) return 1; const d = val < min ? (min - val) : (val - max); return Math.max(0, 1 - d / Math.max(1, (max - min))); }; const metricsScore = (front, cfg) => { const sum = front.reduce((a, b) => a + b, 0); const span = front[front.length - 1] - front[0]; const odd = front.filter((n) => n % 2).length; const prime = front.filter(isPrime).length; const ac = acValue(front); const sSum = scoreRange(sum, cfg.sumRange[0], cfg.sumRange[1]); const sSpan = scoreRange(span, cfg.spanRange[0], cfg.spanRange[1]); const sOdd = 1 - Math.min(1, Math.abs(odd - (el("parityFront").value.split("-").map(Number)[0])) / 2); const sPrime = 1 - Math.min(1, Math.abs(prime - cfg.primePrefer[0]) / 2); const sAc = scoreRange(ac, cfg.acRange[0], cfg.acRange[1]); return 0.3 * sSum + 0.25 * sSpan + 0.15 * sOdd + 0.15 * sPrime + 0.15 * sAc; }; const generateQuickAI = () => { const w = state.window; const cfg = quickAIConfig; // 频次(普通)与加权频次(近期越大) const fFreq = computeFreq(state.draws, w, state.frontMax); const bFreq = computeFreq(state.draws, w, state.backMax); const fWFreq = computeWeightedFreq(state.draws, state.frontMax, w, cfg.decay); const bWFreq = computeWeightedFreq(state.draws, state.backMax, w, cfg.decay); // 遗漏统计 const fMissStats = computeMissStats(state.draws, state.frontMax, w); const bMissStats = computeMissStats(state.draws, state.backMax, w); // 热号池:加权频次前30% const hotCountF = Math.max(5, Math.ceil(state.frontMax * cfg.hotTopPercent)); const hotCountB = Math.max(4, Math.ceil(state.backMax * cfg.hotTopPercent)); const hotF = Array.from({ length: state.frontMax }, (_, i) => i + 1) .sort((a, b) => (fWFreq[b - 1] || 0) - (fWFreq[a - 1] || 0)) .slice(0, hotCountF); const hotB = Array.from({ length: state.backMax }, (_, i) => i + 1) .sort((a, b) => (bWFreq[b - 1] || 0) - (bWFreq[a - 1] || 0)) .slice(0, hotCountB); // 冷号池:当前遗漏 ≥ 平均遗漏 × 倍数;不足则按当前遗漏排序补齐 const coldFAll = Array.from({ length: state.frontMax }, (_, i) => i + 1) .filter((n) => fMissStats.current[n - 1] >= cfg.coldMissMultiplier * fMissStats.avgMiss) .sort((a, b) => fMissStats.current[b - 1] - fMissStats.current[a - 1]); const coldBAll = Array.from({ length: state.backMax }, (_, i) => i + 1) .filter((n) => bMissStats.current[n - 1] >= cfg.coldMissMultiplier * bMissStats.avgMiss) .sort((a, b) => bMissStats.current[b - 1] - bMissStats.current[a - 1]); const coldF = coldFAll.length ? coldFAll : Array.from({ length: state.frontMax }, (_, i) => i + 1).sort((a, b) => fMissStats.current[b - 1] - fMissStats.current[a - 1]).slice(0, 12); const coldB = coldBAll.length ? coldBAll : Array.from({ length: state.backMax }, (_, i) => i + 1).sort((a, b) => bMissStats.current[b - 1] - bMissStats.current[a - 1]).slice(0, 6); const last = state.draws[state.draws.length - 1] || { front: [], back: [] }; const sets = []; // 组合一:热号主导型 { const fcand = hotF.slice(0, 14); const fw = buildCandidateWeights(state.frontMax, fWFreq, fcand, 0); // 使用加权频次 let front = weightedPick(fw, 5); front = ensureFrontBalance(front); const bcand = hotB.slice(0, 6); const bw = buildCandidateWeights(state.backMax, bWFreq, bcand, 0); let back = weightedPick(bw, 2); back = ensureBackBalance(back); sets.push({ front, back, name: "热号主导" }); } // 组合二:冷热均衡型(3热 + 2冷;后区 1热 + 1冷) { const [hF, cF] = cfg.ratioFront; const [hB, cB] = cfg.ratioBack; const hotCand = hotF.slice(0, 18); const coldCand = coldF.slice(0, 20); const fwHot = buildCandidateWeights(state.frontMax, fWFreq, hotCand, 0); const fwCold = buildCandidateWeights(state.frontMax, fMissStats.current.map((x) => x + 0.01), coldCand, 0); // 按当前遗漏加权 const hot3 = weightedPick(fwHot, hF); const exclude = new Set(hot3); // 将非候选权重设为0,并排除已选 const fwColdEx = fwCold.map((w, i) => (exclude.has(i + 1) ? 0 : w)); const cold2 = weightedPick(fwColdEx, cF); let front = Array.from(new Set([...hot3, ...cold2])).sort((a, b) => a - b); if (front.length < 5) { const fillW = buildCandidateWeights(state.frontMax, fWFreq, hotF.slice(0, 35), 0.01); while (front.length < 5) { const pick = weightedPick(fillW, 1)[0]; if (!front.includes(pick)) front.push(pick); } front.sort((a, b) => a - b); } front = ensureFrontBalance(front); const bHot1 = weightedPick(buildCandidateWeights(state.backMax, bWFreq, hotB.slice(0, 6), 0), hB); const exB = new Set(bHot1); const bCold1 = weightedPick(buildCandidateWeights(state.backMax, bMissStats.current.map((x) => x + 0.01), coldB.slice(0, 6), 0).map((w, i) => (exB.has(i + 1) ? 0 : w)), cB); let back = Array.from(new Set([...bHot1, ...bCold1])).sort((a, b) => a - b); back = ensureBackBalance(back); sets.push({ front, back, name: "冷热均衡" }); } // 组合三:区间对称型(01-12, 13-24, 25-35 -> 2/2/1),并尝试“对应数”关系 { const r1 = Array.from({ length: 12 }, (_, i) => i + 1); const r2 = Array.from({ length: 12 }, (_, i) => i + 13); const r3 = Array.from({ length: 11 }, (_, i) => i + 25); const fwr1 = buildCandidateWeights(state.frontMax, fWFreq, r1, 0); const fwr2 = buildCandidateWeights(state.frontMax, fWFreq, r2, 0); const fwr3 = buildCandidateWeights(state.frontMax, fWFreq, r3, 0); const p1 = weightedPick(fwr1, cfg.regionsFront[0].count); const ex12 = new Set(p1); const fwr2ex = fwr2.map((w, i) => (ex12.has(i + 1) ? 0 : w)); const p2 = weightedPick(fwr2ex, cfg.regionsFront[1].count); const ex1234 = new Set([...p1, ...p2]); const fwr3ex = fwr3.map((w, i) => (ex1234.has(i + 1) ? 0 : w)); let front = Array.from(new Set([...p1, ...p2, ...weightedPick(fwr3ex, cfg.regionsFront[2].count)])).sort((a, b) => a - b); // 若没有对应数(和35或尾数和10),尝试补充 if (!hasPairSum35(front) && !hasUnitsSum10(front)) { const base = front[0]; const complement = 35 - base; if (complement >= 1 && complement <= state.frontMax && !front.includes(complement)) { front[front.length - 1] = complement; front.sort((a, b) => a - b); } } front = ensureFrontBalance(front); let back = weightedPick(buildCandidateWeights(state.backMax, bWFreq, Array.from({ length: state.backMax }, (_, i) => i + 1), 0.01), 2); back = ensureBackBalance(back); sets.push({ front, back, name: "区间对称" }); } // 组合四:连号重号关注型(包含一对连号 + 上期重号) { const selected = new Set(); const lf = (last.front || []); if (lf.length) { selected.add(lf[Math.floor(Math.random() * lf.length)]); } const anchor = hotF[0] || 1; selected.add(anchor); if (anchor + 1 <= state.frontMax) selected.add(anchor + 1); const fillW = buildCandidateWeights(state.frontMax, fWFreq, hotF.slice(0, 20), 0); const fillWEx = fillW.map((w, i) => (selected.has(i + 1) ? 0 : w)); while (selected.size < 5) { const p = weightedPick(fillWEx, 1)[0]; selected.add(p); } let front = Array.from(selected).sort((a, b) => a - b); if (!hasConsecutive(front)) { const c = front[0]; const alt = c + 1 <= state.frontMax && !front.includes(c + 1) ? c + 1 : (c - 1 >= 1 ? c - 1 : c); front[front.length - 1] = alt; front = Array.from(new Set(front)).sort((a, b) => a - b); } front = ensureFrontBalance(front); let back = []; const lb = (last.back || []); if (lb.length) back.push(lb[Math.floor(Math.random() * lb.length)]); const bwFill = buildCandidateWeights(state.backMax, bWFreq, hotB.slice(0, 6), 0); const bwFillEx = bwFill.map((w, i) => (back.includes(i + 1) ? 0 : w)); while (back.length < 2) { const p = weightedPick(bwFillEx, 1)[0]; if (!back.includes(p)) back.push(p); } back.sort((a, b) => a - b); back = ensureBackBalance(back); sets.push({ front, back, name: "连号重号" }); } // 组合五:综合数据分析型(多指标打分) { // 候选池:加权热号前列 + 少量冷号 const topF = hotF.slice(0, 18); const mixF = Array.from(new Set([...topF, ...coldF.slice(0, 10)])).sort((a, b) => a - b); const fwMix = buildCandidateWeights(state.frontMax, fWFreq.map((x) => x + 0.01), mixF, 0.005); // 采样生成候选组合并按多指标评分选择最优 let bestFront = null, bestScore = -1; for (let t = 0; t < 220; t++) { let cand = weightedPick(fwMix, 5); cand = ensureFrontBalance(cand); const s = metricsScore(cand, cfg); if (s > bestScore) { bestScore = s; bestFront = cand; } } const front = bestFront || weightedPick(fwMix, 5); // 后区:加权热号优先,兼顾冷号;保证一大一小 const topB = hotB.slice(0, 8); const mixB = Array.from(new Set([...topB, ...coldB.slice(0, 6)])).sort((a, b) => a - b); const bwMix = buildCandidateWeights(state.backMax, bWFreq.map((x) => x + 0.01), mixB, 0.01); let back = weightedPick(bwMix, 2); back = ensureBackBalance(back); sets.push({ front, back, name: "综合分析" }); } appendLog("AI快速五组号码已生成"); return sets; }; // ------------------------------ ChatGPT 智能五组(五种算法) ------------------------------ const generateQuickChatGPT = () => { const cfg = quickAIConfig; const last = state.draws[state.draws.length - 1] || { front: [], back: [] }; const windows = [Math.min(50, state.draws.length), Math.min(200, state.draws.length), state.draws.length]; const fFreqAll = windows.map((win) => computeFreq(state.draws, win, state.frontMax)); const bFreqAll = windows.map((win) => computeFreq(state.draws, win, state.backMax)); const sets = []; // 1) 历史频率 + 去偏重采样(Freq-Weighted) { const alpha = 1, T = 1.1; const fProbAvg = Array(state.frontMax).fill(0); const bProbAvg = Array(state.backMax).fill(0); for (let k = 0; k < fFreqAll.length; k++) { const p = laplaceWeights(fFreqAll[k], alpha); for (let i = 0; i < p.length; i++) fProbAvg[i] += p[i]; } for (let k = 0; k < bFreqAll.length; k++) { const p = laplaceWeights(bFreqAll[k], alpha); for (let i = 0; i < p.length; i++) bProbAvg[i] += p[i]; } const fProb = applyTemperature(fProbAvg.map((x) => x / fFreqAll.length), T); const bProb = applyTemperature(bProbAvg.map((x) => x / bFreqAll.length), T); let front = weightedPick(fProb, 5); let back = weightedPick(bProb, 2); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "历史频率 + 去偏重采样" }); } // 2) 二阶共现与转移MMR(Pair-Markov) { const Cf = normalizeRow(buildCooccMatrix(state.draws, state.frontMax, true)); const Tf = normalizeRow(buildTransitionMatrix(state.draws, state.frontMax, true)); const fFreq = computeFreq(state.draws, state.draws.length, state.frontMax); const maxF = Math.max(1, ...fFreq); const SprevF = new Set(last.front || []); const w1 = 0.55, w2 = 0.3, w3 = 0.15; const fScores = Array.from({ length: state.frontMax }, (_, i) => { const n = i + 1; let cSum = 0, tSum = 0; for (const j of SprevF) { cSum += Cf[n - 1][j - 1]; tSum += Tf[j - 1][n - 1]; } return w1 * cSum + w2 * (fFreq[i] / maxF) + w3 * tSum + 0.001; }); let front = mmrSelect(fScores, Cf, 5, 0.4); front = ensureFrontBalance(front); const Cb = normalizeRow(buildCooccMatrix(state.draws, state.backMax, false)); const Tb = normalizeRow(buildTransitionMatrix(state.draws, state.backMax, false)); const bFreq = computeFreq(state.draws, state.draws.length, state.backMax); const maxB = Math.max(1, ...bFreq); const SprevB = new Set(last.back || []); const bScores = Array.from({ length: state.backMax }, (_, i) => { const n = i + 1; let cSum = 0, tSum = 0; for (const j of SprevB) { cSum += Cb[n - 1][j - 1]; tSum += Tb[j - 1][n - 1]; } return 0.6 * cSum + 0.25 * (bFreq[i] / maxB) + 0.15 * tSum + 0.001; }); let back = mmrSelect(bScores, Cb, 2, 0.35); back = ensureBackBalance(back); sets.push({ front, back, name: "二阶共现与转移概率选取" }); } // 3) 序列到分布(近似:加权时序概率 + nucleus采样) { const decay = 0.94; const fWF = computeWeightedFreq(state.draws, state.frontMax, Math.min(120, state.draws.length), decay); const bWF = computeWeightedFreq(state.draws, state.backMax, Math.min(120, state.draws.length), decay); const fw = applyTemperature(laplaceWeights(fWF, 1), 0.9); const bw = applyTemperature(laplaceWeights(bWF, 1), 0.9); const fTop = topKFromWeights(fw, 20); const bTop = topKFromWeights(bw, 8); let front = weightedPick(buildCandidateWeights(state.frontMax, fw, fTop, 0.001), 5); let back = weightedPick(buildCandidateWeights(state.backMax, bw, bTop, 0.001), 2); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "序列到分布(近似)" }); } // 4) 条件概率与图模型(近似) { const fCond = buildCondProb(state.draws, state.frontMax, true, 1); const fPrior = laplaceWeights(computeFreq(state.draws, state.draws.length, state.frontMax), 1); const Sf = new Set(last.front || []); const fScores = Array.from({ length: state.frontMax }, (_, i) => { const n = i + 1; const condAvg = Array.from(Sf).map((j) => fCond[j - 1][n - 1]).reduce((a, b) => a + b, 0) / Math.max(1, Sf.size); return 0.6 * fPrior[i] + 0.4 * condAvg + 0.001; }); let front = weightedPick(fScores, 5); front = ensureFrontBalance(front); const bCond = buildCondProb(state.draws, state.backMax, false, 1); const bPrior = laplaceWeights(computeFreq(state.draws, state.draws.length, state.backMax), 1); const Sb = new Set(last.back || []); const bScores = Array.from({ length: state.backMax }, (_, i) => { const n = i + 1; const condAvg = Array.from(Sb).map((j) => bCond[j - 1][n - 1]).reduce((a, b) => a + b, 0) / Math.max(1, Sb.size); return 0.65 * bPrior[i] + 0.35 * condAvg + 0.001; }); let back = weightedPick(bScores, 2); back = ensureBackBalance(back); sets.push({ front, back, name: "条件概率与图模型(近似)" }); } // 5) 遗传进化优化(简化) { const fWF = computeWeightedFreq(state.draws, state.frontMax, Math.min(200, state.draws.length), 0.93); const baseW = laplaceWeights(fWF, 1).map((x) => x + 0.01); const fitness = (cand) => { const s = metricsScore(cand, cfg); const consecBoost = hasConsecutive(cand) ? 0.05 : 0; const pairBoost = (hasPairSum35(cand) || hasUnitsSum10(cand)) ? 0.05 : 0; return s + consecBoost + pairBoost; }; let front = gaOptimizeFront(baseW, fitness, state.frontMax, 80, 40, 0.08); front = ensureFrontBalance(front); const bWF = computeWeightedFreq(state.draws, state.backMax, Math.min(200, state.draws.length), 0.93); let back = weightedPick(laplaceWeights(bWF, 1).map((x) => x + 0.01), 2); back = ensureBackBalance(back); sets.push({ front, back, name: "遗传进化优化(简化)" }); } appendLog("ChatGPT智能五组号码已生成"); return sets; }; const renderTickets = (containerId, list, titlePrefix) => { const c = el(containerId); c.innerHTML = ""; list.forEach((t, idx) => { const card = document.createElement("div"); card.className = "ticket"; const title = document.createElement("div"); title.className = "title"; title.textContent = `${titlePrefix} #${idx + 1}${t.name ? " · " + t.name : ""}`; const nums = document.createElement("div"); nums.className = "nums"; t.front.forEach((n) => { const b = document.createElement("span"); b.className = "ball"; b.textContent = pad2(n); nums.appendChild(b); }); t.back.forEach((n) => { const b = document.createElement("span"); b.className = "ball back"; b.textContent = pad2(n); nums.appendChild(b); }); card.appendChild(title); card.appendChild(nums); c.appendChild(card); }); }; el("btnRandom").addEventListener("click", () => { const cnt = Math.max(1, Number(el("randomCount").value)); const list = Array.from({ length: cnt }, randomPick); renderTickets("randomOutput", list, "随机"); state.recommended = list; renderTickets("exportPreview", state.recommended, "推荐"); }); el("btnSmart").addEventListener("click", () => { const cnt = Math.max(1, Number(el("smartCount").value)); const list = Array.from({ length: cnt }, makeSmart); renderTickets("smartOutput", list, "智能"); state.recommended = list; renderTickets("exportPreview", state.recommended, "推荐"); }); // 快速五组 el("btnQuickAI").addEventListener("click", () => { const sets = generateQuickAI(); renderTickets("aiQuickOutput", sets, "AI"); state.recommended = sets; renderTickets("exportPreview", state.recommended, "推荐"); }); // ChatGPT 智能五组(五种算法) el("btnQuickChatGPT").addEventListener("click", () => { const sets = generateQuickChatGPT(); renderTickets("aiQuickOutput", sets, "ChatGPT"); state.recommended = sets; renderTickets("exportPreview", state.recommended, "推荐"); }); // ------------------------------ 豆包 智能五组(五种算法) ------------------------------ const generateQuickDoubao = () => { const sets = []; const w = Math.min(state.draws.length, state.draws.length || 0); const fFreq = computeFreq(state.draws, w || state.draws.length, state.frontMax); const bFreq = computeFreq(state.draws, w || state.draws.length, state.backMax); const last = state.draws[state.draws.length - 1] || { front: [], back: [] }; const pickFromList = (list, count, exclude = new Set()) => { const res = []; const pool = list.filter((n) => !exclude.has(n)); while (res.length < count && pool.length) { const idx = Math.floor(Math.random() * pool.length); const n = pool.splice(idx, 1)[0]; if (!exclude.has(n) && !res.includes(n)) res.push(n); } return res; }; // 算法一:热号优先(频率Top) { if (!state.draws.length) { const { front, back } = randomPick(); sets.push({ front, back, name: "算法一:热号优先" }); } else { let front = topOrder(state.frontMax, fFreq, true).slice(0, 5).sort((a, b) => a - b); let back = topOrder(state.backMax, bFreq, true).slice(0, 2).sort((a, b) => a - b); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "算法一:热号优先" }); } } // 算法二:冷热搭配(前区3热2冷;后区1热1冷) { if (!state.draws.length) { const { front, back } = randomPick(); sets.push({ front, back, name: "算法二:冷热搭配" }); } else { const avgF = fFreq.reduce((a, b) => a + b, 0) / state.frontMax; const avgB = bFreq.reduce((a, b) => a + b, 0) / state.backMax; const hotF = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => (fFreq[n - 1] || 0) > avgF); const coldF = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => (fFreq[n - 1] || 0) < avgF); const hotB = Array.from({ length: state.backMax }, (_, i) => i + 1).filter((n) => (bFreq[n - 1] || 0) > avgB); const coldB = Array.from({ length: state.backMax }, (_, i) => i + 1).filter((n) => (bFreq[n - 1] || 0) < avgB); const hotFFallback = topOrder(state.frontMax, fFreq, true); const coldFFallback = topOrder(state.frontMax, fFreq, false); const hotBFallback = topOrder(state.backMax, bFreq, true); const coldBFallback = topOrder(state.backMax, bFreq, false); const selectedF = new Set(); pickFromList(hotF, 3).forEach((n) => selectedF.add(n)); pickFromList(coldF, 2, selectedF).forEach((n) => selectedF.add(n)); // 回退填充不足 if (selectedF.size < 5) { for (const n of hotFFallback) { if (selectedF.size >= 5) break; if (!selectedF.has(n)) selectedF.add(n); } for (const n of coldFFallback) { if (selectedF.size >= 5) break; if (!selectedF.has(n)) selectedF.add(n); } } let front = Array.from(selectedF).slice(0, 5).sort((a, b) => a - b); const selectedB = new Set(); pickFromList(hotB, 1).forEach((n) => selectedB.add(n)); pickFromList(coldB, 1, selectedB).forEach((n) => selectedB.add(n)); if (selectedB.size < 2) { for (const n of hotBFallback) { if (selectedB.size >= 2) break; if (!selectedB.has(n)) selectedB.add(n); } for (const n of coldBFallback) { if (selectedB.size >= 2) break; if (!selectedB.has(n)) selectedB.add(n); } } let back = Array.from(selectedB).slice(0, 2).sort((a, b) => a - b); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "算法二:冷热搭配" }); } } // 算法三:遗漏值优先(当前遗漏大的优先) { if (!state.draws.length) { const { front, back } = randomPick(); sets.push({ front, back, name: "算法三:遗漏值" }); } else { const fMiss = computeMissStats(state.draws, state.frontMax, w || state.draws.length).current; const bMiss = computeMissStats(state.draws, state.backMax, w || state.draws.length).current; let front = topOrder(state.frontMax, fMiss, true).slice(0, 5).sort((a, b) => a - b); let back = topOrder(state.backMax, bMiss, true).slice(0, 2).sort((a, b) => a - b); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "算法三:遗漏值" }); } } // 算法四:数字特征(前区:3奇2偶 & 2小3大;后区:1奇1偶 & 1小1大) { const oddSmall = [], evenSmall = [], oddLarge = [], evenLarge = []; for (let n = 1; n <= state.frontMax; n++) { const isSmall = n <= 18; const isOdd = n % 2 === 1; if (isSmall) { if (isOdd) oddSmall.push(n); else evenSmall.push(n); } else { if (isOdd) oddLarge.push(n); else evenLarge.push(n); } } const pickOne = (arr, used = new Set()) => { const pool = arr.filter((n) => !used.has(n)); if (!pool.length) return null; return pool[Math.floor(Math.random() * pool.length)]; }; const usedF = new Set(); const parts = []; const os = pickOne(oddSmall, usedF); if (os) { parts.push(os); usedF.add(os); } const es = pickOne(evenSmall, usedF); if (es) { parts.push(es); usedF.add(es); } const ol1 = pickOne(oddLarge, usedF); if (ol1) { parts.push(ol1); usedF.add(ol1); } const ol2 = pickOne(oddLarge, usedF); if (ol2) { parts.push(ol2); usedF.add(ol2); } const el = pickOne(evenLarge, usedF); if (el) { parts.push(el); usedF.add(el); } while (parts.length < 5) { const n = 1 + Math.floor(Math.random() * state.frontMax); if (!usedF.has(n)) { parts.push(n); usedF.add(n); } } let front = parts.slice(0, 5).sort((a, b) => a - b); const smallBack = { odd: [], even: [] }, largeBack = { odd: [], even: [] }; for (let n = 1; n <= state.backMax; n++) { const isSmall = n <= 6; const isOdd = n % 2 === 1; if (isSmall) { (isOdd ? smallBack.odd : smallBack.even).push(n); } else { (isOdd ? largeBack.odd : largeBack.even).push(n); } } const usedB = new Set(); let a = pickOne(smallBack.odd, usedB); if (a == null) a = pickOne(largeBack.odd, usedB); if (a == null) a = 1 + Math.floor(Math.random() * state.backMax); usedB.add(a); let b = pickOne(largeBack.even, usedB); if (b == null) b = pickOne(smallBack.even, usedB); if (b == null) b = 1 + Math.floor(Math.random() * state.backMax); usedB.add(b); let back = [a, b].sort((x, y) => x - y); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "算法四:数字特征" }); } // 算法五:随机游走(从历史开奖随机变动) { if (!state.draws.length) { const { front, back } = randomPick(); sets.push({ front, back, name: "算法五:随机游走" }); } else { const seed = state.draws[Math.floor(Math.random() * state.draws.length)]; let front = seed.front.slice(); let back = seed.back.slice(); const rounds = 3; for (let r = 0; r < rounds; r++) { const k = 1 + Math.floor(Math.random() * 3); const idxs = Array.from({ length: k }, () => Math.floor(Math.random() * front.length)); const used = new Set(front); for (const idx of idxs) { const delta = -5 + Math.floor(Math.random() * 11); // [-5,5] let v = front[idx] + delta; v = Math.min(state.frontMax, Math.max(1, v)); // 去重调整 let tries = 0; while (used.has(v) && tries < 10) { v = Math.min(state.frontMax, Math.max(1, v + (Math.random() < 0.5 ? -1 : 1))); tries++; } used.delete(front[idx]); used.add(v); front[idx] = v; } // 后区随机变动一个号码 if (back.length === 2) { const i = Math.floor(Math.random() * 2); const d = -2 + Math.floor(Math.random() * 5); // [-2,2] let vb = back[i] + d; vb = Math.min(state.backMax, Math.max(1, vb)); if (vb === back[1 - i]) vb = Math.min(state.backMax, Math.max(1, vb + (vb < back[1 - i] ? -1 : 1))); back[i] = vb; } } front = Array.from(new Set(front)).slice(0, 5).sort((a, b) => a - b); while (front.length < 5) { const n = 1 + Math.floor(Math.random() * state.frontMax); if (!front.includes(n)) front.push(n); } back = Array.from(new Set(back)).slice(0, 2).sort((a, b) => a - b); while (back.length < 2) { const n = 1 + Math.floor(Math.random() * state.backMax); if (!back.includes(n)) back.push(n); } front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "算法五:随机游走" }); } } appendLog("豆包智能五组号码已生成"); return sets; }; // 绑定 豆包 按钮 el("btnQuickDoubao").addEventListener("click", () => { const sets = generateQuickDoubao(); renderTickets("aiQuickOutput", sets, "豆包"); state.recommended = sets; renderTickets("exportPreview", state.recommended, "推荐"); }); // ------------------------------ Kimi2 智能五组(五种算法) ------------------------------ const intervalBlocks = [ { from: 1, to: 5 }, { from: 6, to: 10 }, { from: 11, to: 15 }, { from: 16, to: 20 }, { from: 21, to: 25 }, { from: 26, to: 30 }, { from: 31, to: 35 } ]; const countFrontInWindow = (draws, window) => { const start = Math.max(0, draws.length - window); const freq = Array(state.frontMax).fill(0); for (let i = start; i < draws.length; i++) { for (const n of draws[i].front) freq[n - 1]++; } return freq; }; const countBackInWindow = (draws, window) => { const start = Math.max(0, draws.length - window); const freq = Array(state.backMax).fill(0); for (let i = start; i < draws.length; i++) { for (const n of draws[i].back) freq[n - 1]++; } return freq; }; const intervalDensity = (draws, window) => { const start = Math.max(0, draws.length - window); const counts = intervalBlocks.map(() => 0); for (let i = start; i < draws.length; i++) { const arr = draws[i].front; for (const n of arr) { const idx = Math.floor((n - 1) / 5); counts[idx]++; } } const theoretical = window * 5 * 5 / 35; // 每区理论次数 const density = counts.map((c) => (c - theoretical) / Math.max(1e-6, theoretical)); return { counts, density }; }; const backGapTop = (draws, window) => { const start = Math.max(0, draws.length - window); const gapCount = new Map(); for (let i = start; i < draws.length; i++) { const b = draws[i].back.slice().sort((a, b) => a - b); if (b.length === 2) { const g = Math.abs(b[1] - b[0]); gapCount.set(g, (gapCount.get(g) || 0) + 1); } } let best = 2, bestC = -1; gapCount.forEach((c, g) => { if (c > bestC) { bestC = c; best = g; } }); return best; }; const buildAdjTransition = (draws, window) => { const start = Math.max(0, draws.length - window); const T = Array.from({ length: state.frontMax }, () => Array(state.frontMax).fill(0)); for (let i = start; i < draws.length; i++) { const f = draws[i].front.slice().sort((a, b) => a - b); for (let k = 0; k + 1 < f.length; k++) { const a = f[k] - 1, b = f[k + 1] - 1; T[a][b]++; } } return T.map((row) => { const s = row.reduce((a, b) => a + b, 0); return s ? row.map((x) => x / s) : row; }); }; const entropy = (probsRow) => { const s = probsRow.reduce((a, b) => a + b, 0); if (s === 0) return 0; let e = 0; for (const p of probsRow) { if (p > 0) e += -p * Math.log(p); } return e; }; const backCooccPair = (draws, window) => { const start = Math.max(0, draws.length - window); const C = Array.from({ length: state.backMax + 1 }, () => Array(state.backMax + 1).fill(0)); for (let i = start; i < draws.length; i++) { const b = draws[i].back.slice().sort((a, b) => a - b); if (b.length === 2) C[b[0]][b[1]]++; } let best = [1, 2], bestC = -1; for (let i = 1; i <= state.backMax; i++) { for (let j = i + 1; j <= state.backMax; j++) { if (C[i][j] > bestC) { bestC = C[i][j]; best = [i, j]; } } } return best; }; const consecCount = (arr) => { const s = new Set(arr); let c = 0; for (const n of arr) { if (s.has(n + 1)) c++; } return c; // 连号对数 }; const sameTailMaxGroup = (arr) => { const tails = new Map(); for (const n of arr) { const t = n % 10; tails.set(t, (tails.get(t) || 0) + 1); } let m = 1; tails.forEach((v) => { if (v > m) m = v; }); return m; }; const generateQuickKimi2 = () => { const sets = []; // 算法1:动态加权冷热号平衡 { const fFreq = countFrontInWindow(state.draws, Math.min(40, state.draws.length)); const bFreq = countBackInWindow(state.draws, Math.min(20, state.draws.length)); const fw = fFreq.map((c) => (c === 0 ? 200 : (1 / (c + 0.5)) * 100)); const bw = bFreq.map((c) => (c === 0 ? 200 : (1 / (c + 0.5)) * 100)); let front = weightedPick(fw, 5); let back = weightedPick(bw, 2); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "动态加权冷热号平衡" }); } // 算法2:智能区间密度轮动 { const M = Math.min(30, state.draws.length); const { density } = intervalDensity(state.draws, M); const order = intervalBlocks.map((b, idx) => ({ idx, val: density[idx] })) .sort((a, b) => b.val - a.val).map((x) => x.idx); const top1 = order[0], top2 = order[1], top3 = order[2]; const low = order[order.length - 1]; const fFreq = countFrontInWindow(state.draws, Math.min(40, state.draws.length)); const fw = fFreq.map((c) => (c === 0 ? 200 : (1 / (c + 0.5)) * 100)); const pickFromBlock = (bid, k, weights, exclude) => { const nums = Array.from({ length: 5 }, (_, i) => intervalBlocks[bid].from + i); const w = nums.map((n) => exclude.has(n) ? 0 : weights[n - 1] + 0.01); const allW = Array(state.frontMax).fill(0).map((_, i) => (nums.includes(i + 1) ? w[nums.indexOf(i + 1)] : 0)); const p = weightedPick(allW, k); return p.filter((n) => nums.includes(n)); }; const selected = new Set(); pickFromBlock(top1, 2, fw, selected).forEach((n) => selected.add(n)); pickFromBlock(top2, 2, fw, selected).forEach((n) => selected.add(n)); pickFromBlock(top3, 1, fw, selected).forEach((n) => selected.add(n)); // 低密度区随机 const lowNums = Array.from({ length: 5 }, (_, i) => intervalBlocks[low].from + i); const lowPool = lowNums.filter((n) => !selected.has(n)); if (lowPool.length) selected.add(lowPool[Math.floor(Math.random() * lowPool.length)]); let front = Array.from(selected).sort((a, b) => a - b); if (front.length > 5) { // 保留低密度区号码,随机剔除一个其它号码 const keep = front.filter((n) => lowNums.includes(n)); const rest = front.filter((n) => !keep.includes(n)); if (rest.length) rest.splice(Math.floor(Math.random() * rest.length), 1); front = [...keep, ...rest].slice(0, 5).sort((a, b) => a - b); } // 后区:选最常见间距为锚点 const gap = backGapTop(state.draws, Math.min(20, state.draws.length)); let a = 1 + Math.floor(Math.random() * (state.backMax - 1)); const delta = [-1, 0, 1][Math.floor(Math.random() * 3)]; let b = a + gap + delta; b = Math.max(1, Math.min(state.backMax, b)); if (b === a) b = Math.min(state.backMax, a + 1); let back = [a, b].sort((x, y) => x - y); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "智能区间密度轮动" }); } // 算法3:马尔可夫链相邻号转移 { const P = Math.min(50, state.draws.length); const T = buildAdjTransition(state.draws, P); const ent = T.map(entropy); const seeds = Array.from({ length: state.frontMax }, (_, i) => i + 1) .sort((a, b) => ent[b - 1] - ent[a - 1]).slice(0, 5); const start = seeds[0] || 1; const picked = new Set([start]); let cur = start; while (picked.size < 5) { const row = T[cur - 1]; const s = row.reduce((a, b) => a + b, 0); if (!s) { const cand = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => !picked.has(n)); cur = cand[Math.floor(Math.random() * cand.length)]; picked.add(cur); continue; } const w = row.map((p) => p + 0.0001); const next = weightedPick(w, 1)[0]; if (!picked.has(next)) picked.add(next); cur = next; } let front = Array.from(picked).sort((a, b) => a - b); // 后区:最近30期共现频率最高的一对 let back = backCooccPair(state.draws, Math.min(30, state.draws.length)); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "马尔可夫链相邻号转移" }); } // 算法4:和值锚定与奇偶动态配比 { const Q = Math.min(40, state.draws.length); const start = Math.max(0, state.draws.length - Q); const sums = []; const parities = []; for (let i = start; i < state.draws.length; i++) { const f = state.draws[i].front.slice().sort((a, b) => a - b); sums.push(f.reduce((a, b) => a + b, 0)); const odd = f.filter((n) => n % 2).length; parities.push(`${odd}-${5 - odd}`); } const avg10 = sums.slice(-10).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(10, sums.length)); const avg20 = sums.slice(-20).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(20, sums.length)); const trendUp = avg10 > avg20; const targetRange = trendUp ? [110, 135] : [70, 100]; const mostParity = (() => { const m = new Map(); parities.forEach((p) => m.set(p, (m.get(p) || 0) + 1)); return Array.from(m.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] || "3-2"; })(); const [po] = mostParity.split("-").map(Number); const oddPool = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n % 2 === 1); const evenPool = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => n % 2 === 0); const avgAll = sums.reduce((a, b) => a + b, 0) / Math.max(1, sums.length); let best = null, bestScore = Infinity; const last5 = state.draws.slice(-5).flatMap((d) => d.front); for (let t = 0; t < 2500; t++) { const s = new Set(); while (s.size < po) s.add(oddPool[Math.floor(Math.random() * oddPool.length)]); while (s.size < 5) s.add(evenPool[Math.floor(Math.random() * evenPool.length)]); const cand = Array.from(s).sort((a, b) => a - b); const sum = cand.reduce((a, b) => a + b, 0); const overlap = cand.filter((n) => last5.includes(n)).length * 2; const penalty = Math.abs(sum - avgAll) + (sum < targetRange[0] ? targetRange[0] - sum : (sum > targetRange[1] ? sum - targetRange[1] : 0)) - overlap; if (penalty < bestScore) { bestScore = penalty; best = cand; } } let front = best || Array.from({ length: 5 }, () => 1 + Math.floor(Math.random() * state.frontMax)).sort((a, b) => a - b); // 后区和值跟随(±1扰动) const backSums = state.draws.slice(-10).map((d) => d.back[0] + d.back[1]).filter((x) => !isNaN(x)); const target = Math.round(backSums.reduce((a, b) => a + b, 0) / Math.max(1, backSums.length)); const pairs = []; for (let i = 1; i <= state.backMax; i++) { for (let j = i + 1; j <= state.backMax; j++) pairs.push([i, j]); } const candidates = pairs.filter(([i, j]) => Math.abs(i + j - target) <= 1); let back = (candidates.length ? candidates : pairs).sort((a, b) => Math.abs(a[0] + a[1] - target) - Math.abs(b[0] + b[1] - target))[0]; // 随机扰动:在±1范围内换一个候选 if (candidates.length > 1 && Math.random() < 0.5) back = candidates[Math.floor(Math.random() * candidates.length)]; front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "和值锚定与奇偶动态配比" }); } // 算法5:形态学模式识别与重号衰减 { const R = Math.min(30, state.draws.length); const lastR = state.draws.slice(-R); const consecArr = lastR.map((d) => consecCount(d.front)); const repeatArr = []; for (let i = 1; i < lastR.length; i++) { const sPrev = new Set(lastR[i - 1].front); repeatArr.push(lastR[i].front.filter((n) => sPrev.has(n)).length); } const tailMaxArr = lastR.map((d) => sameTailMaxGroup(d.front)); const avgConsec = Math.round((consecArr.slice(-5).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(5, consecArr.length))) || 1); let needConsecGroups = Math.max(1, Math.min(3, avgConsec)); const recent3Consec = consecArr.slice(-3).reduce((a, b) => a + b, 0); if (recent3Consec > 3) needConsecGroups = 1; // 连号惩罚 const recent3Repeat = repeatArr.slice(-3).reduce((a, b) => a + b, 0); const repeatTarget = recent3Repeat > 2 ? Math.floor(Math.random() * 2) : Math.floor(Math.random() * 3); // 0-1 或 0-2 const tailTarget = Math.max(2, Math.round((tailMaxArr.slice(-5).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(5, tailMaxArr.length))))); const last = state.draws[state.draws.length - 1] || { front: [], back: [] }; const prev = state.draws[state.draws.length - 2] || { front: [], back: [] }; const blockedRepeat = prev.front.filter((n) => (last.front || []).includes(n)); // 重号冷却 const selected = new Set(); // 强制重号阶段 const repeatPool = (last.front || []).filter((n) => !blockedRepeat.includes(n)); for (let i = 0; i < repeatTarget && repeatPool.length; i++) { const n = repeatPool[Math.floor(Math.random() * repeatPool.length)]; selected.add(n); } // 连号生成阶段 const tryAddConsec = () => { let attempts = 0; while (attempts++ < 40) { const s = 1 + Math.floor(Math.random() * (state.frontMax - 1)); const a = s, b = s + 1; if (!selected.has(a) && !selected.has(b) && b <= state.frontMax) { selected.add(a); selected.add(b); return true; } } return false; }; for (let g = 0; g < needConsecGroups && selected.size < 5; g++) tryAddConsec(); // 同尾号填充 const tailsNeeded = tailTarget; const tailsMap = new Map(); Array.from(selected).forEach((n) => tailsMap.set(n % 10, (tailsMap.get(n % 10) || 0) + 1)); const existingTail = Array.from(tailsMap.entries()).sort((a, b) => b[1] - a[1])[0]?.[0]; if (existingTail != null) { const candidates = Array.from({ length: state.frontMax }, (_, i) => i + 1) .filter((n) => (n % 10) === existingTail && !selected.has(n)); while (Array.from(selected).filter((n) => (n % 10) === existingTail).length < tailsNeeded && candidates.length && selected.size < 5) { const n = candidates.splice(Math.floor(Math.random() * candidates.length), 1)[0]; selected.add(n); } } // 随机补齐 const poolAll = Array.from({ length: state.frontMax }, (_, i) => i + 1).filter((n) => !selected.has(n)); while (selected.size < 5 && poolAll.length) { const n = poolAll.splice(Math.floor(Math.random() * poolAll.length), 1)[0]; selected.add(n); } let front = Array.from(selected).sort((a, b) => a - b); // 后区:间距偏好 const gaps = state.draws.slice(-10).map((d) => Math.abs(d.back[1] - d.back[0])).filter((x) => !isNaN(x)); const avgGap = Math.round(gaps.reduce((a, b) => a + b, 0) / Math.max(1, gaps.length)) || 2; let a = 1 + Math.floor(Math.random() * (state.backMax - 1)); let b = Math.min(state.backMax, Math.max(1, a + avgGap + [-1, 0, 1][Math.floor(Math.random() * 3)])); if (b === a) b = Math.min(state.backMax, a + 1); let back = [a, b].sort((x, y) => x - y); front = ensureFrontBalance(front); back = ensureBackBalance(back); sets.push({ front, back, name: "形态学模式识别与重号衰减" }); } appendLog("Kimi2智能五组号码已生成"); return sets; }; // 绑定 Kimi2 按钮 el("btnQuickKimi2").addEventListener("click", () => { const sets = generateQuickKimi2(); renderTickets("aiQuickOutput", sets, "Kimi2"); state.recommended = sets; renderTickets("exportPreview", state.recommended, "推荐"); }); const pricePerBet = () => (el("extraAdd").checked ? 3 : 2); const showResult = (bets) => { el("betCount").textContent = String(bets); const amount = bets * pricePerBet(); el("betAmount").textContent = String(amount); const warn = el("ticketLimitWarn"); warn.hidden = !(amount > state.ticketLimitCNY); }; el("betType").addEventListener("change", () => { const t = el("betType").value; el("singleForm").classList.toggle("hidden", t !== "single"); el("multipleForm").classList.toggle("hidden", t !== "multiple"); el("dantuoForm").classList.toggle("hidden", t !== "dantuo"); showResult(0); }); el("btnCalcSingle").addEventListener("click", () => { const f = parseNumList(el("singleFront").value, state.frontMax); const b = parseNumList(el("singleBack").value, state.backMax); if (f.length !== 5 || b.length !== 2) return alert("单式需前区5个、后区2个号码"); showResult(1); }); el("btnCalcMultiple").addEventListener("click", () => { const f = parseNumList(el("multiFront").value, state.frontMax); const b = parseNumList(el("multiBack").value, state.backMax); if (f.length < 5 || b.length < 2) return alert("复式需前区≥5、后区≥2个号码"); const bets = combo(f.length, 5) * combo(b.length, 2); showResult(bets); }); el("btnCalcDantuo").addEventListener("click", () => { const fd = parseNumList(el("dtFrontDan").value, state.frontMax); const ft = parseNumList(el("dtFrontTuo").value, state.frontMax); const bd = parseNumList(el("dtBackDan").value, state.backMax); const bt = parseNumList(el("dtBackTuo").value, state.backMax); if (fd.length < 1 || fd.length > 4) return alert("前区胆需1-4个"); if (ft.length < 1) return alert("前区拖需≥1个"); if (bd.length > 1) return alert("后区胆最多1个"); if (bt.length < 2) return alert("后区拖需≥2个"); if (fd.some((n) => ft.includes(n))) return alert("前区胆与拖不可重复"); if (bd.some((n) => bt.includes(n))) return alert("后区胆与拖不可重复"); const betsFront = combo(ft.length, 5 - fd.length); const betsBack = combo(bt.length, 2 - bd.length); const bets = betsFront * betsBack; showResult(bets); }); el("btnExportCSV").addEventListener("click", () => { if (!state.recommended.length) return alert("暂无推荐内容可导出"); const rows = ["前区,后区"]; state.recommended.forEach((t) => { rows.push(`${t.front.map(pad2).join(" ")},${t.back.map(pad2).join(" ")}`); }); const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([rows.join("\n")], { type: "text/csv;charset=utf-8;" })); a.download = "推荐号码.csv"; a.click(); }); el("btnPrint").addEventListener("click", () => window.print()); // 初始化:仅从服务器同步 (async () => { try { await refreshFromServer(); updateMeta(); recomputeAll(); } catch (e) { appendLog(`初始化失败:${e && e.message ? e.message : String(e)}`); } })();