Home » 项目 » 【笔记】Python3 中模拟 Minecraft 史莱姆区块算法

【笔记】Python3 中模拟 Minecraft 史莱姆区块算法

发布于 April 18, 2022 项目

分析

Minecraft 主要用 Java 编写,史莱姆区块的生成是采用了伪随机算法。伪随机意为在同样的种子下,生成的结果是相同的。所以当有了种子之后,可以推算出几乎所有自然生成结构的位置。甚至,如果你有大量有效的自然生成结构位置,可以反推出种子。

// https://minecraft.fandom.com/wiki/Slime
import java.util.Random; 
public class checkSlimechunk{ 
    public static void main(String args[]) 
    { 
        // the seed from /seed as a 64bit long literal
        long seed = 12345L;
        int xPosition = 123;
        int zPosition = 456;
        Random rnd = new Random(
            // 第一部分
            seed +
            // 第二部分
            (int) (xPosition * xPosition * 0x4c1906) +
            // 第三部分
            (int) (xPosition * 0x5ac0db) +
            // 第四部分
            (int) (zPosition * zPosition) * 0x4307a7L +
            //第五部分
            (int) (zPosition * 0x5f24f)
            ^ 0x3ad8025fL
        );
        System.out.println(rnd.nextInt(10) == 0);
    } 
}

这是wiki中扒出的游戏算法,这段代码可以理解为:伪随机种子由5部分构成,五部分相加后按位异或一个 long 值得出进入随机前的最终种子。五部分中,一共需要3个输入值,分别是 long 类种子、int 类区块x坐标、int 类区块z坐标。

  • 五部分的第一部分是游戏中 /seed 获得的种子,类是 long 。按照 intlong 之间加减的转换顺序和随机种子算式的优先级,所有之后的五部分都会最终转换成 long
  • 第二部分为x坐标平方再乘一个十六进制数 0x4c1906,换算成十进制就是 4987142,在 Python 中不需要换成十进制计算,直接相乘。
  • 第三部分同理。
  • 第四部分注意,z平方是在 int 类中完成计算,在乘括号外的十进制时需要转化成 long
  • 最后同样也是一个 int 类计算,要注意最后的<u>按位异或的优先级在java中是比加减乘除要低的</u>,所以应该是所有部分加起来后再运算。

在RNG对象赋值好后,最后是用 .nextInt() 输出一个0-9的数字,如果数字是 0 ,则该区块是史莱姆区块。

实现

框架构建
class slime(object):
    def __init__(self):
        self.seed = None
        self.x = None
        self.z = None

if __name__ == "__main__":
    obj = slime()
Random Number Generator 种子计算
def javaInt(self, val):
    limit = 2147483647
    if not (-1*limit)-1 <= val <= limit:
        val = (val+(limit+1))%(2*(limit+1))-limit-1
    return val
def javaLong(self, val):
    limit = 9223372036854775807
    if not (-1*limit)-1 <= val <= limit:
        val = (val+(limit+1))%(2*(limit+1))-limit-1
    return val

Python 3中int类大小是无限的,而java中int和long都有上限。越过限制后,会从另一端继续计算,例如int的上限是 2,147,483,647,这个数+1等于-2,147,483,648,+2等于-2,147,483,647 以此类推。根据上方算法分析,坐标乘以的十六进制数算是比较大的,所以有很大可能性会过界,所以必须要有一种溢出算法能让python的不过界数模你出 Java的过界效果。

def rngSeed(self):
    a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
    b = self.javaLong(self.javaInt(self.x*0x5ac0db))
    c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
    d = self.javaLong(self.javaInt(self.z*0x5f24f))
    return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))

设置好溢出后套入算法即可,需要注意 Java的long类会在对象后加一个L和int类区分开来,在python中要把L去掉,比如 0x3ad8025fL 就变成 0x3ad8025f。

模拟 java.util.Random 算法
# https://github.com/MostAwesomeDude/java-random
class javaRandom(object):
    def __init__(self):
        self.seed = None
    def setSeed(self, seed):
        self.seed = (seed^0x5deece66d)&((1<<48)-1)
    def next(self, bits):
        if bits < 1:
            bits = 1
        elif bits > 32:
            bits = 32
        self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
        retval = self.seed>>(48-bits)
        if retval & (1<<31):
            retval -= (1<<32)
        return retval
    def nextInt(self, n):
        if n > 1:
            if not (n&(n-1)):
                return (n*self.next(31))>>31
            bits = self.next(31)
            val = bits%n
            while (bits-val+n-1) < 0:
                bits = self.next(31)
                val = bits%n
            return val

参考 MostAwesomeDude 的模拟 Java 伪随机算法,去除了不需要的一些next方法,只留下了分析中需要的 .nextInt。这个类是参照官方文档对 Random() 定义写出的,并且做了Python的适配,可以直接使用。

关于 Java Random() 的介绍,参考 java.util - Class Random

结果转换
def check(self, seed, x, z):
    # 输入
    self.seed = seed
    self.x = x
    self.z = z
    # 最终种子
    rng = self.rngSeed()
    # 初始化
    obj = javaRandom()
    # 设置最终种子
    obj.setSeed(rng)
    # 得到结果
    rtn = obj.nextInt(10)
    # 判断结果
    if rtn == 0:
        return True
    else:
        return False

总结

源码
class slime(object):
    def __init__(self):
        self.seed = None
        self.x = None
        self.z = None
    def javaInt(self, val):
        limit = 2147483647
        if not (-1*limit)-1 <= val <= limit:
            val = (val+(limit+1))%(2*(limit+1))-limit-1
        return val
    def javaLong(self, val):
        limit = 9223372036854775807
        if not (-1*limit)-1 <= val <= limit:
            val = (val+(limit+1))%(2*(limit+1))-limit-1
        return val
    def rngSeed(self):
        a = self.javaLong(self.javaInt((self.x**2)*0x4c1906))
        b = self.javaLong(self.javaInt(self.x*0x5ac0db))
        c = self.javaLong((self.javaInt(self.z**2)*0x4307a7))
        d = self.javaLong(self.javaInt(self.z*0x5f24f))
        return self.javaLong(((self.seed+a+b+c+d)^0x3ad8025f))
    def check(self, seed, x, z):
        self.seed = seed
        self.x = x
        self.z = z
        rng = self.rngSeed()
        obj = javaRandom()
        obj.setSeed(rng)
        rtn = obj.nextInt(10)
        if rtn == 0:
            return True
        else:
            return False

class javaRandom(object):
    def __init__(self):
        self.seed = None
    def setSeed(self, seed):
        self.seed = (seed^0x5deece66d)&((1<<48)-1)
    def next(self, bits):
        if bits < 1:
            bits = 1
        elif bits > 32:
            bits = 32
        self.seed = (self.seed*0x5deece66d+0xb)&((1<<48)-1)
        retval = self.seed>>(48-bits)
        if retval & (1<<31):
            retval -= (1<<32)
        return retval
    def nextInt(self, n):
        if n > 1:
            if not (n&(n-1)):
                return (n*self.next(31))>>31
            bits = self.next(31)
            val = bits%n
            while (bits-val+n-1) < 0:
                bits = self.next(31)
                val = bits%n
            return val

if __name__ == "__main__":
    obj = slime()
    exe = obj.check(seed=1664026956323281049, x=-9, z=-17)
    print(exe)
    
返回
$ python3 demo.py
True
Add Comment