lua math.random伪随机问题浅析

我们的一般编写随机如下:

-- 获取当前系统时间(秒)作为随机种子,有时也会使用毫秒:os.time() * 1000
math.randomseed(os.time())

--[[
参数的方式:
1. 不带参数调用时,获取的是[0,1)范围内的随机浮点数
2. 带一个整型参数时,获取的是[1,n]范围内的随机整数
3. 带两个整型参数m,n时,获取的是[m,n]范围内随机整数
注意Lua5.3以后,参数一定要为整数,否则会返回错误:
bad argument #1 to 'random' (number has no integer representation)
]]
math.random(10, 30)

为避免伪随机,为何要使用os.time()获取系统时间秒数作为种子呢?接下来我们看下lua中的random,randomseed在C下的实现,参考资料:

lua源码下载:http://www.lua.org/ftp/

在线lua C库:http://www.lua.org/source/5.1/

Math库:http://lua-users.org/wiki/MathLibraryTutorial

我们可以摘录下lmathlib.c下关于random,randomseed C实现相关:

// 摘录:http://www.lua.org/source/5.1/lmathlib.c.html

static const luaL_Reg mathlib[] = {
  {"random",     math_random},
  {"randomseed", math_randomseed},
  {NULL, NULL}
};

static int math_random (lua_State *L) {
  /* the `%' avoids the (rare) case of r==1, and is needed also because on
     some systems (SunOS!) `rand()' may return a value larger than RAND_MAX */
  lua_Number r = (lua_Number)(rand()%RAND_MAX) / (lua_Number)RAND_MAX;
  switch (lua_gettop(L)) {  /* 检测参数 */
    case 0: {  /* no arguments */
      lua_pushnumber(L, r);  /* Number between 0 and 1 */
      break;
    }
    case 1: {  /* only upper limit */
      int u = luaL_checkint(L, 1);
      luaL_argcheck(L, 1<=u, 1, "interval is empty");
      lua_pushnumber(L, floor(r*u)+1);  /* int between 1 and `u' */
      break;
    }
    case 2: {  /* lower and upper limits */
      int l = luaL_checkint(L, 1);
      int u = luaL_checkint(L, 2);
      luaL_argcheck(L, l<=u, 2, "interval is empty");
      lua_pushnumber(L, floor(r*(u-l+1))+l);  /* int between `l' and `u' */
      break;
    }
    default: return luaL_error(L, "wrong number of arguments");
  }
  return 1;
}

static int math_randomseed (lua_State *L) {
  srand(luaL_checkint(L, 1));
  return 0;
}

看到如上代码,我们可以了解:

1. 未调用srand,其next随机种子为1, 此时rand() 产生的随机数每次运行都会与上一次相同。

2. 若next随机种子设置的越大,伪随机的可能性就越小。比如:

-- 随机种子数值为100,每次执行的结果都是:1 1 1 3 4 2 4 1 4 1
math.randomseed(100)
-- 随机种子数字为os.time(),每次执行结果会发生改变
math.randomseed(os.time())
local randNum = {}
for i = 1,10 do
    table.insert(randNum,math.random(1,5))
end
print(table.concat(randNum," "))

在cocos2d-lua下的function.lua中实现了新的随机数种子:

-- 摘录于: src/cocos/cocos2d/functions.lua
-- 设置随机数种子
function math.newrandomseed()
    local ok, socket = pcall(function()
        return require("socket")
    end)

    if ok then
        // 关于*1000,在32位机器上,可能数值超过机器的最大值限定,而使得设置种子无效
        math.randomseed(socket.gettime() * 1000)        
    else
        math.randomseed(os.time())     -- 此处可修改为数值反转
    end
    math.random()
    math.random()
    math.random()
    math.random()
end

但是你的程序如果在1秒内有多次操作(密码锁),产生的随机数依然存在伪随机。因此我们可以这样:

-- 将os.time()获取的系统秒数数值翻转(低位变高位),再取高6位,这样即使time变化很小
-- 但是由于低位变高位,种子数值就会变化很大,这样1秒内进行多次运行的话,效果会好些
local next = tostring(os.time()):reverse():sub(1, 6)
math.randomseed(next)