Skip to main content

mingli_core/
sampler.rs

1//! S6 可审计随机种子 → 均匀抽样(家族 C 的随机源)。
2//!
3//! 占卜的「随机起卦」用**可复现的种子**驱动(反巴纳姆:种子留痕则同一占可复算)。
4//! 提供 splitmix64 PRNG、独立二进制位(易经/地占起卦)、无放回洗牌(塔罗/卢恩抽牌)。
5
6/// splitmix64:极简、确定性、可种子的 PRNG。
7#[derive(Debug, Clone)]
8pub struct SplitMix64 {
9    state: u64,
10}
11
12impl SplitMix64 {
13    /// 以给定种子构造(同种子 → 同序列,可复现)。
14    #[must_use]
15    pub fn new(seed: u64) -> Self {
16        SplitMix64 { state: seed }
17    }
18    /// 推进并返回下一个 64 位伪随机数。
19    pub fn next_u64(&mut self) -> u64 {
20        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
21        let mut z = self.state;
22        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
23        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
24        z ^ (z >> 31)
25    }
26    /// `[0, n)` 上近似均匀整数。
27    pub fn below(&mut self, n: u64) -> u64 {
28        self.next_u64() % n
29    }
30    /// 一个均匀二进制位(家族 C:易经一爻、地占一行)。
31    #[must_use]
32    pub fn bit(&mut self) -> bool {
33        self.next_u64() & 1 == 1
34    }
35}
36
37/// 无放回均匀洗牌(Fisher-Yates):返回 0..len 的一个置换,由种子决定、可复现。
38/// 塔罗/卢恩"抽 k 张" = 取此置换前 k 个。
39#[must_use]
40#[allow(
41    clippy::cast_possible_truncation,
42    reason = "j ∈ [0,i] ≤ len ≤ usize::MAX,回写 usize 不会截断"
43)]
44pub fn shuffle(len: usize, seed: u64) -> Vec<usize> {
45    let mut a: Vec<usize> = (0..len).collect();
46    let mut rng = SplitMix64::new(seed);
47    for i in (1..len).rev() {
48        let j = rng.below(i as u64 + 1) as usize;
49        a.swap(i, j);
50    }
51    a
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    /// SplitMix64 的输出向量——钉住具体的数,不只是「同种子同结果」。
59    ///
60    /// 本文件其余几条测试验的都是**自洽**:同种子给同结果、是个置换、不同种子不同、
61    /// bit() 两种值都出现。这些性质有一个共同的盲区——**换一套算术它们照样全绿**。
62    /// 而这个发生器是全仓「同一次取机可复核」那句承诺的根:易经的爻、地占的四母、
63    /// 塔罗的洗牌、Ifá、Sikidy 全从它来。它的输出一旦改变,历史上每一张带取机的盘
64    /// 都会静默变成另一张,且没有任何东西会红——契约快照也不行,
65    /// 那是同一个二进制跑两遍自比,两边一起变。
66    ///
67    /// 两个独立来源给出同一组数:
68    ///
69    /// - Vigna 的参考实现 `prng.di.unimi.it/splitmix64.c`(Java 8 SplittableRandom
70    ///   的定增量版本,doi:10.1145/2714064.2660195)。取回原文编译实跑,
71    ///   非仅阅读——下面 seed 0 / 1 / 0xDEADBEEF 三组各前五个数与本实现逐位相同。
72    /// - Rosetta Code《Pseudo-random numbers/Splitmix64》公布的期望输出:
73    ///   seed 1234567 前五个数为 6457827717110365317 / 3203168211198807973 /
74    ///   9817491932198370423 / 4593380528125082431 / 16408922859458223821。
75    ///
76    /// 两源均与本实现一致。改动这个函数就是改动一份对外承诺,不是重构。
77    #[test]
78    fn splitmix64_matches_the_published_reference_vectors() {
79        let take5 = |seed: u64| -> [u64; 5] {
80            let mut r = SplitMix64::new(seed);
81            [r.next_u64(), r.next_u64(), r.next_u64(), r.next_u64(), r.next_u64()]
82        };
83        assert_eq!(
84            take5(0),
85            [
86                0xE220_A839_7B1D_CDAF,
87                0x6E78_9E6A_A1B9_65F4,
88                0x06C4_5D18_8009_454F,
89                0xF88B_B8A8_724C_81EC,
90                0x1B39_896A_51A8_749B,
91            ],
92            "seed 0 的前五个输出与 Vigna 参考实现不符"
93        );
94        assert_eq!(
95            take5(1),
96            [
97                0x910A_2DEC_8902_5CC1,
98                0xBEEB_8DA1_658E_EC67,
99                0xF893_A2EE_FB32_555E,
100                0x71C1_8690_EE42_C90B,
101                0x71BB_54D8_D101_B5B9,
102            ],
103            "seed 1 的前五个输出与 Vigna 参考实现不符"
104        );
105        assert_eq!(
106            take5(0xDEAD_BEEF),
107            [
108                0x4ADF_B90F_68C9_EB9B,
109                0xDE58_6A31_41A1_0922,
110                0x021F_BC2F_8E1C_FC1D,
111                0x7466_CE73_7BE1_6790,
112                0x3BFA_8764_F685_BD1C,
113            ],
114            "seed 0xDEADBEEF 的前五个输出与 Vigna 参考实现不符"
115        );
116        // 第二个来源给的是十进制,照抄原样,不换算成十六进制再比——
117        // 换算这一步本身就是可能出错的地方
118        assert_eq!(
119            take5(1_234_567),
120            [
121                6_457_827_717_110_365_317,
122                3_203_168_211_198_807_973,
123                9_817_491_932_198_370_423,
124                4_593_380_528_125_082_431,
125                16_408_922_859_458_223_821,
126            ],
127            "seed 1234567 的前五个输出与 Rosetta Code 公布的期望值不符"
128        );
129    }
130
131    #[test]
132    fn deterministic_given_seed() {
133        assert_eq!(shuffle(78, 42), shuffle(78, 42)); // 同种子 → 同结果(可复现)
134    }
135
136    #[test]
137    fn shuffle_is_permutation() {
138        let mut s = shuffle(78, 12345); // 塔罗 78 张
139        s.sort_unstable();
140        assert_eq!(s, (0..78).collect::<Vec<_>>()); // 是 0..78 的置换(无放回)
141    }
142
143    #[test]
144    fn different_seeds_differ() {
145        assert_ne!(shuffle(78, 1), shuffle(78, 2));
146    }
147
148    #[test]
149    fn bit_and_below_cover() {
150        let mut rng = SplitMix64::new(7);
151        // bit() 在足够多次内应同时产出 true 和 false。
152        let mut seen_t = false;
153        let mut seen_f = false;
154        for _ in 0..64 {
155            if rng.bit() {
156                seen_t = true;
157            } else {
158                seen_f = true;
159            }
160        }
161        assert!(seen_t && seen_f);
162        // below(n) 恒在 [0,n)。
163        let mut rng2 = SplitMix64::new(7);
164        for _ in 0..100 {
165            assert!(rng2.below(6) < 6);
166        }
167    }
168
169    #[test]
170    fn shuffle_len_one_and_zero() {
171        assert_eq!(shuffle(1, 9), vec![0]);
172        assert!(shuffle(0, 9).is_empty());
173    }
174
175    use proptest::prelude::*;
176    proptest! {
177        #[test]
178        fn prop_splitmix_deterministic(seed in any::<u64>()) {
179            let (mut a, mut b) = (SplitMix64::new(seed), SplitMix64::new(seed));
180            for _ in 0..8 {
181                prop_assert_eq!(a.next_u64(), b.next_u64());
182            }
183        }
184        #[test]
185        fn prop_below_in_range(seed in any::<u64>(), n in 1u64..1_000_000) {
186            let mut r = SplitMix64::new(seed);
187            for _ in 0..16 {
188                prop_assert!(r.below(n) < n);
189            }
190        }
191        #[test]
192        fn prop_shuffle_is_permutation(len in 0usize..256, seed in any::<u64>()) {
193            let mut p = shuffle(len, seed);
194            prop_assert_eq!(p.len(), len);
195            p.sort_unstable();
196            prop_assert!(p.iter().copied().eq(0..len));
197        }
198    }
199}