用JS写前端可预测的随机

从一次游戏活动页面学习到的JS完成前端运行可预测、可控制的随机文字生成。

前言

“我要实现一个随机效果,把预设的文案打乱排列”

—— Math.random就能搞定

“需要做到每次进来,看到的结果和上次一样。”

—— 那我用localstorage存起来吧

“要是我换了手机、换了浏览器,也要看到一致的结果。”

—— 额,好像需要后台记录了。

“后台说为每个用户记录所有的文案压力可能会比较大”

我们把问题转化一下,变成“如何在前端写可控的随机,每次都能得到确定的结果。

以上就是刺激战场世界杯期间的一个活动需求。前端根据比分和用户id,生成的一场模拟的文字直播,效果如下。

一场在前端生成的文字直播

逻辑整理

一场在前端生成的文字直播

流程中循环的那部分,算作一个回合。每个回合,不管是进攻成功还是失败,都是以“进攻中”开始,以“交换球权”作为结束;成功失败只影响是否积分。

文案分类

文案是按照进攻中、进攻成功、进攻失败、氛围类来分类的。

一场在前端生成的文字直播

比赛格局生成

规则定了,先生成比赛格局,比如先谁谁谁发起进攻了,谁谁谁进球了,但具体里面的文案后面再填充。

这里以双方最终比分2:3的情况来举例。

第一步,将甲乙两方分数(进球的块)随机 汇合成一个数组

一场在前端生成的文字直播

第二步,每个进球前加上一个“进攻中”

一场在前端生成的文字直播

第三步,如果两次得分是同一方,之间加入对方“进攻中”+“进攻失败”的块(交换球权)

一场在前端生成的文字直播

填充内容

比赛的格局已经定型,把对应的文字 随机 挑选填充进去

一场在前端生成的文字直播

有时总比分和太小,比如1:1甚至1:0的情况,会造成回合太少很快结束。

可在数组中增加一些“进攻中”+“进攻失败”的块,不影响比分。增加偶数组,偶数次交换球权,就又回到原来的球权所在方。

这种增加的组数可根据总分和来决定。比如,在现有的每组间隔中,分别再手动插入(5减去总分和)组。

此外有一些场面描写、烘托氛围的语句,和比赛分数、进程没有直接关系的,插入的位置可以使用固定,比如在20%、40%、60%、80%的位置固定插入。

从上面的分析可以看到,难点就在这里的 随机 ,比赛格局的设定、文案数组的打乱,都需要看起来是随机,又要同一场比赛保持可确定性。

随机的核心代码

核心代码主要分为3部分:

  1. 获取随机种子
  2. 随机函数
  3. 根据随机函数生成的随机数,打乱数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function shuffle(array, seed){
// seed:string
let currentIndex = array.length, temporaryValue, randomIndex;
seed = (seed?seed+'' : '');
let seedNum=1;
for (let index = 0; index < seed.length; index++) {
const element = seed[index];
seedNum+=element.charCodeAt(0);
}

// Generate a random num using seed
// theoretical range:[0,1]
// actual range:(0,1)
let random = function() {
let x = Math.sin(seedNum++) * 10000;
return x - Math.floor(x);
};

// Shuffle array
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}

第一步的随机种子获取,这次专题的每次比赛都会有一个比赛的加密字符串,刚好可以用来作为seed种子。

第二步的随机函数其实是根据sine函数,取预设的取样点处的值的小数部分,每调用一次,取样点的位置+1,所以,定义好之后,在随机种子确定的情况下,第n次调用的值是确定的。

虽然严格来说,这样的方式不是完全平均分布的随机,但已经可以满足看起来随机的需求。

第三部分的数组打乱,方法是从数组最后一项开始,和前面随机挑选的一个项交换,然后倒数第二项,然后倒数第三项……直到第一项。

前端随机的其他应用

前端随机的方式,还可以在一些抽签类的小程序中应用。我第一次见到是在全军出击签到小程序中看到,可以利用用户的openid来作为种子生成运势的签序列,然后根据日期作为index去取当天的运势。

参考文献

How to randomize (shuffle) a JavaScript array?

Seeding the random number generator in Javascript