js年会抽奖_嘿!这真的是一个正经的抽奖程序!
編者按:本文作者奇舞團團長月影
奇舞團有一個傳統項目,每年年會由我在現場寫一個抽獎程序,所有人一起review代碼,以確保抽獎算法正確且公平,然后愉快滴開始抽獎。
現場寫的抽獎程序不僅要公平無bug,而且還要有一定的趣味性,且不能和往年的重復。
2017年年會我寫了一個隨機抽紙牌中獎1的程序,而今年年會,我靈機一動?,決定寫一個更有(不)趣(正)味(經)的抽獎程序。
長話短說,我們就著代碼一步步往下看。
首先是常規隨機洗牌三連:
function?random(m,?n)?{
??return?m?+?Math.floor(Math.random()?*?n);
}
function?randomItem(arr,?from?=?0,?to?=?arr.length)?{
??const?index?=?random(from,?to);
??return?{
????index,
????value:?arr[index],
??};
}
function?shuffle(arr)?{
??for(let?i?=?arr.length;?i?>?0;?i--)?{
????const?{index}?=?randomItem(arr,?0,?i);
????[arr[index],?arr[i?-?1]]?=?[arr[i?-?1],?arr[index]];
??}
??return?arr;
}
上面的代碼沒有什么特別的,只是一個樸實的洗牌算法,額外封裝兩個隨機函數,因為后面的代碼中還要使用。對比一下,2017年版的代碼更加“妖艷”:
function*?generatePoker()?{
??const?points?=?['A',?2,?3,?4,?5,?6,?7,?8,?9,?10,?'J',?'Q',?'K'];
??yield*?points.map(p?=>?['??',?p]);
??yield*?points.map(p?=>?['??',?p]);
??yield*?points.map(p?=>?['??',?p]);
??yield*?points.map(p?=>?['??',?p]);
}
const?cards?=?generatePoker();
class?PickedCards?{
??constructor(key,?storage?=?localStorage)?{
????this.key?=?key;
????this.storage?=?storage;
????this.cards?=?JSON.parse(storage.getItem(key))?||?[];
????this.cardSet?=?new?Set(this.cards.map(card?=>?card.join('')));
??}
??add(card)?{
????this.cards.push(card);
????this.cardSet.add(card.join(''));
????this.storage.setItem(this.key,?JSON.stringify(this.cards));
??}
??has(card)?{
????return?this.cardSet.has(card.join(''));
??}
??clear()?{
????this.storage.clear();
??}
}
const?pickedCards?=?new?PickedCards('pickedCards');
function*?shuffle(cards,?pickedCards)?{
??cards?=?[...cards];
??cards?=?cards.filter(card?=>?!pickedCards.has(card));
??let?len?=?cards.length;
??while(len)?{
????const?i?=?Math.floor(Math.random()?*?len);
????pickedCards.add(cards[i]);
????yield?cards[i];
????[cards[i],?cards[len?-?1]]?=?[cards[len?-?1],?cards[i]];
????len--;
??}
}
2017版的隨機撲克牌代碼
有了上面樸實的隨機代碼,理論上我們就可以愉快滴抽獎了:
function?random(m,?n)?{
??return?m?+?Math.floor(Math.random()?*?n);
}
function?randomItem(arr,?from?=?0,?to?=?arr.length)?{
??const?index?=?random(from,?to);
??return?{
????index,
????value:?arr[index],
??};
}
function?shuffle(arr)?{
??for(let?i?=?arr.length;?i?>?0;?i--)?{
????const?{index}?=?randomItem(arr,?0,?i);
????[arr[index],?arr[i?-?1]]?=?[arr[i?-?1],?arr[index]];
??}
??return?arr;
}
let?members?=?['胖虎',?'強夫',?'靜香',?'大雄',?'哆啦A夢',?'呂布',?'張飛',?'關羽',?'劉備',?'曹操',?'孫權',?'周瑜',
??'黃蓋',?'趙云',?'呂蒙',?'孫悟空',?'豬八戒',?'唐僧',?'沙悟凈',?'光頭強',?'熊大',?'熊二',
??'喜洋洋',?'美羊羊',?'紅太狼',?'灰太狼',
];
console.log(shuffle(members).slice(-3));?// 抽取3名獲獎者
當然,我們不會只是這么無聊滴抽取,還是要玩點花樣,不然怎么好意思說自己是前端呢?
HTML必須有:
<!DOCTYPE html>
<html>
<head>
??<meta charset="utf-8">
??<meta name="viewport"?content="width=device-width">
??<title>一起抽獎吧</title>
??<link rel="stylesheet"?href="style.css">
</head>
<body>
??<div id="control"><button id="start">開始</button><button id="clear">清空</button></div>
??<div id="track">
????<div><span?class="horse"></span><span?class="player">1</span></div>
????<div><span?class="horse"></span><span?class="player">2</span></div>
????<div><span?class="horse"></span><span?class="player">3</span></div>
????<div><span?class="horse"></span><span?class="player">4</span></div>
????<div><span?class="horse"></span><span?class="player">5</span></div>
????<div><span?class="horse"></span><span?class="player">6</span></div>
??</div>
??<script src="app.js"></script>
</body>
</html>
CSS簡單寫一個:
html,?body?{
??padding:?0;
??margin:?0;
??width:?100%;
??height:?100%;
}
#control?{
??text-align:?center;
??line-height:?120px;
}
#control button?{
??font-size:?2rem;
??margin:?0?10px;
}
#track?{
??max-width:?1250px;
??max-height:?500px;
??border-top:?solid 1px #aaa;
}
#track div?{
??position:?relative;
??height:?100px;
??line-height:?100px;
??font-size:?1.5rem;
??padding:?0?10px;
??color:?#aaa;
}
#track?.player?{
??float:?right;
}
#track?.horse?{
??display:?inline-block;
??font-size:?5rem;
??transform:?scale(-1,?1);
??position:?absolute;
??left:?850px;
??top:?0;
??z-index:?99999;
}
#track?.horse span?{
??display:?inline-block;
??transform:?scale(-1,?1);
}
#track div:nth-child(2n)?{
??background:?#666;
}
#track div::after?{
??content:?' ';
??position:?absolute;
??left:?930px;
??width:?20px;
??height:?100%;
??background:?#333;
}
這個玩法呢,就是個跑馬小游戲:
接下來,我們開始完善JS代碼。為了記錄抽獎結果和連續抽獎(每個人只能中一次獎),避免不小心刷新了頁面,導致結果丟失,我們用localStorage存一下:
const?prizeStorageKey?=?'prize10';
function?addResults(players)?{
??const?result?=?getResults();
??result.push(...players);
??localStorage.setItem(prizeStorageKey,?result.join());
}
我們如果不小心刷新了頁面,重新開始抽獎之前,我們要把已經中過獎的小伙伴從列表里剔除:
function?getResults()?{
??const?result?=?localStorage.getItem(prizeStorageKey);
??return?result???result.split(',')?:?[];
}
function?filterWinner(members)?{
??const?winners?=?new?Set(getResults());
??return?members.filter(m?=>?!winners.has(m));
}
members?=?filterWinner(members);
然后我們可以點【開始】按鈕抽獎,點【清除】按鈕清除localStorage記錄。
const?startBtn?=?document.getElementById('start');
const?clearBtn?=?document.getElementById('clear');
startBtn.addEventListener('click',?async?()?=>?{
??startBtn.disabled?=?'disabled';
??clearBtn.disabled?=?'disabled';
??// 重新洗牌
??shuffle(members);
??// 取出最后6名同學,倒數3名中獎,剩下3名湊數
??const?candidates?=?members.slice(-6).reverse();
??// 將中獎結果保存到localStorage中
??addResults(candidates.slice(0,?3));
??members.length?-=?3;
??// 開始跑馬程序
??await?race(candidates);
??startBtn.disabled?=?'';
??clearBtn.disabled?=?'';
});
clearBtn.addEventListener('click',?()?=>?{
??// 清除所有中獎記錄
??localStorage.removeItem(prizeStorageKey);
});
接下來就是實現關鍵的跑馬程序了。
其實我們的中獎結果在跑馬程序開始前就已經出來了,跑馬程序只是運行動畫效果,和中獎結果無關。
最簡單的一種方式就是根據排名依次從短到長,生成跑馬總時間,然后將6個人隨機到不同的賽道開始跑馬:
function?race(candidates)?{
??const?durations?=?[];
??for(let?i?=?0,?duration?=?0.9;?i?<?candidates.length;?i++)?{
????durations.push(duration);
????// 每一名次隨機增加 0.02 ~ 0.05 的時間
????duration?+=?random(2,?5)?*?0.01;
??}
??const?players?=?shuffle([...candidates.entries()]);
??...
}
但是這樣有個問題,就是跑馬的時候,名次落后的時間長速度慢,名次靠前的速度快始終跑在前面,勝負毫無懸念,也就失去了賽馬的意義。所以要做隨機,以保留懸念。
產生隨機有很多種方法,這里用一種最簡單的方法,就是把一次比賽分成若干個小階段,每個小階段分配一個基準時間,但是允許每個選手在該階段時間有一定的正負擾動。比如:
A選手跑完全程時間8秒鐘,B選手跑完全程時間為10秒鐘,我們第一階段先取路程的1/4,A跑完1/4程的基準時間是2秒,B是2.5秒,假設擾動參數為正負0.5,那么A跑完1/4程的時間最多是2+0.5=2.5秒,而B跑完1/4程的時間最少是2.5-0.5=2.0秒,這樣就有可能在前1/4賽程里A選手反而落后B選手了。多分幾個賽程,就可以有足夠的懸念。
我們先定義劃分賽程的函數:
function?partRace(durations,?factor)?{
??// 根據賽程總時間 duration 和 factor 來劃分賽程
??// 賽程所用基準時間為 duration * factor,擾動 -0.1 ~ +0.1
??const?subDuration?=?durations.map(d?=>?d?*?factor?*?random(9,?11)?/?10);
??subDuration.map((d,?i)?=>?{
????durations[i]?-=?d;
????return?durations[i];
??});
??return?subDuration;
}
這樣我們把全程劃分4段賽程:
function?race(candidates)?{
??const?durations?=?[];
??for(let?i?=?0,?duration?=?0.9;?i?<?candidates.length;?i++)?{
????durations.push(duration);
????// 每一名次隨機增加 0.02 ~ 0.05 的時間
????duration?+=?random(2,?5)?*?0.01;
??}
??// 劃分4段賽程
??const?round1?=?partRace(durations,?0.25);
??const?round2?=?partRace(durations,?0.33);
??const?round3?=?partRace(durations,?0.5);
??const?round4?=?durations.map(d?=>?d?+?0.1);
??...
}
這里面還有一個小技巧,我們劃分賽程的時候,給最后一輪留下10%的時間,這是為了避免前面幾輪賽程積累的隨機擾動使得最后一程的時間太短。
這樣我們就可以繪制賽馬動畫了:
function?partRace(durations,?factor)?{
??// 根據賽程總時間 duration 和 factor 來劃分賽程
??// 賽程所用基準時間為 duration * factor,擾動 -0.1 ~ +0.1
??const?subDuration?=?durations.map(d?=>?d?*?factor?*?random(9,?11)?/?10);
??subDuration.map((d,?i)?=>?{
????durations[i]?-=?d;
????return?durations[i];
??});
??return?subDuration;
}
function?race(candidates)?{
??const?durations?=?[];
??for(let?i?=?0,?duration?=?0.9;?i?<?candidates.length;?i++)?{
????durations.push(duration);
????// 每一名次隨機增加 0.02 ~ 0.05 的時間
????duration?+=?random(2,?5)?*?0.01;
??}
??const?players?=?shuffle([...candidates.entries()]);
??trackEl.innerHTML?=?players.map((p,?i)?=>?{
????return?`
?${randomItem(['?',?'?',?'?',?'?']).value}
?${p[1]}?${i?+?1}
`;??}).join('');
??// 劃分4段賽程
??const?round1?=?partRace(durations,?0.25);
??const?round2?=?partRace(durations,?0.33);
??const?round3?=?partRace(durations,?0.5);
??const?round4?=?durations.map(d?=>?d?+?0.1);
??const?results?=?['?',?'?',?'?',?'?',?'?',?'?'];
??const?T?=?8000;
??const?horses?=?document.querySelectorAll('.horse');
??const?promises?=?[];
??for(let?i?=?0;?i?<?horses.length;?i++)?{
????const?horse?=?horses[i];
????const?idx?=?players[i][0];
????promises.push(raceHorse(horse,?round1[idx]?*?T)
??????.then(()?=>?{
????????return?raceHorse(horse,?round2[idx]?*?T,?30?+?trackLen?/?4);
??????})
??????.then(()?=>?{
????????return?raceHorse(horse,?round3[idx]?*?T,?30?+?2?*?trackLen?/?4);
??????})
??????.then(()?=>?{
????????return?raceHorse(horse,?round4[idx]?*?T,?30?+?3?*?trackLen?/?4);
??????})
??????.then(()?=>?{
????????horse.innerHTML?=?`${results[idx]}${horse.innerHTML}`;
????????return?raceHorse(horse,?0.1?*?T,?30?+?trackLen,?100);
??????}));
??}
??return?Promise.all(promises);
}
具體的raceHorse就是一個簡單的DOM勻速動畫繪制過程:
function?raceHorse(horseEl,?duration,?from?=?30,?by?=?trackLen?/?4)?{
??return?new?Promise((resolve)?=>?{
????const?startTime?=?Date.now();
????requestAnimationFrame(function?f()?{
??????let?p?=?(Date.now()?-?startTime)?/?duration;
??????p?=?Math.min(p,?1.0);
??????horseEl.style.left?=?`${from?+?p?*?by}px`;
??????if(p?<?1.0)?requestAnimationFrame(f);
??????else?resolve();
????});
??});
}
把完整的代碼匯總一下:
function?random(m,?n)?{
??return?m?+?Math.floor(Math.random()?*?n);
}
function?randomItem(arr,?from?=?0,?to?=?arr.length)?{
??const?index?=?random(from,?to);
??return?{
????index,
????value:?arr[index],
??};
}
function?shuffle(arr)?{
??for(let?i?=?arr.length;?i?>?0;?i--)?{
????const?{index}?=?randomItem(arr,?0,?i);
????[arr[index],?arr[i?-?1]]?=?[arr[i?-?1],?arr[index]];
??}
??return?arr;
}
const?prizeStorageKey?=?'prize10';
function?getResults()?{
??const?result?=?localStorage.getItem(prizeStorageKey);
??return?result???result.split(',')?:?[];
}
function?addResults(players)?{
??const?result?=?getResults();
??result.push(...players);
??localStorage.setItem(prizeStorageKey,?result.join());
}
function?filterWinner(members)?{
??const?winners?=?new?Set(getResults());
??return?members.filter(m?=>?!winners.has(m));
}
let?members?=?['胖虎',?'強夫',?'靜香',?'大雄',?'哆啦A夢',?'呂布',?'張飛',?'關羽',?'劉備',?'曹操',?'孫權',?'周瑜',
??'黃蓋',?'趙云',?'呂蒙',?'孫悟空',?'豬八戒',?'唐僧',?'沙悟凈',?'光頭強',?'熊大',?'熊二',
??'喜洋洋',?'美羊羊',?'紅太狼',?'灰太狼',
];
members?=?filterWinner(members);
const?startBtn?=?document.getElementById('start');
const?clearBtn?=?document.getElementById('clear');
startBtn.addEventListener('click',?async?()?=>?{
??startBtn.disabled?=?'disabled';
??clearBtn.disabled?=?'disabled';
??// 重新洗牌
??shuffle(members);
??// 取出最后6名同學,倒數3名中獎,剩下3名湊數
??const?candidates?=?members.slice(-6).reverse();
??// 將中獎結果保存到localStorage中
??addResults(candidates.slice(0,?3));
??members.length?-=?3;
??// 開始跑馬程序
??await?race(candidates);
??startBtn.disabled?=?'';
??clearBtn.disabled?=?'';
});
clearBtn.addEventListener('click',?()?=>?{
??// 清除所有中獎記錄
??localStorage.removeItem(prizeStorageKey);
});
const?trackLen?=?820;?// 205 * 4
const?trackEl?=?document.getElementById('track');
function?partRace(durations,?factor)?{
??// 根據賽程總時間 duration 和 factor 來劃分賽程
??// 賽程所用基準時間為 duration * factor,擾動 -0.1 ~ +0.1
??const?subDuration?=?durations.map(d?=>?d?*?factor?*?random(9,?11)?/?10);
??subDuration.map((d,?i)?=>?{
????durations[i]?-=?d;
????return?durations[i];
??});
??return?subDuration;
}
function?race(candidates)?{
??const?durations?=?[];
??for(let?i?=?0,?duration?=?0.9;?i?<?candidates.length;?i++)?{
????durations.push(duration);
????// 每一名次隨機增加 0.02 ~ 0.05 的時間
????duration?+=?random(2,?5)?*?0.01;
??}
??const?players?=?shuffle([...candidates.entries()]);
??trackEl.innerHTML?=?players.map((p,?i)?=>?{
????return?`
?${randomItem(['?',?'?',?'?',?'?']).value}
?${p[1]}?${i?+?1}
`;??}).join('');
??// 劃分4段賽程
??const?round1?=?partRace(durations,?0.25);
??const?round2?=?partRace(durations,?0.33);
??const?round3?=?partRace(durations,?0.5);
??const?round4?=?durations.map(d?=>?d?+?0.1);
??const?results?=?['?',?'?',?'?',?'?',?'?',?'?'];
??const?T?=?8000;
??const?horses?=?document.querySelectorAll('.horse');
??const?promises?=?[];
??for(let?i?=?0;?i?<?horses.length;?i++)?{
????const?horse?=?horses[i];
????const?idx?=?players[i][0];
????promises.push(raceHorse(horse,?round1[idx]?*?T)
??????.then(()?=>?{
????????return?raceHorse(horse,?round2[idx]?*?T,?30?+?trackLen?/?4);
??????})
??????.then(()?=>?{
????????return?raceHorse(horse,?round3[idx]?*?T,?30?+?2?*?trackLen?/?4);
??????})
??????.then(()?=>?{
????????return?raceHorse(horse,?round4[idx]?*?T,?30?+?3?*?trackLen?/?4);
??????})
??????.then(()?=>?{
????????horse.innerHTML?=?`${results[idx]}${horse.innerHTML}`;
????????return?raceHorse(horse,?0.1?*?T,?30?+?trackLen,?100);
??????}));
??}
??return?Promise.all(promises);
}
function?raceHorse(horseEl,?duration,?from?=?30,?by?=?trackLen?/?4)?{
??return?new?Promise((resolve)?=>?{
????const?startTime?=?Date.now();
????requestAnimationFrame(function?f()?{
??????let?p?=?(Date.now()?-?startTime)?/?duration;
??????p?=?Math.min(p,?1.0);
??????horseEl.style.left?=?`${from?+?p?*?by}px`;
??????if(p?<?1.0)?requestAnimationFrame(f);
??????else?resolve();
????});
??});
}
最終的效果:
以上就是今年奇舞團年會的抽獎程序,大家有什么想法可以關注我們的GitHub倉庫2與我們交流。
2019年年會的抽獎程序我也已經想好了,只會更加有趣,不過暫時不能透露?,2019年年會繼續加油~
文內鏈接
https://github.com/75team/raffle/tree/master/2017
https://github.com/75team/raffle
關于奇舞周刊
《奇舞周刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公眾號后,直接發送鏈接到后臺即可給我們投稿。
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的js年会抽奖_嘿!这真的是一个正经的抽奖程序!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET的命名空间
- 下一篇: shell脚本拼接中间带空格的两个变量成