一位赌狗前端的自我修养

17,065 阅读5分钟

这段时间工作巨多,直到今天才有时间写点东西。可是要输出点什么比较好呢?这时候看到了隔壁桌面上放着一张机打的写着一组一组数字的纸,有了!那要不抽个奖吧!

问题分析

抽奖,讲究一个随机,只要把随机摇号解决了不就不管什么规则都能抽了?所以我们需要一个可以产生随机数的函数。不仅要随机数,我们还需要的是一个闭区间随机取整数的函数。所以

const random = (m, n) => m + Math.floor(Math.random() * (n - m))

没吃过猪肉还见过猪跑,号码肯定是抽一个少一个,所以根本不存在随机数能相同的情况!所以这个方法是不能用的。那么,拿来抽的方法肯定得满足三个条件:

  • 有范围,是闭区间
  • 起始值肯定大于1
  • 随机数不能有重复

问题解决

关于随机不重复我最先想到的是利用sort对数组随机打散的方法。这个时候可能会有其他声音:

要不要这么水?sort用来排序的不懂?

水不水不知道,看一下MDN。

arr.sort([compareFunction])

  • 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前;
  • 如果 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变。备注: ECMAScript 标准并不保证这一行为,而且也不是所有浏览器都会遵守(例如 Mozilla 在 2003 年之前的版本);
  • 如果 compareFunction(a, b) 大于 0 , b 会被排列到 a 之前。
  • compareFunction(a, b) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。

so

// arr 数组,num 取个数
const random = (arr:number[], num: number) =>
  arr.sort(() => Math.random() - 0.5).splice(0, num)

具体问题具体解决

首先,我们需要通过百度查一下业界知名抽奖活动典型的游戏规则:

某抽奖游戏分为红色区和蓝色区,红色区由1-33共三十三个号码组成,蓝色区由1-16共十六个号码组成。需要在红色区选择6个号码和在蓝色区选择1个号码合成一次有效号码组。

作为赌狗前端,提取这段话几个关键点:

  • 红色区 1 ~ 33
  • 蓝色区 1 ~ 16
  • 取红 6 个,取蓝 1 个

别说了我脑子里又有函数了

// ...random

const dualColor = () => {
  const reds = [1, 2, ..33]
  const blues = [1, 2, ..16]

  const red = random(reds, 6)
  const blue = random(blues, 1)

  return [red, blue]
}

这个时候我们看一眼方法,你这方法不对啊!以红色为例子,我们的函数表达结果是:直接把号码打散后流出前 6 个,而游戏规则指出号码不仅一个一个放出,而且放的过程中并没有停止打散。所以这个随机函数显然不合理。那还能怎么办,改啊!

// 打散后流出第一个和剩下的号码组
function random(arr) {
  const newarr = arr.sort(() => Math.random() - 0.5)
  const val = newarr.shift()
  return [val, newarr]
}

function dualColor() {
  let redballs = [1, 2, ..33]
  let blueballs = [1, 2, ..16]
  let red = [], blue = []

  for (let i = 0; i < 6; i++) {
    const balls = random(redballs)
    red.push(balls[0])
    redballs = balls[1]
  }

  blue.push(random(blueballs)[0])

  return [red, blue]
}

做法优化

说实话这么写程序可憋死我了…

不会真的有人这么写数组吧

如果按照正常随机数方法那么做的话,我确实提供一个最大值一个最小值就结束了,关键这是个数组,又没有什么range之类的可以用,不会真的有人手写 1 ~ 33 吧。不过我们可以曲线捞一下

// 0 ~ 9
const arr = [...Array(10).keys()]

所以第一点改造两个球数组

  let reds: number[] = [...Array(33).keys()].map(i => i+1)
  let blues: number[] = [...Array(16).keys()].map(i => i+1)

为什么 +1 呢?号码也没从 0 开始的啊!

也许我们应该让随机函数更纯更通用?

我们需要让抽奖更灵活一些,毕竟只要是抽的,我全都要。我想让随机函数能接受一个数组和我想要的个数,再返回结果。

同样的会出现需要循环取值的情况,为什么我们不用神奇的尾递归呢?

function randomVal(
  arr: number[],
  total: number,
  temp: number[] = [],
): number[] {
  const [head, ...body] = arr
    .sort(() => Math.random() - 0.5)
  return !total
    ? temp
    : randomVal(body, total - 1, temp.concat(head));
}

俺寻思这两个号组写起来可太累了

改!都可以改!我们可以改成存放起始值和终止值的元组

function dualColor() {
  const reds: [number, number] = [1, 33]
  const blues: [number, number] =  [1, 16]

  return [randomVal(reds, 6), randomVal(blues, 1)]
}

相应的随机函数也做点小改动

function randomVal(
  fromto: number[],
  total: number,
  temp: number[] = [],
): number[] {
  const [head, ...body] = (temp.length
    ? fromto
    : [...Array(fromto[1]).keys()]
        .map(item => item + 1)
        .splice(fromto[0] - 1)
  ).sort(() => Math.random() - 0.5);
  return !total
    ? temp
    : randomVal(body, total - 1, temp.concat(head))
}

我好像看漏了规则

除此之外还有三种: (一)从红色区中选择7--20个,从蓝色区中选择1个。 (二)从红色区中选择6个,从蓝色区中选择2--16个。 (三)从红色区中选择7--20个,从蓝色区中选择2--16个。

虽然我看不懂,但是这么改就没问题了吧

// 我管你几个,全给你安排上!
function dualColor(red: number = 6, blue: number = 1) {
  const reds: [number, number] = [1, 33]
  const blues: [number, number] =  [1, 16]

  return [randomVal(reds, red), randomVal(blues, blue)]
}

所以最后解决方案是

function dualColor(red: number = 6, blue: number = 1) {
  const reds: [number, number] = [1, 33]
  const blues: [number, number] = [1, 16]

  return [randomBall(reds, red), randomBall(blues, blue)]
}

function randomBall(
  fromto: number[],
  total: number,
  temp: number[] = [],
): number[] {
  const [head, ...body] = (temp.length
    ? fromto
    : [...Array(fromto[1]).keys()]
        .splice(fromto[0] - 1)
        .map(item => item + 1)
  ).sort(() => Math.random() - 0.5);
  return !total
    ? temp
    : randomBall(body, total - 1, temp.concat(head))
}

赏心悦目?