1863 lines
77 KiB
JavaScript
1863 lines
77 KiB
JavaScript
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)}`);
|
||
}
|
||
})();
|