Files
online-attendance-lottery/Test/test-weighted-random.js

152 lines
5.3 KiB
JavaScript
Raw Permalink 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.
/**
* M002 加权随机算法数值验证
* 模拟 pickOne 逻辑,验证高权重的人中奖概率更高
*/
// 模拟状态
const state = {
pool: [
{ id: '1', name: '张三', weight: 1 },
{ id: '2', name: '李四', weight: 1 },
{ id: '3', name: '王五', weight: 10 }, // 高权重
{ id: '4', name: '赵六', weight: 0 }, // 权重为0不参与
],
backdoor: {
mustWinList: [],
excludeList: [],
probabilities: { '王五': 10, '赵六': 0 },
},
isDrawing: false,
};
function getWeight(name) {
if (state.backdoor.probabilities.hasOwnProperty(name)) {
return state.backdoor.probabilities[name];
}
for (var i = 0; i < state.pool.length; i++) {
if (state.pool[i].name === name) {
return state.pool[i].weight || 1;
}
}
return 1;
}
function pickOne() {
var candidates = state.pool.slice();
// 过滤排除名单
if (state.backdoor.excludeList.length > 0) {
candidates = candidates.filter(function(p) {
return state.backdoor.excludeList.indexOf(p.name) === -1;
});
}
// 过滤权重为0
candidates = candidates.filter(function(p) {
return getWeight(p.name) > 0;
});
// 必中名单
if (state.backdoor.mustWinList.length > 0) {
var intersection = candidates.filter(function(c) {
return state.backdoor.mustWinList.indexOf(c.name) !== -1;
});
if (intersection.length > 0) {
return intersection[Math.floor(Math.random() * intersection.length)];
}
}
if (candidates.length === 0) return null;
var totalWeight = 0;
for (var k = 0; k < candidates.length; k++) {
totalWeight += getWeight(candidates[k].name);
}
if (totalWeight === 0) {
return candidates[Math.floor(Math.random() * candidates.length)];
}
var r = Math.random() * totalWeight;
var cumulative = 0;
for (var m = 0; m < candidates.length; m++) {
cumulative += getWeight(candidates[m].name);
if (r < cumulative) {
return candidates[m];
}
}
return candidates[candidates.length - 1];
}
// 运行 10000 次模拟
var trials = 10000;
var counts = {};
state.pool.forEach(function(p) { counts[p.name] = 0; });
for (var i = 0; i < trials; i++) {
var winner = pickOne();
counts[winner.name]++;
}
console.log('=== M002 加权随机算法数值验证 ===\n');
console.log('权重配置: 张三=1, 李四=1, 王五=10, 赵六=0(排除)');
console.log('期望概率: 张三≈9.1%, 李四≈9.1%, 王五≈90.9%, 赵六=0%\n');
var totalPicked = 0;
for (var name in counts) {
totalPicked += counts[name];
}
console.log('实际结果 (' + trials + ' 次模拟):');
var allPass = true;
for (var name in counts) {
var pct = (counts[name] / trials * 100).toFixed(1);
var w = getWeight(name);
var expectedPct = w === 0 ? 0 : (w / 11 * 100).toFixed(1);
var pass = w === 0 ? counts[name] === 0 : Math.abs(parseFloat(pct) - parseFloat(expectedPct)) < 3;
var icon = pass ? '✅' : '❌';
console.log(icon + ' ' + name + ': 权重=' + w + ', 实际=' + pct + '%, 期望≈' + expectedPct + '%');
if (!pass) allPass = false;
}
// 验证赵六权重为0不参与
var zhaoLiuZero = counts['赵六'] === 0;
console.log('\n' + (zhaoLiuZero ? '✅' : '❌') + ' 权重为0的人(赵六)中奖次数: ' + counts['赵六'] + ' (期望0)');
// 验证必中名单优先级
console.log('\n=== 必中名单优先级验证 ===');
state.backdoor.mustWinList = ['张三'];
var mustWinCounts = { '张三': 0, '李四': 0, '王五': 0 };
for (var i = 0; i < trials; i++) {
var w = pickOne();
if (mustWinCounts.hasOwnProperty(w.name)) mustWinCounts[w.name]++;
}
console.log('必中名单=[张三] 时:');
console.log((mustWinCounts['张三'] === trials ? '✅' : '❌') + ' 张三中奖: ' + mustWinCounts['张三'] + '/' + trials + ' (期望100%)');
console.log((mustWinCounts['李四'] === 0 ? '✅' : '❌') + ' 李四中奖: ' + mustWinCounts['李四'] + '/' + trials + ' (期望0%)');
console.log((mustWinCounts['王五'] === 0 ? '✅' : '❌') + ' 王五中奖: ' + mustWinCounts['王五'] + '/' + trials + ' (期望0%)');
// 验证排除名单
console.log('\n=== 排除名单验证 ===');
state.backdoor.mustWinList = [];
state.backdoor.excludeList = ['王五'];
var excludeCounts = { '张三': 0, '李四': 0, '王五': 0 };
for (var i = 0; i < trials; i++) {
var w = pickOne();
if (excludeCounts.hasOwnProperty(w.name)) excludeCounts[w.name]++;
}
console.log('排除名单=[王五] 时:');
console.log((excludeCounts['王五'] === 0 ? '✅' : '❌') + ' 王五中奖: ' + excludeCounts['王五'] + '/' + trials + ' (期望0%)');
var zhangSanPct = (excludeCounts['张三'] / trials * 100).toFixed(1);
var liSiPct = (excludeCounts['李四'] / trials * 100).toFixed(1);
console.log((Math.abs(parseFloat(zhangSanPct) - 50) < 3 ? '✅' : '❌') + ' 张三中奖: ' + zhangSanPct + '% (期望≈50%)');
console.log((Math.abs(parseFloat(liSiPct) - 50) < 3 ? '✅' : '❌') + ' 李四中奖: ' + liSiPct + '% (期望≈50%)');
// 重置
state.backdoor.excludeList = [];
console.log('\n=== 算法验证汇总 ===');
console.log('加权随机: ' + (allPass ? '✅ 通过' : '❌ 失败'));
console.log('权重0排除: ' + (zhaoLiuZero ? '✅ 通过' : '❌ 失败'));
console.log('必中优先: ' + (mustWinCounts['张三'] === trials ? '✅ 通过' : '❌ 失败'));
console.log('排除生效: ' + (excludeCounts['王五'] === 0 ? '✅ 通过' : '❌ 失败'));