"""A seeded random generator that is the same on every Python: xoshiro256** seeded through splitmix64 from a SHA-256 of the seed and a name. `sub(name)` gives an independent stream, so changing how many posts are drawn never reshuffles the follow graph.""" import hashlib MASK = (1 << 64) - 1 def _rotl(x, k): return ((x << k) | (x >> (64 - k))) & MASK def _splitmix(state): while True: state = (state + 0x9E3779B97F4A7C15) & MASK z = state z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & MASK z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & MASK yield z ^ (z >> 31) class Rng: def __init__(self, seed, name="root"): self.seed = seed self.name = name digest = hashlib.sha256(f"{seed}/{name}".encode()).digest() mix = _splitmix(int.from_bytes(digest[:8], "little")) self.s = [next(mix) for _ in range(4)] def sub(self, name): return Rng(self.seed, f"{self.name}/{name}") def next64(self): s = self.s result = (_rotl((s[1] * 5) & MASK, 7) * 9) & MASK t = (s[1] << 17) & MASK s[2] ^= s[0] s[3] ^= s[1] s[1] ^= s[2] s[0] ^= s[3] s[2] ^= t s[3] = _rotl(s[3], 45) return result def random(self): return (self.next64() >> 11) / float(1 << 53) def below(self, n): """An integer in [0, n).""" if n <= 0: raise ValueError("below() needs n > 0") limit = MASK - (MASK % n) while True: x = self.next64() if x < limit: return x % n def between(self, lo, hi): """An integer in [lo, hi].""" return lo + self.below(hi - lo + 1) def chance(self, p): return self.random() < p def choice(self, seq): return seq[self.below(len(seq))] def weighted(self, weights): """A key of {key: weight}, keys taken in sorted order so the draw does not depend on dict order.""" keys = sorted(k for k, w in weights.items() if w > 0) total = sum(weights[k] for k in keys) x = self.random() * total for k in keys: x -= weights[k] if x < 0: return k return keys[-1] def shuffle(self, items): items = list(items) for i in range(len(items) - 1, 0, -1): j = self.below(i + 1) items[i], items[j] = items[j], items[i] return items def sample(self, seq, k): return self.shuffle(seq)[:max(0, min(k, len(seq)))] def span(self, pair): """A value drawn from [lo, hi] given as a two-item list, or the value itself.""" if isinstance(pair, (list, tuple)): return self.between(pair[0], pair[1]) return pair