Files
Toolbox/public/tools/dlt_draws/assets/app.js
T

1863 lines
77 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)}`);
}
})();