cdxy.me
Footprints on Cyber Security and Python

0x01 游戏规则

该游戏名叫红包接龙,规则如下:

年会会场内所有人都通过钉钉群的方式参与该游戏,会场人数一般为200~300人(大部分能时候是超过红包最大拆分份数):

  1. 由老板发出第一个种子红包,金额 b = 500,红包分成100份,每份金额是随机的,红包发到钉钉群后,大家可以由两种选择:抢 or 不抢;
  2. 如果选择不抢,则本轮无损失也没有收益。
  3. 如果选择了抢,还需要拼手速,因为大部分时候选择抢的同学个数依然大于红包最大拆分份数;
  4. 所有红包拆分份数都被抢完之后,由本轮抢的最大金额的同学发下轮的红包,每轮的红包金额需要在之前的基础上增加200;
  5. 游戏循环进行,直到达到5000元上限结束;

0x02 红包算法

红包算法采用min,max之间随机生成:

  • min=0.01(一分钱)
  • max=剩余金额*2/剩余红包数
In [1]:
import random
import math
import numpy as np

class redpacket:
    def __init__(self, money, part):
        self.remain_money = money
        self.remain_size = part

    def get_random_money(self):
        if self.remain_size == 1:
            self.remain_size -= 1
            return round(self.remain_money * 100) / 100.0

        r = random.random()
        min = 0.01
        max = self.remain_money * 2 / float(self.remain_size)
        money = r * max
        money = money if money >= min else min
        money = math.floor(money * 100) / 100.0
        self.remain_size -= 1
        self.remain_money -= money
        return money

    def predict(self):
        ans = []
        for i in range(self.remain_size):
            current_round = self.get_random_money()
            ans.append(current_round)
        return ans
In [2]:
print redpacket(100,10).predict()
[16.22, 13.73, 9.48, 4.35, 0.95, 2.8, 11.64, 22.79, 8.55, 9.49]

0x03 领取顺序带来的影响

为了量化收益与领取红包的顺序的关系,我们按照领取顺序将用户分为1-10号,在总额100的状态下进行1000次试验,得到结论:

  • 收益期望不会被顺序影响,但是方差会,也就是说越靠后领取的红包数值波动越大
In [ ]:
from pyecharts import Bar

user_cnt = 10
times = 1000
test1k = [redpacket(100,10).predict() for i in range(times)]
attr = ['user_{}'.format(i+1) for i in range(user_cnt)]
v1 = np.around(np.average(test1k, axis=0), 2)
bar = Bar('收益期望')
bar.add('',attr,v1,mark_line=["average"],is_label_show=True,label_text_color='#000')
bar

png

In [ ]:
from pyecharts import Scatter

user_cnt = 10
times = 1000
ans_sca = np.array([redpacket(100,10).predict() for i in range(times)])
scatter = Scatter("收益散点图")
v1 = []
v2 = []
for i in range(user_cnt):
    v1.extend([i+1]*times)
    v2.extend(ans_sca[:,i])
scatter.add("", v1, v2,symbol_size=3)
scatter

png

由散点图可以看出,越靠后领取的红包金额波动(方差)越大,在1号用户视角,由于max值被限制在奖池余额*2/剩余红包个数 = 100*2/10 = 20之内,因此1号用户领取的金额只能在[0.01,20.]之内波动;而10号用户的波动区间更大,假设前9人全都是0.01,那10号可以领到99.91。

  • 红包金额波动会对游戏策略带来什么影响?我们加入游戏规则中的“处罚策略”,实现完整的实验代码。
In [6]:
class game:
    def __init__(self, people, round, start_money, step):
        self.people = people
        self.round = round
        self.start_money = start_money
        self.note = np.array([0] * people, dtype=float)
        self.step = step

    def start_game(self):
        for i in range(self.round):
            r = redpacket(self.start_money, self.people)
            ans = r.predict()
            self.note += np.array(ans, dtype=float)
            self.start_money += self.step
            # 处罚策略:本轮抢的最大金额的同学发下轮的红包,每轮的红包金额需要在之前的基础上增加N;
            if i < self.round - 1:  # 最后一轮不处罚
                best_score = max(ans)
                # print 'round:{},best:{}'.format(i, ans.index(best_score))
                self.note[ans.index(best_score)] -= self.start_money
        return self.note

实验:10个人,初始老板发一个500元的奖池,共30轮抽奖,每次抽到最大红包的人加200元成为下一轮奖池。

10000次实验的收益期望如下:

In [ ]:
from pyecharts import Bar

user_cnt = 10
times = 10000
result = [game(user_cnt, 30, start_money=500, step=200).start_game() for i in range(times)]
attr = ['user_{}'.format(i+1) for i in range(user_cnt)]
v1 = np.around(np.average(result, axis=0), 2)
bar = Bar('收益期望')
bar.add('',attr,v1,mark_line=["average"],is_label_show=True,label_text_color='#000')
bar

png

按照初始奖池500/10=50是总体收益的期望,我们可以看出收益是先拉升最终再降低,结果是便于理解的。

  1. 头部的节点,由于波动区间小,且靠近底部,每次只能拿到一个偏低的数额。
  2. 尾部的节点,由于波动范围大,如果拿到小数额那么收益有限,如果拿到大数额则很可能被处罚(下一轮发红包),因此尾部属于慈善玩家。

因此,如果我们能控制抢红包的顺序的话,可以在中前部出手,收益略高。具体位置需要通过人数、起步奖池、轮数、处罚金额来计算

0x04 自动抢红包脚本入场?

如果我们能用程序在每有一个人领取红包之后,迅速计算全盘状态并给出决策,会有什么变化?

  • 如果可以准确控制我们的入场位置,则可以计算出"绝对安全"的领取时机。

想象10个人抢100元红包的情况,第一个人直接命中了当前位置的上限20,后面180奖池+9人,下一轮max值=80*2/9<20,所以下一个领取红包的人无论抢多少,都不会成为本局最大值被处罚。

改进一下predict的过程,打印出每个位序是"绝对安全"(1)或者"有可能被惩罚"(0)

In [8]:
class redpacket: 
    def __init__(self, money, part):
        self.remain_money = money
        self.remain_size = part

    def get_random_money(self):
        if self.remain_size == 1:
            self.remain_size -= 1
            return round(self.remain_money * 100) / 100.0

        r = random.random()
        min = 0.01
        max = self.remain_money * 2 / float(self.remain_size)
        money = r * max
        money = money if money >= min else min
        money = math.floor(money * 100) / 100.0
        self.remain_size -= 1
        self.remain_money -= money
        return money

    def predict(self):
        ans = []
        ans_print = []
        for i in range(self.remain_size):
            ans_max = max(ans) if ans else 0
            if (self.remain_money * 2 / float(self.remain_size)) < ans_max:
                ans_print.append(1)  # 安全
            else:
                ans_print.append(0)
            current_round = self.get_random_money()
            ans.append(current_round)
        return ans, ans_print
In [9]:
for i in range(5):
    print redpacket(100,10).predict()
([0.05, 8.87, 15.79, 7.49, 8.27, 5.09, 22.44, 11.64, 4.23, 16.13], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0])
([1.29, 17.17, 1.16, 14.51, 12.3, 10.46, 20.98, 3.46, 5.78, 12.89], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0])
([3.09, 15.17, 15.21, 4.71, 13.02, 8.22, 16.7, 11.22, 7.1, 5.56], [0, 0, 0, 0, 0, 0, 0, 1, 1, 1])
([18.5, 7.61, 14.32, 4.55, 13.31, 1.73, 0.82, 18.35, 18.03, 2.78], [0, 1, 1, 1, 1, 1, 0, 0, 0, 1])
([3.69, 7.76, 5.67, 23.51, 4.77, 12.9, 6.38, 16.78, 17.2, 1.34], [0, 0, 0, 0, 1, 1, 1, 0, 1, 1])
In [10]:
test = [1 if 1 in redpacket(100,10).predict()[-1] else 0 for i in range(10000)]
print sum(test)/10000.0
0.8699
  • 从结果来看,在87%的游戏中我们可以等到"绝对安全"的位置出现。